diff --git a/packages/delegation-core/README.md b/packages/delegation-core/README.md index 9b083cde..0c8a9cf1 100644 --- a/packages/delegation-core/README.md +++ b/packages/delegation-core/README.md @@ -67,8 +67,8 @@ Creates terms for a Timestamp caveat that enforces time-based constraints on del **Parameters:** - `terms: TimestampTerms` - - `timestampAfterThreshold: number` - Timestamp (seconds) after which delegation can be used - - `timestampBeforeThreshold: number` - Timestamp (seconds) before which delegation can be used + - `afterThreshold: number` - Timestamp (seconds) after which delegation can be used + - `beforeThreshold: number` - Timestamp (seconds) before which delegation can be used - `options?: EncodingOptions` - Optional encoding options **Returns:** `Hex | Uint8Array` - 32-byte encoded terms (16 bytes per timestamp) @@ -80,14 +80,14 @@ import { createTimestampTerms } from '@metamask/delegation-core'; // Valid between Jan 1, 2022 and Jan 1, 2023 const terms = createTimestampTerms({ - timestampAfterThreshold: 1640995200, // 2022-01-01 00:00:00 UTC - timestampBeforeThreshold: 1672531200, // 2023-01-01 00:00:00 UTC + afterThreshold: 1640995200, // 2022-01-01 00:00:00 UTC + beforeThreshold: 1672531200, // 2023-01-01 00:00:00 UTC }); // Only valid after a certain time (no end time) const openEndedTerms = createTimestampTerms({ - timestampAfterThreshold: 1640995200, - timestampBeforeThreshold: 0, + afterThreshold: 1640995200, + beforeThreshold: 0, }); ``` @@ -354,8 +354,8 @@ export type ValueLteTerms = { }; export type TimestampTerms = { - timestampAfterThreshold: number; - timestampBeforeThreshold: number; + afterThreshold: number; + beforeThreshold: number; }; export type ExactCalldataTerms = { diff --git a/packages/delegation-core/src/caveats/allowedCalldata.ts b/packages/delegation-core/src/caveats/allowedCalldata.ts index c2a5a180..878a01d2 100644 --- a/packages/delegation-core/src/caveats/allowedCalldata.ts +++ b/packages/delegation-core/src/caveats/allowedCalldata.ts @@ -1,9 +1,24 @@ +/** + * ## AllowedCalldataEnforcer + * + * Constrains the calldata bytes starting at a given byte offset to match an expected fragment. + * + * Terms are encoded as a 32-byte big-endian start index followed by the expected calldata bytes (not ABI-wrapped). + */ + import { bytesToHex, remove0x, type BytesLike } from '@metamask/utils'; -import { toHexString } from '../internalUtils'; import { + assertHexBytesMinLength, + extractNumber, + extractRemainingHex, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,9 +27,9 @@ import type { Hex } from '../types'; /** * Terms for configuring an AllowedCalldata caveat. */ -export type AllowedCalldataTerms = { +export type AllowedCalldataTerms = { startIndex: number; - value: BytesLike; + value: TBytesLike; }; /** @@ -23,7 +38,7 @@ export type AllowedCalldataTerms = { * * @param terms - The terms for the AllowedCalldata caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as the calldata itself. + * @returns Encoded terms. * @throws Error if the `calldata` is invalid. */ export function createAllowedCalldataTerms( @@ -40,7 +55,7 @@ export function createAllowedCalldataTerms( * * @param terms - The terms for the AllowedCalldata caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as the calldata itself. + * @returns Encoded terms. * @throws Error if the `calldata` is invalid. */ export function createAllowedCalldataTerms( @@ -70,6 +85,47 @@ export function createAllowedCalldataTerms( const indexHex = toHexString({ value: startIndex, size: 32 }); - // The terms are the index encoded as 32 bytes followed by the expected value. return prepareResult(`0x${indexHex}${unprefixedValue}`, encodingOptions); } + +/** + * Decodes terms for an AllowedCalldata caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether the decoded value fragment is returned as hex or bytes. + * @returns The decoded AllowedCalldataTerms object. + */ +export function decodeAllowedCalldataTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): AllowedCalldataTerms>; +export function decodeAllowedCalldataTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): AllowedCalldataTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether the decoded value fragment is returned as hex or bytes. + * @returns The decoded AllowedCalldataTerms object. + */ +export function decodeAllowedCalldataTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | AllowedCalldataTerms> + | AllowedCalldataTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexBytesMinLength( + hexTerms, + 32, + 'Invalid AllowedCalldata terms: must be at least 32 bytes', + ); + + const startIndex = extractNumber(hexTerms, 0, 32); + const valueHex = extractRemainingHex(hexTerms, 32); + const value = prepareResult(valueHex, encodingOptions); + + return { startIndex, value } as + | AllowedCalldataTerms> + | AllowedCalldataTerms>; +} diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index 65af5189..703b067a 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -1,9 +1,24 @@ +/** + * ## AllowedMethodsEnforcer + * + * Specifies 4 byte method selectors that the delegate is allowed to call. + * + * Terms are encoded as a concatenation of 4-byte function selectors with no padding between selectors. + */ + import { bytesToHex, isHexString, type BytesLike } from '@metamask/utils'; -import { concatHex } from '../internalUtils'; import { + assertHexByteLengthAtLeastOneMultipleOf, + concatHex, + extractHex, + getByteLength, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,9 +27,9 @@ import type { Hex } from '../types'; /** * Terms for configuring an AllowedMethods caveat. */ -export type AllowedMethodsTerms = { +export type AllowedMethodsTerms = { /** An array of 4-byte method selectors that the delegate is allowed to call. */ - selectors: BytesLike[]; + selectors: TBytesLike[]; }; const FUNCTION_SELECTOR_STRING_LENGTH = 10; // 0x + 8 hex chars @@ -26,7 +41,7 @@ const INVALID_SELECTOR_ERROR = * * @param terms - The terms for the AllowedMethods caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated method selectors. + * @returns Encoded terms. * @throws Error if the selectors array is empty or contains invalid selectors. */ export function createAllowedMethodsTerms( @@ -42,7 +57,7 @@ export function createAllowedMethodsTerms( * * @param terms - The terms for the AllowedMethods caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated method selectors. + * @returns Encoded terms. * @throws Error if the selectors array is empty or contains invalid selectors. */ export function createAllowedMethodsTerms( @@ -76,3 +91,50 @@ export function createAllowedMethodsTerms( const hexValue = concatHex(normalizedSelectors); return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an AllowedMethods caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded selector values are returned as hex or bytes. + * @returns The decoded AllowedMethodsTerms object. + */ +export function decodeAllowedMethodsTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): AllowedMethodsTerms>; +export function decodeAllowedMethodsTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): AllowedMethodsTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded selector values are returned as hex or bytes. + * @returns The decoded AllowedMethodsTerms object. + */ +export function decodeAllowedMethodsTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | AllowedMethodsTerms> + | AllowedMethodsTerms> { + const hexTerms = bytesLikeToHex(terms); + + const selectorSize = 4; + assertHexByteLengthAtLeastOneMultipleOf( + hexTerms, + selectorSize, + 'Invalid selectors: must be a multiple of 4', + ); + const selectorCount = getByteLength(hexTerms) / selectorSize; + + const selectors: (Hex | Uint8Array)[] = []; + for (let i = 0; i < selectorCount; i++) { + const selector = extractHex(hexTerms, i * selectorSize, selectorSize); + selectors.push(prepareResult(selector, encodingOptions)); + } + + return { selectors } as + | AllowedMethodsTerms> + | AllowedMethodsTerms>; +} diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index eae6056b..0e3be495 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -1,9 +1,25 @@ +/** + * ## AllowedTargetsEnforcer + * + * Restricts which contract addresses the delegate may call. + * + * Terms are encoded as the concatenation of 20-byte addresses in order with no padding between addresses. + */ + import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress } from '../internalUtils'; import { + assertHexByteLengthAtLeastOneMultipleOf, + concatHex, + extractAddress, + getByteLength, + normalizeAddress, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,9 +28,9 @@ import type { Hex } from '../types'; /** * Terms for configuring an AllowedTargets caveat. */ -export type AllowedTargetsTerms = { +export type AllowedTargetsTerms = { /** An array of target addresses that the delegate is allowed to call. */ - targets: BytesLike[]; + targets: TBytesLike[]; }; /** @@ -22,7 +38,7 @@ export type AllowedTargetsTerms = { * * @param terms - The terms for the AllowedTargets caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated target addresses. + * @returns Encoded terms. * @throws Error if the targets array is empty or contains invalid addresses. */ export function createAllowedTargetsTerms( @@ -38,7 +54,7 @@ export function createAllowedTargetsTerms( * * @param terms - The terms for the AllowedTargets caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated target addresses. + * @returns Encoded terms. * @throws Error if the targets array is empty or contains invalid addresses. */ export function createAllowedTargetsTerms( @@ -60,3 +76,50 @@ export function createAllowedTargetsTerms( const hexValue = concatHex(normalizedTargets); return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an AllowedTargets caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded AllowedTargetsTerms object. + */ +export function decodeAllowedTargetsTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): AllowedTargetsTerms>; +export function decodeAllowedTargetsTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): AllowedTargetsTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded AllowedTargetsTerms object. + */ +export function decodeAllowedTargetsTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | AllowedTargetsTerms> + | AllowedTargetsTerms> { + const hexTerms = bytesLikeToHex(terms); + + const addressSize = 20; + assertHexByteLengthAtLeastOneMultipleOf( + hexTerms, + addressSize, + 'Invalid targets: must be a multiple of 20', + ); + const addressCount = getByteLength(hexTerms) / addressSize; + + const targets: (Hex | Uint8Array)[] = []; + for (let i = 0; i < addressCount; i++) { + const target = extractAddress(hexTerms, i * addressSize); + targets.push(prepareResult(target, encodingOptions)); + } + + return { targets } as + | AllowedTargetsTerms> + | AllowedTargetsTerms>; +} diff --git a/packages/delegation-core/src/caveats/argsEqualityCheck.ts b/packages/delegation-core/src/caveats/argsEqualityCheck.ts index 4be828cb..3ae764e8 100644 --- a/packages/delegation-core/src/caveats/argsEqualityCheck.ts +++ b/packages/delegation-core/src/caveats/argsEqualityCheck.ts @@ -1,9 +1,19 @@ +/** + * ## ArgsEqualityCheckEnforcer + * + * Requires args on the caveat to equal an expected byte sequence. + * + * Terms are encoded as the raw expected args hex. + */ + import type { BytesLike } from '@metamask/utils'; import { normalizeHex } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,9 +22,9 @@ import type { Hex } from '../types'; /** * Terms for configuring an ArgsEqualityCheck caveat. */ -export type ArgsEqualityCheckTerms = { +export type ArgsEqualityCheckTerms = { /** The expected args that must match exactly when redeeming the delegation. */ - args: BytesLike; + args: TBytesLike; }; /** @@ -22,7 +32,7 @@ export type ArgsEqualityCheckTerms = { * * @param terms - The terms for the ArgsEqualityCheck caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as the args themselves. + * @returns Encoded terms. * @throws Error if args is not a valid hex string. */ export function createArgsEqualityCheckTerms( @@ -38,7 +48,7 @@ export function createArgsEqualityCheckTerms( * * @param terms - The terms for the ArgsEqualityCheck caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as the args themselves. + * @returns Encoded terms. * @throws Error if args is not a valid hex string. */ export function createArgsEqualityCheckTerms( @@ -58,3 +68,36 @@ export function createArgsEqualityCheckTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ArgsEqualityCheck caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded args are returned as hex or bytes. + * @returns The decoded ArgsEqualityCheckTerms object. + */ +export function decodeArgsEqualityCheckTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ArgsEqualityCheckTerms>; +export function decodeArgsEqualityCheckTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ArgsEqualityCheckTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded args are returned as hex or bytes. + * @returns The decoded ArgsEqualityCheckTerms object. + */ +export function decodeArgsEqualityCheckTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ArgsEqualityCheckTerms> + | ArgsEqualityCheckTerms> { + const argsHex = bytesLikeToHex(terms); + const args = prepareResult(argsHex, encodingOptions); + return { args } as + | ArgsEqualityCheckTerms> + | ArgsEqualityCheckTerms>; +} diff --git a/packages/delegation-core/src/caveats/blockNumber.ts b/packages/delegation-core/src/caveats/blockNumber.ts index 89c6f6db..e37d5a99 100644 --- a/packages/delegation-core/src/caveats/blockNumber.ts +++ b/packages/delegation-core/src/caveats/blockNumber.ts @@ -1,5 +1,20 @@ -import { toHexString } from '../internalUtils'; +/** + * ## BlockNumberEnforcer + * + * Restricts redemption to a block number range (strict inequalities on-chain: valid when `block.number > afterThreshold` if after is set, and `block.number < beforeThreshold` if before is set). + * + * Terms are encoded as a 16-byte after threshold followed by a 16-byte before threshold (each big-endian, zero-padded; interpreted as `uint128`). + */ + +import type { BytesLike } from '@metamask/utils'; + import { + assertHexByteExactLength, + extractBigInt, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -22,7 +37,7 @@ export type BlockNumberTerms = { * * @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). + * @returns Encoded terms. * @throws Error if both thresholds are zero or if afterThreshold >= beforeThreshold when both are set. */ export function createBlockNumberTerms( @@ -38,7 +53,7 @@ export function createBlockNumberTerms( * * @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). + * @returns Encoded terms. * @throws Error if both thresholds are zero or if afterThreshold >= beforeThreshold when both are set. */ export function createBlockNumberTerms( @@ -69,3 +84,21 @@ export function createBlockNumberTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a BlockNumber caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @returns The decoded BlockNumberTerms object. + */ +export function decodeBlockNumberTerms(terms: BytesLike): BlockNumberTerms { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 32, + 'Invalid BlockNumber terms: must be exactly 32 bytes', + ); + const afterThreshold = extractBigInt(hexTerms, 0, 16); + const beforeThreshold = extractBigInt(hexTerms, 16, 16); + return { afterThreshold, beforeThreshold }; +} diff --git a/packages/delegation-core/src/caveats/deployed.ts b/packages/delegation-core/src/caveats/deployed.ts index 5e573489..a75cb78e 100644 --- a/packages/delegation-core/src/caveats/deployed.ts +++ b/packages/delegation-core/src/caveats/deployed.ts @@ -1,10 +1,28 @@ +/** + * ## DeployedEnforcer + * + * Constrains contract deployment to a specific address, salt, and bytecode. + * + * Terms are encoded as 20-byte contract address, 32-byte left-padded salt, then creation bytecode bytes. + */ + import type { BytesLike } from '@metamask/utils'; import { remove0x } from '@metamask/utils'; -import { concatHex, normalizeAddress, normalizeHex } from '../internalUtils'; import { + assertHexBytesMinLength, + concatHex, + extractAddress, + extractHex, + extractRemainingHex, + normalizeAddress, + normalizeHex, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -13,13 +31,13 @@ import type { Hex } from '../types'; /** * Terms for configuring a Deployed caveat. */ -export type DeployedTerms = { +export type DeployedTerms = { /** The contract address. */ - contractAddress: BytesLike; + contractAddress: TBytesLike; /** The deployment salt. */ - salt: BytesLike; + salt: TBytesLike; /** The contract bytecode. */ - bytecode: BytesLike; + bytecode: TBytesLike; }; /** @@ -27,7 +45,7 @@ export type DeployedTerms = { * * @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. + * @returns Encoded terms. * @throws Error if the contract address, salt, or bytecode is invalid. */ export function createDeployedTerms( @@ -43,7 +61,7 @@ export function createDeployedTerms( * * @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. + * @returns Encoded terms. * @throws Error if the contract address, salt, or bytecode is invalid. */ export function createDeployedTerms( @@ -74,3 +92,49 @@ export function createDeployedTerms( const hexValue = concatHex([contractAddressHex, paddedSalt, bytecodeHex]); return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a Deployed caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded address, salt, and bytecode are returned as hex or bytes. + * @returns The decoded DeployedTerms object. + */ +export function decodeDeployedTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): DeployedTerms>; +export function decodeDeployedTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): DeployedTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded address, salt, and bytecode are returned as hex or bytes. + * @returns The decoded DeployedTerms object. + */ +export function decodeDeployedTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | DeployedTerms> + | DeployedTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexBytesMinLength( + hexTerms, + 52, + 'Invalid Deployed terms: must be at least 52 bytes', + ); + + const contractAddressHex = extractAddress(hexTerms, 0); + const saltHex = extractHex(hexTerms, 20, 32); + const bytecodeHex = extractRemainingHex(hexTerms, 52); + + return { + contractAddress: prepareResult(contractAddressHex, encodingOptions), + salt: prepareResult(saltHex, encodingOptions), + bytecode: prepareResult(bytecodeHex, encodingOptions), + } as + | DeployedTerms> + | DeployedTerms>; +} diff --git a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts index b54561da..a9323b66 100644 --- a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts @@ -1,13 +1,27 @@ +/** + * ## ERC1155BalanceChangeEnforcer + * + * Constrains ERC-1155 balance change for a token id and recipient. + * + * Terms are encoded as 1-byte direction (`0x00` = minimum increase, any non-zero e.g. `0x01` = maximum decrease), 20-byte token address, 20-byte recipient, then 32-byte token id and 32-byte balance (each big-endian uint256). + */ + import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, + extractAddress, + extractBigInt, + extractNumber, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -17,11 +31,13 @@ import { BalanceChangeType } from './types'; /** * Terms for configuring an ERC1155BalanceChange caveat. */ -export type ERC1155BalanceChangeTerms = { +export type ERC1155BalanceChangeTerms< + TBytesLike extends BytesLike = BytesLike, +> = { /** The ERC-1155 token address. */ - tokenAddress: BytesLike; + tokenAddress: TBytesLike; /** The recipient address. */ - recipient: BytesLike; + recipient: TBytesLike; /** The token id. */ tokenId: bigint; /** The balance change amount. */ @@ -35,7 +51,7 @@ export type ERC1155BalanceChangeTerms = { * * @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. + * @returns Encoded terms. * @throws Error if any parameter is invalid. */ export function createERC1155BalanceChangeTerms( @@ -51,7 +67,7 @@ export function createERC1155BalanceChangeTerms( * * @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. + * @returns Encoded terms. * @throws Error if any parameter is invalid. */ export function createERC1155BalanceChangeTerms( @@ -105,3 +121,53 @@ export function createERC1155BalanceChangeTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ERC1155BalanceChange caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded ERC1155BalanceChangeTerms object. + */ +export function decodeERC1155BalanceChangeTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ERC1155BalanceChangeTerms>; +export function decodeERC1155BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC1155BalanceChangeTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded ERC1155BalanceChangeTerms object. + */ +export function decodeERC1155BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC1155BalanceChangeTerms> + | ERC1155BalanceChangeTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 105, + 'Invalid ERC1155BalanceChange terms: must be exactly 105 bytes', + ); + + const changeType = extractNumber(hexTerms, 0, 1); + const tokenAddressHex = extractAddress(hexTerms, 1); + const recipientHex = extractAddress(hexTerms, 21); + const tokenId = extractBigInt(hexTerms, 41, 32); + const balance = extractBigInt(hexTerms, 73, 32); + + return { + changeType, + tokenAddress: prepareResult(tokenAddressHex, encodingOptions), + recipient: prepareResult(recipientHex, encodingOptions), + tokenId, + balance, + } as + | ERC1155BalanceChangeTerms> + | ERC1155BalanceChangeTerms>; +} diff --git a/packages/delegation-core/src/caveats/erc20BalanceChange.ts b/packages/delegation-core/src/caveats/erc20BalanceChange.ts index 5f9bffa1..fe0d301d 100644 --- a/packages/delegation-core/src/caveats/erc20BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc20BalanceChange.ts @@ -1,13 +1,27 @@ +/** + * ## ERC20BalanceChangeEnforcer + * + * Constrains ERC-20 balance change for a recipient relative to a reference balance. + * + * Terms are encoded as 1-byte direction (`0x00` = minimum increase, any non-zero e.g. `0x01` = maximum decrease), 20-byte token address, 20-byte recipient, then 32-byte big-endian balance amount. + */ + import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, + extractAddress, + extractBigInt, + extractNumber, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -17,23 +31,24 @@ 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; -}; +export type ERC20BalanceChangeTerms = + { + /** The ERC-20 token address. */ + tokenAddress: TBytesLike; + /** The recipient address. */ + recipient: TBytesLike; + /** 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. + * @returns Encoded terms. * @throws Error if any parameter is invalid. */ export function createERC20BalanceChangeTerms( @@ -49,7 +64,7 @@ export function createERC20BalanceChangeTerms( * * @param terms - The terms for the ERC20BalanceChange caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as changeType + tokenAddress + recipient + balance. + * @returns Encoded terms. * @throws Error if any parameter is invalid. */ export function createERC20BalanceChangeTerms( @@ -96,3 +111,51 @@ export function createERC20BalanceChangeTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ERC20BalanceChange caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded ERC20BalanceChangeTerms object. + */ +export function decodeERC20BalanceChangeTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ERC20BalanceChangeTerms>; +export function decodeERC20BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC20BalanceChangeTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded ERC20BalanceChangeTerms object. + */ +export function decodeERC20BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC20BalanceChangeTerms> + | ERC20BalanceChangeTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 73, + 'Invalid ERC20BalanceChange terms: must be exactly 73 bytes', + ); + + const changeType = extractNumber(hexTerms, 0, 1); + const tokenAddressHex = extractAddress(hexTerms, 1); + const recipientHex = extractAddress(hexTerms, 21); + const balance = extractBigInt(hexTerms, 41, 32); + + return { + changeType, + tokenAddress: prepareResult(tokenAddressHex, encodingOptions), + recipient: prepareResult(recipientHex, encodingOptions), + balance, + } as + | ERC20BalanceChangeTerms> + | ERC20BalanceChangeTerms>; +} diff --git a/packages/delegation-core/src/caveats/erc20Streaming.ts b/packages/delegation-core/src/caveats/erc20Streaming.ts index bf424674..9b8b9e0f 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -1,9 +1,25 @@ +/** + * ## ERC20StreamingEnforcer + * + * Configures a linear streaming allowance for an ERC-20 token over time. + * + * Terms are encoded as 20-byte token address then four 32-byte big-endian uint256 words: initial amount, max amount, amount per second, start time. + */ + import { type BytesLike, bytesToHex, isHexString } from '@metamask/utils'; -import { toHexString } from '../internalUtils'; import { + assertHexByteExactLength, + extractAddress, + extractBigInt, + extractNumber, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -15,9 +31,9 @@ const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; /** * Terms for configuring a linear streaming allowance of ERC20 tokens. */ -export type ERC20StreamingTerms = { +export type ERC20StreamingTerms = { /** The address of the ERC20 token contract. */ - tokenAddress: BytesLike; + tokenAddress: TBytesLike; /** The initial amount available immediately. */ initialAmount: bigint; /** The maximum total amount that can be transferred. */ @@ -57,7 +73,7 @@ export function createERC20StreamingTerms( * * @param terms - The terms for the ERC20Streaming caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 160-byte hex string. + * @returns Encoded terms. * @throws Error if any of the parameters are invalid. */ export function createERC20StreamingTerms( @@ -120,3 +136,53 @@ export function createERC20StreamingTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ERC20Streaming caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token address is returned as hex or bytes. + * @returns The decoded ERC20StreamingTerms object. + */ +export function decodeERC20StreamingTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ERC20StreamingTerms>; +export function decodeERC20StreamingTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC20StreamingTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token address is returned as hex or bytes. + * @returns The decoded ERC20StreamingTerms object. + */ +export function decodeERC20StreamingTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC20StreamingTerms> + | ERC20StreamingTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 148, + 'Invalid ERC20Streaming terms: must be exactly 148 bytes', + ); + + const tokenAddressHex = extractAddress(hexTerms, 0); + const initialAmount = extractBigInt(hexTerms, 20, 32); + const maxAmount = extractBigInt(hexTerms, 52, 32); + const amountPerSecond = extractBigInt(hexTerms, 84, 32); + const startTime = extractNumber(hexTerms, 116, 32); + + return { + tokenAddress: prepareResult(tokenAddressHex, encodingOptions), + initialAmount, + maxAmount, + amountPerSecond, + startTime, + } as + | ERC20StreamingTerms> + | ERC20StreamingTerms>; +} diff --git a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts index 954d8f07..ee0f1aa1 100644 --- a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -1,9 +1,25 @@ +/** + * ## ERC20TokenPeriodTransferEnforcer + * + * Limits periodic ERC-20 transfers for a token using amount, period length, and start date. + * + * Terms are encoded as 20-byte token address then three 32-byte big-endian uint256 words: period amount, period duration, start date. + */ + import { type BytesLike, isHexString, bytesToHex } from '@metamask/utils'; -import { toHexString } from '../internalUtils'; import { + assertHexByteExactLength, + extractAddress, + extractBigInt, + extractNumber, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,9 +28,11 @@ import type { Hex } from '../types'; /** * Terms for configuring a periodic transfer allowance of ERC20 tokens. */ -export type ERC20TokenPeriodTransferTerms = { +export type ERC20TokenPeriodTransferTerms< + TBytesLike extends BytesLike = BytesLike, +> = { /** The address of the ERC20 token. */ - tokenAddress: BytesLike; + tokenAddress: TBytesLike; /** The maximum amount that can be transferred within each period. */ periodAmount: bigint; /** The duration of each period in seconds. */ @@ -30,7 +48,7 @@ export type ERC20TokenPeriodTransferTerms = { * * @param terms - The terms for the ERC20TokenPeriodTransfer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 128-byte hex string (32 bytes for each parameter). + * @returns Encoded terms. * @throws Error if any of the numeric parameters are invalid. */ export function createERC20TokenPeriodTransferTerms( @@ -47,7 +65,7 @@ export function createERC20TokenPeriodTransferTerms( * * @param terms - The terms for the ERC20TokenPeriodTransfer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 128-byte hex string (32 bytes for each parameter). + * @returns Encoded terms. * @throws Error if any of the numeric parameters are invalid. */ export function createERC20TokenPeriodTransferTerms( @@ -94,3 +112,51 @@ export function createERC20TokenPeriodTransferTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ERC20TokenPeriodTransfer caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token address is returned as hex or bytes. + * @returns The decoded ERC20TokenPeriodTransferTerms object. + */ +export function decodeERC20TokenPeriodTransferTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ERC20TokenPeriodTransferTerms>; +export function decodeERC20TokenPeriodTransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC20TokenPeriodTransferTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token address is returned as hex or bytes. + * @returns The decoded ERC20TokenPeriodTransferTerms object. + */ +export function decodeERC20TokenPeriodTransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC20TokenPeriodTransferTerms> + | ERC20TokenPeriodTransferTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 116, + 'Invalid ERC20TokenPeriodTransfer terms: must be exactly 116 bytes', + ); + + const tokenAddressHex = extractAddress(hexTerms, 0); + const periodAmount = extractBigInt(hexTerms, 20, 32); + const periodDuration = extractNumber(hexTerms, 52, 32); + const startDate = extractNumber(hexTerms, 84, 32); + + return { + tokenAddress: prepareResult(tokenAddressHex, encodingOptions), + periodAmount, + periodDuration, + startDate, + } as + | ERC20TokenPeriodTransferTerms> + | ERC20TokenPeriodTransferTerms>; +} diff --git a/packages/delegation-core/src/caveats/erc20TransferAmount.ts b/packages/delegation-core/src/caveats/erc20TransferAmount.ts index 50f189ee..76e33e55 100644 --- a/packages/delegation-core/src/caveats/erc20TransferAmount.ts +++ b/packages/delegation-core/src/caveats/erc20TransferAmount.ts @@ -1,9 +1,26 @@ +/** + * ## ERC20TransferAmountEnforcer + * + * Limits the amount of a given ERC-20 token that may be transferred. + * + * Terms are encoded as 20-byte token address followed by a 32-byte big-endian uint256 max amount. + */ + import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + assertHexByteExactLength, + concatHex, + extractAddress, + extractBigInt, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,19 +29,20 @@ 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; -}; +export type ERC20TransferAmountTerms = + { + /** The ERC-20 token address. */ + tokenAddress: TBytesLike; + /** 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. + * @returns Encoded terms. * @throws Error if the token address is invalid or maxAmount is not positive. */ export function createERC20TransferAmountTerms( @@ -40,7 +58,7 @@ export function createERC20TransferAmountTerms( * * @param terms - The terms for the ERC20TransferAmount caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as tokenAddress + maxAmount. + * @returns Encoded terms. * @throws Error if the token address is invalid or maxAmount is not positive. */ export function createERC20TransferAmountTerms( @@ -63,3 +81,47 @@ export function createERC20TransferAmountTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ERC20TransferAmount caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token address is returned as hex or bytes. + * @returns The decoded ERC20TransferAmountTerms object. + */ +export function decodeERC20TransferAmountTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ERC20TransferAmountTerms>; +export function decodeERC20TransferAmountTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC20TransferAmountTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token address is returned as hex or bytes. + * @returns The decoded ERC20TransferAmountTerms object. + */ +export function decodeERC20TransferAmountTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC20TransferAmountTerms> + | ERC20TransferAmountTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 52, + 'Invalid ERC20TransferAmount terms: must be exactly 52 bytes', + ); + + const tokenAddressHex = extractAddress(hexTerms, 0); + const maxAmount = extractBigInt(hexTerms, 20, 32); + + return { + tokenAddress: prepareResult(tokenAddressHex, encodingOptions), + maxAmount, + } as + | ERC20TransferAmountTerms> + | ERC20TransferAmountTerms>; +} diff --git a/packages/delegation-core/src/caveats/erc721BalanceChange.ts b/packages/delegation-core/src/caveats/erc721BalanceChange.ts index 912f0454..5f6f90d8 100644 --- a/packages/delegation-core/src/caveats/erc721BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc721BalanceChange.ts @@ -1,13 +1,27 @@ +/** + * ## ERC721BalanceChangeEnforcer + * + * Constrains ERC-721 balance (id count) change for a recipient. + * + * Terms are encoded as 1-byte direction (`0x00` = minimum increase, any non-zero e.g. `0x01` = maximum decrease), 20-byte token address, 20-byte recipient, then 32-byte big-endian amount. + */ + import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, + extractAddress, + extractBigInt, + extractNumber, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -17,23 +31,24 @@ 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; -}; +export type ERC721BalanceChangeTerms = + { + /** The ERC-721 token address. */ + tokenAddress: TBytesLike; + /** The recipient address. */ + recipient: TBytesLike; + /** 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. + * @returns Encoded terms. * @throws Error if any parameter is invalid. */ export function createERC721BalanceChangeTerms( @@ -49,7 +64,7 @@ export function createERC721BalanceChangeTerms( * * @param terms - The terms for the ERC721BalanceChange caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as changeType + tokenAddress + recipient + amount. + * @returns Encoded terms. * @throws Error if any parameter is invalid. */ export function createERC721BalanceChangeTerms( @@ -96,3 +111,51 @@ export function createERC721BalanceChangeTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ERC721BalanceChange caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded ERC721BalanceChangeTerms object. + */ +export function decodeERC721BalanceChangeTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ERC721BalanceChangeTerms>; +export function decodeERC721BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC721BalanceChangeTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded ERC721BalanceChangeTerms object. + */ +export function decodeERC721BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC721BalanceChangeTerms> + | ERC721BalanceChangeTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 73, + 'Invalid ERC721BalanceChange terms: must be exactly 73 bytes', + ); + + const changeType = extractNumber(hexTerms, 0, 1); + const tokenAddressHex = extractAddress(hexTerms, 1); + const recipientHex = extractAddress(hexTerms, 21); + const amount = extractBigInt(hexTerms, 41, 32); + + return { + changeType, + tokenAddress: prepareResult(tokenAddressHex, encodingOptions), + recipient: prepareResult(recipientHex, encodingOptions), + amount, + } as + | ERC721BalanceChangeTerms> + | ERC721BalanceChangeTerms>; +} diff --git a/packages/delegation-core/src/caveats/erc721Transfer.ts b/packages/delegation-core/src/caveats/erc721Transfer.ts index 8270dbad..dfc8fec1 100644 --- a/packages/delegation-core/src/caveats/erc721Transfer.ts +++ b/packages/delegation-core/src/caveats/erc721Transfer.ts @@ -1,9 +1,26 @@ +/** + * ## ERC721TransferEnforcer + * + * Constrains transfer of a specific ERC-721 token id for a collection. + * + * Terms are encoded as 20-byte token address followed by a 32-byte big-endian uint256 token id. + */ + import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + assertHexByteExactLength, + concatHex, + extractAddress, + extractBigInt, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,9 +29,9 @@ import type { Hex } from '../types'; /** * Terms for configuring an ERC721Transfer caveat. */ -export type ERC721TransferTerms = { +export type ERC721TransferTerms = { /** The ERC-721 token address. */ - tokenAddress: BytesLike; + tokenAddress: TBytesLike; /** The token id. */ tokenId: bigint; }; @@ -24,7 +41,7 @@ export type ERC721TransferTerms = { * * @param terms - The terms for the ERC721Transfer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as tokenAddress + tokenId. + * @returns Encoded terms. * @throws Error if the token address is invalid or tokenId is negative. */ export function createERC721TransferTerms( @@ -40,7 +57,7 @@ export function createERC721TransferTerms( * * @param terms - The terms for the ERC721Transfer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as tokenAddress + tokenId. + * @returns Encoded terms. * @throws Error if the token address is invalid or tokenId is negative. */ export function createERC721TransferTerms( @@ -63,3 +80,47 @@ export function createERC721TransferTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ERC721Transfer caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token address is returned as hex or bytes. + * @returns The decoded ERC721TransferTerms object. + */ +export function decodeERC721TransferTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ERC721TransferTerms>; +export function decodeERC721TransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC721TransferTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token address is returned as hex or bytes. + * @returns The decoded ERC721TransferTerms object. + */ +export function decodeERC721TransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC721TransferTerms> + | ERC721TransferTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 52, + 'Invalid ERC721Transfer terms: must be exactly 52 bytes', + ); + + const tokenAddressHex = extractAddress(hexTerms, 0); + const tokenId = extractBigInt(hexTerms, 20, 32); + + return { + tokenAddress: prepareResult(tokenAddressHex, encodingOptions), + tokenId, + } as + | ERC721TransferTerms> + | ERC721TransferTerms>; +} diff --git a/packages/delegation-core/src/caveats/exactCalldata.ts b/packages/delegation-core/src/caveats/exactCalldata.ts index 43b25fd9..ba5802ab 100644 --- a/packages/delegation-core/src/caveats/exactCalldata.ts +++ b/packages/delegation-core/src/caveats/exactCalldata.ts @@ -1,8 +1,18 @@ +/** + * ## ExactCalldataEnforcer + * + * Requires the full execution calldata to match exactly. + * + * Terms are encoded as the calldata bytes only with no additional encoding. + */ + import type { BytesLike } from '@metamask/utils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -11,9 +21,9 @@ import type { Hex } from '../types'; /** * Terms for configuring an ExactCalldata caveat. */ -export type ExactCalldataTerms = { +export type ExactCalldataTerms = { /** The expected calldata to match against. */ - calldata: BytesLike; + calldata: TBytesLike; }; /** @@ -22,7 +32,7 @@ export type ExactCalldataTerms = { * * @param terms - The terms for the ExactCalldata caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as the calldata itself. + * @returns Encoded terms. * @throws Error if the `calldata` is invalid. */ export function createExactCalldataTerms( @@ -39,7 +49,7 @@ export function createExactCalldataTerms( * * @param terms - The terms for the ExactCalldata caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as the calldata itself. + * @returns Encoded terms. * @throws Error if the `calldata` is invalid. */ export function createExactCalldataTerms( @@ -56,6 +66,38 @@ export function createExactCalldataTerms( throw new Error('Invalid calldata: must be a hex string starting with 0x'); } - // For exact calldata, the terms are simply the expected calldata return prepareResult(calldata, encodingOptions); } + +/** + * Decodes terms for an ExactCalldata caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded calldata is returned as hex or bytes. + * @returns The decoded ExactCalldataTerms object. + */ +export function decodeExactCalldataTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ExactCalldataTerms>; +export function decodeExactCalldataTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ExactCalldataTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded calldata is returned as hex or bytes. + * @returns The decoded ExactCalldataTerms object. + */ +export function decodeExactCalldataTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ExactCalldataTerms> + | ExactCalldataTerms> { + const calldataHex = bytesLikeToHex(terms); + const calldata = prepareResult(calldataHex, encodingOptions); + return { calldata } as + | ExactCalldataTerms> + | ExactCalldataTerms>; +} diff --git a/packages/delegation-core/src/caveats/exactCalldataBatch.ts b/packages/delegation-core/src/caveats/exactCalldataBatch.ts index cfe4ec33..c3d54944 100644 --- a/packages/delegation-core/src/caveats/exactCalldataBatch.ts +++ b/packages/delegation-core/src/caveats/exactCalldataBatch.ts @@ -1,10 +1,20 @@ -import { encodeSingle } from '@metamask/abi-utils'; +/** + * ## ExactCalldataBatchEnforcer + * + * Requires each execution in a batch to match the corresponding expected calldata (the enforcer compares only `callData`; target and value in terms must still align with `Execution[]` layout used by `decodeBatch`). + * + * Terms are encoded as ABI-encoded `(address,uint256,bytes)[]`, i.e. the same tuple-array shape as batch executions. + */ + +import { decodeSingle, encodeSingle } from '@metamask/abi-utils'; import { bytesToHex, type BytesLike } from '@metamask/utils'; import { normalizeAddress } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -13,14 +23,15 @@ 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; - }[]; -}; +export type ExactCalldataBatchTerms = + { + /** The executions that must be matched exactly in the batch. */ + executions: { + target: TBytesLike; + value: bigint; + callData: TBytesLike; + }[]; + }; const EXECUTION_ARRAY_ABI = '(address,uint256,bytes)[]'; @@ -29,7 +40,7 @@ const EXECUTION_ARRAY_ABI = '(address,uint256,bytes)[]'; * * @param terms - The terms for the ExactCalldataBatch caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as ABI-encoded execution array. + * @returns Encoded terms. * @throws Error if any execution parameters are invalid. */ export function createExactCalldataBatchTerms( @@ -45,7 +56,7 @@ export function createExactCalldataBatchTerms( * * @param terms - The terms for the ExactCalldataBatch caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as ABI-encoded execution array. + * @returns Encoded terms. * @throws Error if any execution parameters are invalid. */ export function createExactCalldataBatchTerms( @@ -86,3 +97,46 @@ export function createExactCalldataBatchTerms( const hexValue = encodeSingle(EXECUTION_ARRAY_ABI, encodableExecutions); return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ExactCalldataBatch caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded targets and calldata are returned as hex or bytes. + * @returns The decoded ExactCalldataBatchTerms object. + */ +export function decodeExactCalldataBatchTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ExactCalldataBatchTerms>; +export function decodeExactCalldataBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ExactCalldataBatchTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded targets and calldata are returned as hex or bytes. + * @returns The decoded ExactCalldataBatchTerms object. + */ +export function decodeExactCalldataBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ExactCalldataBatchTerms> + | ExactCalldataBatchTerms> { + const hexTerms = bytesLikeToHex(terms); + + const decoded = decodeSingle(EXECUTION_ARRAY_ABI, hexTerms); + + const executions = (decoded as [string, bigint, Uint8Array][]).map( + ([target, value, callData]) => ({ + target: prepareResult(target, encodingOptions), + value, + callData: prepareResult(bytesToHex(callData), encodingOptions), + }), + ); + + return { executions } as + | ExactCalldataBatchTerms> + | ExactCalldataBatchTerms>; +} diff --git a/packages/delegation-core/src/caveats/exactExecution.ts b/packages/delegation-core/src/caveats/exactExecution.ts index f8d3a56c..d852a6b0 100644 --- a/packages/delegation-core/src/caveats/exactExecution.ts +++ b/packages/delegation-core/src/caveats/exactExecution.ts @@ -1,9 +1,27 @@ +/** + * ## ExactExecutionEnforcer + * + * Requires a single execution (target, value, calldata) to match exactly. + * + * Terms are encoded as 20-byte target, 32-byte big-endian value, then calldata bytes. + */ + import { bytesToHex, type BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + assertHexBytesMinLength, + concatHex, + extractAddress, + extractBigInt, + extractRemainingHex, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,12 +30,12 @@ import type { Hex } from '../types'; /** * Terms for configuring an ExactExecution caveat. */ -export type ExactExecutionTerms = { +export type ExactExecutionTerms = { /** The execution that must be matched exactly. */ execution: { - target: BytesLike; + target: TBytesLike; value: bigint; - callData: BytesLike; + callData: TBytesLike; }; }; @@ -26,7 +44,7 @@ export type ExactExecutionTerms = { * * @param terms - The terms for the ExactExecution caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated target + value + calldata. + * @returns Encoded terms. * @throws Error if any execution parameters are invalid. */ export function createExactExecutionTerms( @@ -42,7 +60,7 @@ export function createExactExecutionTerms( * * @param terms - The terms for the ExactExecution caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated target + value + calldata. + * @returns Encoded terms. * @throws Error if any execution parameters are invalid. */ export function createExactExecutionTerms( @@ -77,3 +95,51 @@ export function createExactExecutionTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ExactExecution caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded target and calldata are returned as hex or bytes. + * @returns The decoded ExactExecutionTerms object. + */ +export function decodeExactExecutionTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ExactExecutionTerms>; +export function decodeExactExecutionTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ExactExecutionTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded target and calldata are returned as hex or bytes. + * @returns The decoded ExactExecutionTerms object. + */ +export function decodeExactExecutionTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ExactExecutionTerms> + | ExactExecutionTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexBytesMinLength( + hexTerms, + 52, + 'Invalid ExactExecution terms: must be at least 52 bytes', + ); + + const targetHex = extractAddress(hexTerms, 0); + const value = extractBigInt(hexTerms, 20, 32); + const callDataHex = extractRemainingHex(hexTerms, 52); + + return { + execution: { + target: prepareResult(targetHex, encodingOptions), + value, + callData: prepareResult(callDataHex, encodingOptions), + }, + } as + | ExactExecutionTerms> + | ExactExecutionTerms>; +} diff --git a/packages/delegation-core/src/caveats/exactExecutionBatch.ts b/packages/delegation-core/src/caveats/exactExecutionBatch.ts index a8a72e25..9093c474 100644 --- a/packages/delegation-core/src/caveats/exactExecutionBatch.ts +++ b/packages/delegation-core/src/caveats/exactExecutionBatch.ts @@ -1,10 +1,20 @@ -import { encodeSingle } from '@metamask/abi-utils'; +/** + * ## ExactExecutionBatchEnforcer + * + * Requires a batch of executions to match exactly on target, value, and calldata. + * + * Terms are encoded as ABI-encoded (address,uint256,bytes)[]. + */ + +import { decodeSingle, encodeSingle } from '@metamask/abi-utils'; import { bytesToHex, type BytesLike } from '@metamask/utils'; import { normalizeAddress } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -13,14 +23,15 @@ 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; - }[]; -}; +export type ExactExecutionBatchTerms = + { + /** The executions that must be matched exactly in the batch. */ + executions: { + target: TBytesLike; + value: bigint; + callData: TBytesLike; + }[]; + }; const EXECUTION_ARRAY_ABI = '(address,uint256,bytes)[]'; @@ -29,7 +40,7 @@ const EXECUTION_ARRAY_ABI = '(address,uint256,bytes)[]'; * * @param terms - The terms for the ExactExecutionBatch caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as ABI-encoded execution array. + * @returns Encoded terms. * @throws Error if any execution parameters are invalid. */ export function createExactExecutionBatchTerms( @@ -45,7 +56,7 @@ export function createExactExecutionBatchTerms( * * @param terms - The terms for the ExactExecutionBatch caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as ABI-encoded execution array. + * @returns Encoded terms. * @throws Error if any execution parameters are invalid. */ export function createExactExecutionBatchTerms( @@ -86,3 +97,46 @@ export function createExactExecutionBatchTerms( const hexValue = encodeSingle(EXECUTION_ARRAY_ABI, encodableExecutions); return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an ExactExecutionBatch caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded targets and calldata are returned as hex or bytes. + * @returns The decoded ExactExecutionBatchTerms object. + */ +export function decodeExactExecutionBatchTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ExactExecutionBatchTerms>; +export function decodeExactExecutionBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ExactExecutionBatchTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded targets and calldata are returned as hex or bytes. + * @returns The decoded ExactExecutionBatchTerms object. + */ +export function decodeExactExecutionBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ExactExecutionBatchTerms> + | ExactExecutionBatchTerms> { + const hexTerms = bytesLikeToHex(terms); + + const decoded = decodeSingle(EXECUTION_ARRAY_ABI, hexTerms); + + const executions = (decoded as [string, bigint, Uint8Array][]).map( + ([target, value, callData]) => ({ + target: prepareResult(target, encodingOptions), + value, + callData: prepareResult(bytesToHex(callData), encodingOptions), + }), + ); + + return { executions } as + | ExactExecutionBatchTerms> + | ExactExecutionBatchTerms>; +} diff --git a/packages/delegation-core/src/caveats/id.ts b/packages/delegation-core/src/caveats/id.ts index de6f1ea9..90a3d630 100644 --- a/packages/delegation-core/src/caveats/id.ts +++ b/packages/delegation-core/src/caveats/id.ts @@ -1,5 +1,20 @@ -import { toHexString } from '../internalUtils'; +/** + * ## IdEnforcer + * + * Ensures each delegation redemption uses a unique numeric id. + * + * Terms are encoded as a single 32-byte big-endian uint256 id. + */ + +import type { BytesLike } from '@metamask/utils'; + import { + assertHexByteExactLength, + extractBigInt, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -22,7 +37,7 @@ export type IdTerms = { * * @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. + * @returns Encoded terms. * @throws Error if the id is invalid or out of range. */ export function createIdTerms( @@ -38,7 +53,7 @@ export function createIdTerms( * * @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. + * @returns Encoded terms. * @throws Error if the id is invalid or out of range. */ export function createIdTerms( @@ -71,3 +86,20 @@ export function createIdTerms( const hexValue = `0x${toHexString({ value: idBigInt, size: 32 })}`; return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for an Id caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @returns The decoded IdTerms object. + */ +export function decodeIdTerms(terms: BytesLike): IdTerms { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 32, + 'Invalid Id terms: must be exactly 32 bytes', + ); + const id = extractBigInt(hexTerms, 0, 32); + return { id }; +} diff --git a/packages/delegation-core/src/caveats/index.ts b/packages/delegation-core/src/caveats/index.ts index 9d30a363..68d69cf8 100644 --- a/packages/delegation-core/src/caveats/index.ts +++ b/packages/delegation-core/src/caveats/index.ts @@ -1,31 +1,103 @@ -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'; +export { createValueLteTerms, decodeValueLteTerms } from './valueLte'; +export { createTimestampTerms, decodeTimestampTerms } from './timestamp'; +export { + createNativeTokenPeriodTransferTerms, + decodeNativeTokenPeriodTransferTerms, +} from './nativeTokenPeriodTransfer'; +export { + createExactCalldataTerms, + decodeExactCalldataTerms, +} from './exactCalldata'; +export { + createExactCalldataBatchTerms, + decodeExactCalldataBatchTerms, +} from './exactCalldataBatch'; +export { + createExactExecutionTerms, + decodeExactExecutionTerms, +} from './exactExecution'; +export { + createExactExecutionBatchTerms, + decodeExactExecutionBatchTerms, +} from './exactExecutionBatch'; +export { + createNativeTokenStreamingTerms, + decodeNativeTokenStreamingTerms, +} from './nativeTokenStreaming'; +export { + createNativeTokenTransferAmountTerms, + decodeNativeTokenTransferAmountTerms, +} from './nativeTokenTransferAmount'; +export { + createNativeTokenPaymentTerms, + decodeNativeTokenPaymentTerms, +} from './nativeTokenPayment'; +export { + createNativeBalanceChangeTerms, + decodeNativeBalanceChangeTerms, +} from './nativeBalanceChange'; +export { + createERC20StreamingTerms, + decodeERC20StreamingTerms, +} from './erc20Streaming'; +export { + createERC20TokenPeriodTransferTerms, + decodeERC20TokenPeriodTransferTerms, +} from './erc20TokenPeriodTransfer'; +export { + createERC20TransferAmountTerms, + decodeERC20TransferAmountTerms, +} from './erc20TransferAmount'; +export { + createERC20BalanceChangeTerms, + decodeERC20BalanceChangeTerms, +} from './erc20BalanceChange'; +export { + createERC721BalanceChangeTerms, + decodeERC721BalanceChangeTerms, +} from './erc721BalanceChange'; +export { + createERC721TransferTerms, + decodeERC721TransferTerms, +} from './erc721Transfer'; +export { + createERC1155BalanceChangeTerms, + decodeERC1155BalanceChangeTerms, +} from './erc1155BalanceChange'; +export { createNonceTerms, decodeNonceTerms } from './nonce'; +export { + createAllowedCalldataTerms, + decodeAllowedCalldataTerms, +} from './allowedCalldata'; +export { + createAllowedMethodsTerms, + decodeAllowedMethodsTerms, +} from './allowedMethods'; +export { + createAllowedTargetsTerms, + decodeAllowedTargetsTerms, +} from './allowedTargets'; +export { + createArgsEqualityCheckTerms, + decodeArgsEqualityCheckTerms, +} from './argsEqualityCheck'; +export { createBlockNumberTerms, decodeBlockNumberTerms } from './blockNumber'; +export { createDeployedTerms, decodeDeployedTerms } from './deployed'; +export { createIdTerms, decodeIdTerms } from './id'; +export { + createLimitedCallsTerms, + decodeLimitedCallsTerms, +} from './limitedCalls'; +export { + createMultiTokenPeriodTerms, + decodeMultiTokenPeriodTerms, +} from './multiTokenPeriod'; +export { + createOwnershipTransferTerms, + decodeOwnershipTransferTerms, +} from './ownershipTransfer'; +export { createRedeemerTerms, decodeRedeemerTerms } from './redeemer'; +export { + createSpecificActionERC20TransferBatchTerms, + decodeSpecificActionERC20TransferBatchTerms, +} from './specificActionERC20TransferBatch'; diff --git a/packages/delegation-core/src/caveats/limitedCalls.ts b/packages/delegation-core/src/caveats/limitedCalls.ts index 624a8269..122b8409 100644 --- a/packages/delegation-core/src/caveats/limitedCalls.ts +++ b/packages/delegation-core/src/caveats/limitedCalls.ts @@ -1,5 +1,20 @@ -import { toHexString } from '../internalUtils'; +/** + * ## LimitedCallsEnforcer + * + * Caps how many times the delegation may be redeemed. + * + * Terms are encoded as a single 32-byte big-endian uint256 call limit. + */ + +import type { BytesLike } from '@metamask/utils'; + import { + assertHexByteExactLength, + extractNumber, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -20,7 +35,7 @@ export type LimitedCallsTerms = { * * @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. + * @returns Encoded terms. * @throws Error if the limit is not a positive integer. */ export function createLimitedCallsTerms( @@ -36,7 +51,7 @@ export function createLimitedCallsTerms( * * @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. + * @returns Encoded terms. * @throws Error if the limit is not a positive integer. */ export function createLimitedCallsTerms( @@ -56,3 +71,20 @@ export function createLimitedCallsTerms( const hexValue = `0x${toHexString({ value: limit, size: 32 })}`; return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a LimitedCalls caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @returns The decoded LimitedCallsTerms object. + */ +export function decodeLimitedCallsTerms(terms: BytesLike): LimitedCallsTerms { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 32, + 'Invalid LimitedCalls terms: must be exactly 32 bytes', + ); + const limit = extractNumber(hexTerms, 0, 32); + return { limit }; +} diff --git a/packages/delegation-core/src/caveats/multiTokenPeriod.ts b/packages/delegation-core/src/caveats/multiTokenPeriod.ts index 15c85f85..44486cc2 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -1,9 +1,28 @@ +/** + * ## MultiTokenPeriodEnforcer + * + * Sets independent periodic transfer limits for multiple tokens (ERC-20 or native). + * + * Terms are encoded by repeating, per entry: 20-byte token address (`address(0)` denotes native token) then three 32-byte big-endian uint256 words (period amount, period duration, start date). + */ + import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + assertHexByteLengthAtLeastOneMultipleOf, + concatHex, + extractAddress, + extractBigInt, + extractNumber, + getByteLength, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,8 +31,8 @@ import type { Hex } from '../types'; /** * Configuration for a single token in MultiTokenPeriod terms. */ -export type TokenPeriodConfig = { - token: BytesLike; +export type TokenPeriodConfig = { + token: TBytesLike; periodAmount: bigint; periodDuration: number; startDate: number; @@ -22,8 +41,8 @@ export type TokenPeriodConfig = { /** * Terms for configuring a MultiTokenPeriod caveat. */ -export type MultiTokenPeriodTerms = { - tokenConfigs: TokenPeriodConfig[]; +export type MultiTokenPeriodTerms = { + tokenConfigs: TokenPeriodConfig[]; }; /** @@ -31,7 +50,7 @@ export type MultiTokenPeriodTerms = { * * @param terms - The terms for the MultiTokenPeriod caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated token period configs. + * @returns Encoded terms. * @throws Error if the tokenConfigs array is empty or contains invalid parameters. */ export function createMultiTokenPeriodTerms( @@ -47,7 +66,7 @@ export function createMultiTokenPeriodTerms( * * @param terms - The terms for the MultiTokenPeriod caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated token period configs. + * @returns Encoded terms. * @throws Error if the tokenConfigs array is empty or contains invalid parameters. */ export function createMultiTokenPeriodTerms( @@ -93,3 +112,60 @@ export function createMultiTokenPeriodTerms( const hexValue = concatHex(hexParts); return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a MultiTokenPeriod caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token addresses are returned as hex or bytes. + * @returns The decoded MultiTokenPeriodTerms object. + */ +export function decodeMultiTokenPeriodTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): MultiTokenPeriodTerms>; +export function decodeMultiTokenPeriodTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): MultiTokenPeriodTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded token addresses are returned as hex or bytes. + * @returns The decoded MultiTokenPeriodTerms object. + */ +export function decodeMultiTokenPeriodTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | MultiTokenPeriodTerms> + | MultiTokenPeriodTerms> { + const hexTerms = bytesLikeToHex(terms); + + const configSize = 116; + assertHexByteLengthAtLeastOneMultipleOf( + hexTerms, + configSize, + 'Invalid MultiTokenPeriod terms: must be a multiple of 116 bytes', + ); + const configCount = getByteLength(hexTerms) / configSize; + + const tokenConfigs: TokenPeriodConfig[] = []; + for (let i = 0; i < configCount; i++) { + const offset = i * configSize; + const tokenHex = extractAddress(hexTerms, offset); + const periodAmount = extractBigInt(hexTerms, offset + 20, 32); + const periodDuration = extractNumber(hexTerms, offset + 52, 32); + const startDate = extractNumber(hexTerms, offset + 84, 32); + + tokenConfigs.push({ + token: prepareResult(tokenHex, encodingOptions), + periodAmount, + periodDuration, + startDate, + }); + } + + return { tokenConfigs } as + | MultiTokenPeriodTerms> + | MultiTokenPeriodTerms>; +} diff --git a/packages/delegation-core/src/caveats/nativeBalanceChange.ts b/packages/delegation-core/src/caveats/nativeBalanceChange.ts index a52e2ecb..de937abe 100644 --- a/packages/delegation-core/src/caveats/nativeBalanceChange.ts +++ b/packages/delegation-core/src/caveats/nativeBalanceChange.ts @@ -1,13 +1,27 @@ +/** + * ## NativeBalanceChangeEnforcer + * + * Constrains native balance change for a recipient relative to a reference balance. + * + * Terms are encoded as 1-byte direction (`0x00` = minimum increase, any non-zero e.g. `0x01` = maximum decrease), 20-byte recipient, then 32-byte big-endian balance in wei. + */ + import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, + extractAddress, + extractBigInt, + extractNumber, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -17,21 +31,22 @@ 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; -}; +export type NativeBalanceChangeTerms = + { + /** The recipient address. */ + recipient: TBytesLike; + /** 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. + * @returns Encoded terms. * @throws Error if the recipient address is invalid or balance/changeType are invalid. */ export function createNativeBalanceChangeTerms( @@ -47,7 +62,7 @@ export function createNativeBalanceChangeTerms( * * @param terms - The terms for the NativeBalanceChange caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as changeType + recipient + balance. + * @returns Encoded terms. * @throws Error if the recipient address is invalid or balance/changeType are invalid. */ export function createNativeBalanceChangeTerms( @@ -80,3 +95,49 @@ export function createNativeBalanceChangeTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a NativeBalanceChange caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded recipient is returned as hex or bytes. + * @returns The decoded NativeBalanceChangeTerms object. + */ +export function decodeNativeBalanceChangeTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): NativeBalanceChangeTerms>; +export function decodeNativeBalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): NativeBalanceChangeTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded recipient is returned as hex or bytes. + * @returns The decoded NativeBalanceChangeTerms object. + */ +export function decodeNativeBalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | NativeBalanceChangeTerms> + | NativeBalanceChangeTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 53, + 'Invalid NativeBalanceChange terms: must be exactly 53 bytes', + ); + + const changeType = extractNumber(hexTerms, 0, 1); + const recipientHex = extractAddress(hexTerms, 1); + const balance = extractBigInt(hexTerms, 21, 32); + + return { + changeType, + recipient: prepareResult(recipientHex, encodingOptions), + balance, + } as + | NativeBalanceChangeTerms> + | NativeBalanceChangeTerms>; +} diff --git a/packages/delegation-core/src/caveats/nativeTokenPayment.ts b/packages/delegation-core/src/caveats/nativeTokenPayment.ts index 3074320e..32c7d6ed 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPayment.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPayment.ts @@ -1,13 +1,26 @@ +/** + * ## NativeTokenPaymentEnforcer + * + * Requires a fixed native token payment to a recipient. + * + * Terms are encoded as 20-byte recipient followed by a 32-byte big-endian uint256 amount in wei. + */ + import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, + extractAddress, + extractBigInt, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -16,19 +29,20 @@ 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; -}; +export type NativeTokenPaymentTerms = + { + /** The recipient address. */ + recipient: TBytesLike; + /** 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. + * @returns Encoded terms. * @throws Error if the recipient address is invalid or amount is not positive. */ export function createNativeTokenPaymentTerms( @@ -44,7 +58,7 @@ export function createNativeTokenPaymentTerms( * * @param terms - The terms for the NativeTokenPayment caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as recipient + amount. + * @returns Encoded terms. * @throws Error if the recipient address is invalid or amount is not positive. */ export function createNativeTokenPaymentTerms( @@ -67,3 +81,47 @@ export function createNativeTokenPaymentTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a NativeTokenPayment caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded recipient is returned as hex or bytes. + * @returns The decoded NativeTokenPaymentTerms object. + */ +export function decodeNativeTokenPaymentTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): NativeTokenPaymentTerms>; +export function decodeNativeTokenPaymentTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): NativeTokenPaymentTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded recipient is returned as hex or bytes. + * @returns The decoded NativeTokenPaymentTerms object. + */ +export function decodeNativeTokenPaymentTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | NativeTokenPaymentTerms> + | NativeTokenPaymentTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 52, + 'Invalid NativeTokenPayment terms: must be exactly 52 bytes', + ); + + const recipientHex = extractAddress(hexTerms, 0); + const amount = extractBigInt(hexTerms, 20, 32); + + return { + recipient: prepareResult(recipientHex, encodingOptions), + amount, + } as + | NativeTokenPaymentTerms> + | NativeTokenPaymentTerms>; +} diff --git a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts index 80015d46..b916567b 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts @@ -1,5 +1,21 @@ -import { toHexString } from '../internalUtils'; +/** + * ## NativeTokenPeriodTransferEnforcer + * + * Limits periodic native token transfers using amount, period length, and start date. + * + * Terms are encoded as three consecutive 32-byte big-endian uint256 words: period amount, period duration, start date. + */ + +import type { BytesLike } from '@metamask/utils'; + +import { + assertHexByteExactLength, + extractBigInt, + extractNumber, + toHexString, +} from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -26,7 +42,7 @@ export type NativeTokenPeriodTransferTerms = { * * @param terms - The terms for the NativeTokenPeriodTransfer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 96-byte hex string (32 bytes for each parameter). + * @returns Encoded terms. * @throws Error if any of the numeric parameters are invalid. */ export function createNativeTokenPeriodTransferTerms( @@ -43,7 +59,7 @@ export function createNativeTokenPeriodTransferTerms( * * @param terms - The terms for the NativeTokenPeriodTransfer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 96-byte hex string (32 bytes for each parameter). + * @returns Encoded terms. * @throws Error if any of the numeric parameters are invalid. */ export function createNativeTokenPeriodTransferTerms( @@ -72,3 +88,26 @@ export function createNativeTokenPeriodTransferTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a NativeTokenPeriodTransfer caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @returns The decoded NativeTokenPeriodTransferTerms object. + */ +export function decodeNativeTokenPeriodTransferTerms( + terms: BytesLike, +): NativeTokenPeriodTransferTerms { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 96, + 'Invalid NativeTokenPeriodTransfer terms: must be exactly 96 bytes', + ); + + const periodAmount = extractBigInt(hexTerms, 0, 32); + const periodDuration = extractNumber(hexTerms, 32, 32); + const startDate = extractNumber(hexTerms, 64, 32); + + return { periodAmount, periodDuration, startDate }; +} diff --git a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts index 414c83a8..f96ac89f 100644 --- a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts +++ b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts @@ -1,5 +1,21 @@ -import { toHexString } from '../internalUtils'; +/** + * ## NativeTokenStreamingEnforcer + * + * Configures a linear streaming allowance of native token over time. + * + * Terms are encoded as four consecutive 32-byte big-endian uint256 words: initial amount, max amount, amount per second, start time. + */ + +import type { BytesLike } from '@metamask/utils'; + +import { + assertHexByteExactLength, + extractBigInt, + extractNumber, + toHexString, +} from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -52,7 +68,7 @@ export function createNativeTokenStreamingTerms( * * @param terms - The terms for the NativeTokenStreaming caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 128-byte hex string. + * @returns Encoded terms. * @throws Error if any of the numeric parameters are invalid. */ export function createNativeTokenStreamingTerms( @@ -96,3 +112,27 @@ export function createNativeTokenStreamingTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a NativeTokenStreaming caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @returns The decoded NativeTokenStreamingTerms object. + */ +export function decodeNativeTokenStreamingTerms( + terms: BytesLike, +): NativeTokenStreamingTerms { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 128, + 'Invalid NativeTokenStreaming terms: must be exactly 128 bytes', + ); + + const initialAmount = extractBigInt(hexTerms, 0, 32); + const maxAmount = extractBigInt(hexTerms, 32, 32); + const amountPerSecond = extractBigInt(hexTerms, 64, 32); + const startTime = extractNumber(hexTerms, 96, 32); + + return { initialAmount, maxAmount, amountPerSecond, startTime }; +} diff --git a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts index c468ded8..827af4f9 100644 --- a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts +++ b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts @@ -1,5 +1,20 @@ -import { toHexString } from '../internalUtils'; +/** + * ## NativeTokenTransferAmountEnforcer + * + * Limits how much native token (wei) may be transferred in a single execution. + * + * Terms are encoded as a single 32-byte big-endian uint256 max amount. + */ + +import type { BytesLike } from '@metamask/utils'; + import { + assertHexByteExactLength, + extractBigInt, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -20,7 +35,7 @@ export type NativeTokenTransferAmountTerms = { * * @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. + * @returns Encoded terms. * @throws Error if maxAmount is negative. */ export function createNativeTokenTransferAmountTerms( @@ -36,7 +51,7 @@ export function createNativeTokenTransferAmountTerms( * * @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. + * @returns Encoded terms. * @throws Error if maxAmount is negative. */ export function createNativeTokenTransferAmountTerms( @@ -52,3 +67,22 @@ export function createNativeTokenTransferAmountTerms( const hexValue = `0x${toHexString({ value: maxAmount, size: 32 })}`; return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a NativeTokenTransferAmount caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @returns The decoded NativeTokenTransferAmountTerms object. + */ +export function decodeNativeTokenTransferAmountTerms( + terms: BytesLike, +): NativeTokenTransferAmountTerms { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 32, + 'Invalid NativeTokenTransferAmount terms: must be exactly 32 bytes', + ); + const maxAmount = extractBigInt(hexTerms, 0, 32); + return { maxAmount }; +} diff --git a/packages/delegation-core/src/caveats/nonce.ts b/packages/delegation-core/src/caveats/nonce.ts index 923aa571..1e4da376 100644 --- a/packages/delegation-core/src/caveats/nonce.ts +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -1,10 +1,20 @@ +/** + * ## NonceEnforcer + * + * Binds the delegation to a nonce word for revocation and replay semantics. + * + * Terms are encoded as one 32-byte word with the nonce right-aligned and zero-padded on the left. + */ + import { isHexString } from '@metamask/utils'; import type { BytesLike } from '@metamask/utils'; +import { assertHexByteExactLength } from '../internalUtils'; import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -16,9 +26,9 @@ const MAX_NONCE_STRING_LENGTH = 66; /** * Terms for configuring a Nonce caveat. */ -export type NonceTerms = { +export type NonceTerms = { /** The nonce as BytesLike (0x-prefixed hex string or Uint8Array) to allow bulk revocation of delegations. */ - nonce: BytesLike; + nonce: TBytesLike; }; /** @@ -26,7 +36,7 @@ export type NonceTerms = { * * @param terms - The terms for the Nonce caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 32-byte hex string. + * @returns Encoded terms. * @throws Error if the nonce is invalid. */ export function createNonceTerms( @@ -42,7 +52,7 @@ export function createNonceTerms( * * @param terms - The terms for the Nonce caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 32-byte padded value in the specified encoding format. + * @returns Encoded terms. * @throws Error if the nonce is invalid or empty. */ export function createNonceTerms( @@ -84,3 +94,41 @@ export function createNonceTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a Nonce caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded nonce is returned as hex or bytes. + * @returns The decoded NonceTerms object. + */ +export function decodeNonceTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): NonceTerms>; +export function decodeNonceTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): NonceTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded nonce is returned as hex or bytes. + * @returns The decoded NonceTerms object. + */ +export function decodeNonceTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): NonceTerms> | NonceTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 32, + 'Invalid Nonce terms: must be exactly 32 bytes', + ); + + const nonce = prepareResult(hexTerms, encodingOptions); + + return { nonce } as + | NonceTerms> + | NonceTerms>; +} diff --git a/packages/delegation-core/src/caveats/ownershipTransfer.ts b/packages/delegation-core/src/caveats/ownershipTransfer.ts index 5b0675eb..baf741b7 100644 --- a/packages/delegation-core/src/caveats/ownershipTransfer.ts +++ b/packages/delegation-core/src/caveats/ownershipTransfer.ts @@ -1,9 +1,23 @@ +/** + * ## OwnershipTransferEnforcer + * + * Constrains ownership transfer for a specific contract. + * + * Terms are encoded as the 20-byte contract address only. + */ + import type { BytesLike } from '@metamask/utils'; -import { normalizeAddress } from '../internalUtils'; import { + assertHexByteExactLength, + extractAddress, + normalizeAddress, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,9 +26,9 @@ import type { Hex } from '../types'; /** * Terms for configuring an OwnershipTransfer caveat. */ -export type OwnershipTransferTerms = { +export type OwnershipTransferTerms = { /** The contract address for which ownership transfers are allowed. */ - contractAddress: BytesLike; + contractAddress: TBytesLike; }; /** @@ -22,7 +36,7 @@ export type OwnershipTransferTerms = { * * @param terms - The terms for the OwnershipTransfer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as the contract address. + * @returns Encoded terms. * @throws Error if the contract address is invalid. */ export function createOwnershipTransferTerms( @@ -38,7 +52,7 @@ export function createOwnershipTransferTerms( * * @param terms - The terms for the OwnershipTransfer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as the contract address. + * @returns Encoded terms. * @throws Error if the contract address is invalid. */ export function createOwnershipTransferTerms( @@ -54,3 +68,43 @@ export function createOwnershipTransferTerms( return prepareResult(contractAddressHex, encodingOptions); } + +/** + * Decodes terms for an OwnershipTransfer caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded contract address is returned as hex or bytes. + * @returns The decoded OwnershipTransferTerms object. + */ +export function decodeOwnershipTransferTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): OwnershipTransferTerms>; +export function decodeOwnershipTransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): OwnershipTransferTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded contract address is returned as hex or bytes. + * @returns The decoded OwnershipTransferTerms object. + */ +export function decodeOwnershipTransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | OwnershipTransferTerms> + | OwnershipTransferTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 20, + 'Invalid OwnershipTransfer terms: must be exactly 20 bytes', + ); + const contractAddressHex = extractAddress(hexTerms, 0); + return { + contractAddress: prepareResult(contractAddressHex, encodingOptions), + } as + | OwnershipTransferTerms> + | OwnershipTransferTerms>; +} diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index d88d29f2..81447e47 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -1,9 +1,25 @@ +/** + * ## RedeemerEnforcer + * + * Restricts which addresses may redeem the delegation. + * + * Terms are encoded as the concatenation of 20-byte redeemer addresses in order with no padding between addresses. + */ + import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress } from '../internalUtils'; import { + assertHexByteLengthAtLeastOneMultipleOf, + concatHex, + extractAddress, + getByteLength, + normalizeAddress, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,9 +28,9 @@ import type { Hex } from '../types'; /** * Terms for configuring a Redeemer caveat. */ -export type RedeemerTerms = { +export type RedeemerTerms = { /** An array of addresses allowed to redeem the delegation. */ - redeemers: BytesLike[]; + redeemers: TBytesLike[]; }; /** @@ -22,7 +38,7 @@ export type RedeemerTerms = { * * @param terms - The terms for the Redeemer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated redeemer addresses. + * @returns Encoded terms. * @throws Error if the redeemers array is empty or contains invalid addresses. */ export function createRedeemerTerms( @@ -38,7 +54,7 @@ export function createRedeemerTerms( * * @param terms - The terms for the Redeemer caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as concatenated redeemer addresses. + * @returns Encoded terms. * @throws Error if the redeemers array is empty or contains invalid addresses. */ export function createRedeemerTerms( @@ -60,3 +76,50 @@ export function createRedeemerTerms( const hexValue = concatHex(normalizedRedeemers); return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a Redeemer caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded RedeemerTerms object. + */ +export function decodeRedeemerTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): RedeemerTerms>; +export function decodeRedeemerTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): RedeemerTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses are returned as hex or bytes. + * @returns The decoded RedeemerTerms object. + */ +export function decodeRedeemerTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | RedeemerTerms> + | RedeemerTerms> { + const hexTerms = bytesLikeToHex(terms); + + const addressSize = 20; + assertHexByteLengthAtLeastOneMultipleOf( + hexTerms, + addressSize, + 'Invalid redeemers: must be a multiple of 20', + ); + const addressCount = getByteLength(hexTerms) / addressSize; + + const redeemers: (Hex | Uint8Array)[] = []; + for (let i = 0; i < addressCount; i++) { + const redeemer = extractAddress(hexTerms, i * addressSize); + redeemers.push(prepareResult(redeemer, encodingOptions)); + } + + return { redeemers } as + | RedeemerTerms> + | RedeemerTerms>; +} diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts index bfc33e13..17b5edaf 100644 --- a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -1,9 +1,33 @@ +/** + * ## SpecificActionERC20TransferBatchEnforcer + * + * Encodes caveat terms for a batch of exactly two executions: first call must match + * `target` + `calldata`; second must be `IERC20.transfer` to `recipient` for `amount` + * on `tokenAddress` (see on-chain `beforeHook`). + * + * - bytes 0–19: ERC-20 token address + * - bytes 20–39: transfer recipient + * - bytes 40–71: transfer amount (uint256, 32 bytes) + * - bytes 72–91: first execution target (`firstTarget` in Enforcer) + * - bytes 92–end: first execution calldata, raw body only (no ABI length prefix; `firstCalldata` in Enforcer) + */ + import { bytesToHex, type BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + assertHexBytesMinLength, + concatHex, + extractAddress, + extractBigInt, + extractRemainingHex, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -12,17 +36,19 @@ import type { Hex } from '../types'; /** * Terms for configuring a SpecificActionERC20TransferBatch caveat. */ -export type SpecificActionERC20TransferBatchTerms = { +export type SpecificActionERC20TransferBatchTerms< + TBytesLike extends BytesLike = BytesLike, +> = { /** The address of the ERC-20 token contract. */ - tokenAddress: BytesLike; + tokenAddress: TBytesLike; /** The recipient of the ERC-20 transfer. */ - recipient: BytesLike; + recipient: TBytesLike; /** 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; + /** The target address for the first batch execution (`firstTarget` in the enforcer). */ + target: TBytesLike; + /** Calldata for the first execution only, without an ABI length prefix (`firstCalldata` on-chain). */ + calldata: TBytesLike; }; /** @@ -31,7 +57,7 @@ export type SpecificActionERC20TransferBatchTerms = { * * @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. + * @returns Encoded terms. * @throws Error if any address is invalid or amount is not positive. */ export function createSpecificActionERC20TransferBatchTerms( @@ -48,7 +74,7 @@ export function createSpecificActionERC20TransferBatchTerms( * * @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. + * @returns Encoded terms. * @throws Error if any address is invalid or amount is not positive. */ export function createSpecificActionERC20TransferBatchTerms( @@ -98,3 +124,53 @@ export function createSpecificActionERC20TransferBatchTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a SpecificActionERC20TransferBatch caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses and calldata are returned as hex or bytes. + * @returns The decoded SpecificActionERC20TransferBatchTerms object. + */ +export function decodeSpecificActionERC20TransferBatchTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): SpecificActionERC20TransferBatchTerms>; +export function decodeSpecificActionERC20TransferBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): SpecificActionERC20TransferBatchTerms>; +/** + * @param terms - The encoded terms as a hex string or Uint8Array. + * @param encodingOptions - Whether decoded addresses and calldata are returned as hex or bytes. + * @returns The decoded SpecificActionERC20TransferBatchTerms object. + */ +export function decodeSpecificActionERC20TransferBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | SpecificActionERC20TransferBatchTerms> + | SpecificActionERC20TransferBatchTerms> { + const hexTerms = bytesLikeToHex(terms); + assertHexBytesMinLength( + hexTerms, + 92, + 'Invalid SpecificActionERC20TransferBatch terms: must be at least 92 bytes', + ); + + const tokenAddressHex = extractAddress(hexTerms, 0); + const recipientHex = extractAddress(hexTerms, 20); + const amount = extractBigInt(hexTerms, 40, 32); + const targetHex = extractAddress(hexTerms, 72); + const calldataHex = extractRemainingHex(hexTerms, 92); + + return { + tokenAddress: prepareResult(tokenAddressHex, encodingOptions), + recipient: prepareResult(recipientHex, encodingOptions), + amount, + target: prepareResult(targetHex, encodingOptions), + calldata: prepareResult(calldataHex, encodingOptions), + } as + | SpecificActionERC20TransferBatchTerms> + | SpecificActionERC20TransferBatchTerms>; +} diff --git a/packages/delegation-core/src/caveats/timestamp.ts b/packages/delegation-core/src/caveats/timestamp.ts index e6f45998..26368086 100644 --- a/packages/delegation-core/src/caveats/timestamp.ts +++ b/packages/delegation-core/src/caveats/timestamp.ts @@ -1,5 +1,20 @@ -import { toHexString } from '../internalUtils'; +/** + * ## TimestampEnforcer + * + * Restricts redemption to a unix timestamp window (strict inequalities on-chain: valid when `block.timestamp > afterThreshold` if after is set, and `block.timestamp < beforeThreshold` if before is set). + * + * Terms are encoded as two 16-byte big-endian fields: timestamp after, then timestamp before (each zero-padded; interpreted as `uint128`). + */ + +import type { BytesLike } from '@metamask/utils'; + import { + assertHexByteExactLength, + extractNumber, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -15,9 +30,9 @@ const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; */ export type TimestampTerms = { /** The timestamp (in seconds) after which the delegation can be used. */ - timestampAfterThreshold: number; + afterThreshold: number; /** The timestamp (in seconds) before which the delegation can be used. */ - timestampBeforeThreshold: number; + beforeThreshold: number; }; /** @@ -25,7 +40,7 @@ export type TimestampTerms = { * * @param terms - The terms for the Timestamp caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 32-byte hex string (16 bytes for each timestamp). + * @returns Encoded terms. * @throws Error if the timestamps are invalid. */ export function createTimestampTerms( @@ -41,54 +56,47 @@ export function createTimestampTerms( * * @param terms - The terms for the Timestamp caveat. * @param encodingOptions - The encoding options for the result. - * @returns The terms as a 32-byte hex string (16 bytes for each timestamp). + * @returns Encoded terms. * @throws Error if the timestamps are invalid. */ export function createTimestampTerms( terms: TimestampTerms, encodingOptions: EncodingOptions = defaultOptions, ): Hex | Uint8Array { - const { timestampAfterThreshold, timestampBeforeThreshold } = terms; + const { afterThreshold, beforeThreshold } = terms; - if (timestampAfterThreshold < 0) { - throw new Error( - 'Invalid timestampAfterThreshold: must be zero or positive', - ); + if (afterThreshold < 0) { + throw new Error('Invalid afterThreshold: must be zero or positive'); } - if (timestampBeforeThreshold < 0) { - throw new Error( - 'Invalid timestampBeforeThreshold: must be zero or positive', - ); + if (beforeThreshold < 0) { + throw new Error('Invalid beforeThreshold: must be zero or positive'); } - if (timestampBeforeThreshold > TIMESTAMP_UPPER_BOUND_SECONDS) { + if (beforeThreshold > TIMESTAMP_UPPER_BOUND_SECONDS) { throw new Error( - `Invalid timestampBeforeThreshold: must be less than or equal to ${TIMESTAMP_UPPER_BOUND_SECONDS}`, + `Invalid beforeThreshold: must be less than or equal to ${TIMESTAMP_UPPER_BOUND_SECONDS}`, ); } - if (timestampAfterThreshold > TIMESTAMP_UPPER_BOUND_SECONDS) { + if (afterThreshold > TIMESTAMP_UPPER_BOUND_SECONDS) { throw new Error( - `Invalid timestampAfterThreshold: must be less than or equal to ${TIMESTAMP_UPPER_BOUND_SECONDS}`, + `Invalid afterThreshold: must be less than or equal to ${TIMESTAMP_UPPER_BOUND_SECONDS}`, ); } - if ( - timestampBeforeThreshold !== 0 && - timestampAfterThreshold >= timestampBeforeThreshold - ) { + if (beforeThreshold !== 0 && afterThreshold >= beforeThreshold) { throw new Error( - 'Invalid thresholds: timestampBeforeThreshold must be greater than timestampAfterThreshold when both are specified', + 'Invalid thresholds: beforeThreshold must be greater than afterThreshold when both are specified', ); } const afterThresholdHex = toHexString({ - value: timestampAfterThreshold, + value: afterThreshold, size: 16, }); const beforeThresholdHex = toHexString({ - value: timestampBeforeThreshold, + value: beforeThreshold, size: 16, }); @@ -96,3 +104,21 @@ export function createTimestampTerms( return prepareResult(hexValue, encodingOptions); } + +/** + * Decodes terms for a Timestamp caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @returns The decoded TimestampTerms object. + */ +export function decodeTimestampTerms(terms: BytesLike): TimestampTerms { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 32, + 'Invalid Timestamp terms: must be exactly 32 bytes', + ); + const afterThreshold = extractNumber(hexTerms, 0, 16); + const beforeThreshold = extractNumber(hexTerms, 16, 16); + return { afterThreshold, beforeThreshold }; +} diff --git a/packages/delegation-core/src/caveats/valueLte.ts b/packages/delegation-core/src/caveats/valueLte.ts index af5d65b3..07488f0b 100644 --- a/packages/delegation-core/src/caveats/valueLte.ts +++ b/packages/delegation-core/src/caveats/valueLte.ts @@ -1,5 +1,20 @@ -import { toHexString } from '../internalUtils'; +/** + * ## ValueLteEnforcer + * + * Limits the native token (wei) value allowed per execution. + * + * Terms are encoded as a single 32-byte big-endian uint256 max value. + */ + +import type { BytesLike } from '@metamask/utils'; + import { + assertHexByteExactLength, + extractBigInt, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -20,7 +35,7 @@ export type ValueLteTerms = { * * @param terms - The terms for the ValueLte caveat. * @param options - The encoding options for the result. - * @returns The terms as a 32-byte hex string. + * @returns Encoded terms. * @throws Error if the maxValue is negative. */ export function createValueLteTerms( @@ -36,7 +51,7 @@ export function createValueLteTerms( * * @param terms - The terms for the ValueLte caveat. * @param options - The encoding options for the result. - * @returns The terms as a 32-byte hex string. + * @returns Encoded terms. * @throws Error if the maxValue is negative. */ export function createValueLteTerms( @@ -52,3 +67,20 @@ export function createValueLteTerms( return prepareResult(hexValue, options); } + +/** + * Decodes terms for a ValueLte caveat from encoded hex data. + * + * @param terms - The encoded terms as a hex string or Uint8Array. + * @returns The decoded ValueLteTerms object. + */ +export function decodeValueLteTerms(terms: BytesLike): ValueLteTerms { + const hexTerms = bytesLikeToHex(terms); + assertHexByteExactLength( + hexTerms, + 32, + 'Invalid ValueLte terms: must be exactly 32 bytes', + ); + const maxValue = extractBigInt(hexTerms, 0, 32); + return { maxValue }; +} diff --git a/packages/delegation-core/src/index.ts b/packages/delegation-core/src/index.ts index e0bc5fa2..8b1a3793 100644 --- a/packages/delegation-core/src/index.ts +++ b/packages/delegation-core/src/index.ts @@ -6,36 +6,67 @@ export type { export { createValueLteTerms, + decodeValueLteTerms, createTimestampTerms, + decodeTimestampTerms, createNativeTokenPeriodTransferTerms, + decodeNativeTokenPeriodTransferTerms, createExactCalldataTerms, + decodeExactCalldataTerms, createExactCalldataBatchTerms, + decodeExactCalldataBatchTerms, createExactExecutionTerms, + decodeExactExecutionTerms, createExactExecutionBatchTerms, + decodeExactExecutionBatchTerms, createNativeTokenStreamingTerms, + decodeNativeTokenStreamingTerms, createNativeTokenTransferAmountTerms, + decodeNativeTokenTransferAmountTerms, createNativeTokenPaymentTerms, + decodeNativeTokenPaymentTerms, createNativeBalanceChangeTerms, + decodeNativeBalanceChangeTerms, createERC20StreamingTerms, + decodeERC20StreamingTerms, createERC20TokenPeriodTransferTerms, + decodeERC20TokenPeriodTransferTerms, createERC20TransferAmountTerms, + decodeERC20TransferAmountTerms, createERC20BalanceChangeTerms, + decodeERC20BalanceChangeTerms, createERC721BalanceChangeTerms, + decodeERC721BalanceChangeTerms, createERC721TransferTerms, + decodeERC721TransferTerms, createERC1155BalanceChangeTerms, + decodeERC1155BalanceChangeTerms, createNonceTerms, + decodeNonceTerms, createAllowedCalldataTerms, + decodeAllowedCalldataTerms, createAllowedMethodsTerms, + decodeAllowedMethodsTerms, createAllowedTargetsTerms, + decodeAllowedTargetsTerms, createArgsEqualityCheckTerms, + decodeArgsEqualityCheckTerms, createBlockNumberTerms, + decodeBlockNumberTerms, createDeployedTerms, + decodeDeployedTerms, createIdTerms, + decodeIdTerms, createLimitedCallsTerms, + decodeLimitedCallsTerms, createMultiTokenPeriodTerms, + decodeMultiTokenPeriodTerms, createOwnershipTransferTerms, + decodeOwnershipTransferTerms, createRedeemerTerms, + decodeRedeemerTerms, createSpecificActionERC20TransferBatchTerms, + decodeSpecificActionERC20TransferBatchTerms, } from './caveats'; export { diff --git a/packages/delegation-core/src/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts index 61906ba2..5ec4295b 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -3,6 +3,7 @@ import { hexToBytes, isHexString, remove0x, + type Hex, type BytesLike, } from '@metamask/utils'; @@ -112,3 +113,144 @@ export const normalizeAddressLowercase = ( export const concatHex = (parts: string[]): string => { return `0x${parts.map(remove0x).join('')}`; }; + +/** + * Extracts a bigint value from a hex string at a specific byte offset. + * + * @param value - The hex string to extract from. + * @param offset - The byte offset to start extraction. + * @param size - The number of bytes to extract. + * @returns The extracted bigint value. + */ +export const extractBigInt = ( + value: Hex, + offset: number, + size: number, +): bigint => { + const start = 2 + offset * 2; + const end = start + size * 2; + const slice = value.slice(start, end); + + return BigInt(`0x${slice}`); +}; + +/** + * Extracts a number value from a hex string at a specific byte offset. + * + * @param value - The hex string to extract from. + * @param offset - The byte offset to start extraction. + * @param size - The number of bytes to extract. + * @returns The extracted number value. + */ +export const extractNumber = ( + value: Hex, + offset: number, + size: number, +): number => { + const bigIntValue = extractBigInt(value, offset, size); + + if (bigIntValue > Number.MAX_SAFE_INTEGER) { + throw new Error('Number is too large'); + } + + return Number(bigIntValue); +}; + +/** + * Extracts an address from a hex string at a specific byte offset. + * + * @param value - The hex string to extract from. + * @param offset - The byte offset to start extraction. + * @returns The extracted address as a 0x-prefixed hex string. + */ +export const extractAddress = (value: Hex, offset: number): Hex => { + const start = 2 + offset * 2; + const end = start + 40; + + return `0x${value.slice(start, end)}`; +}; + +/** + * Extracts a hex slice from a hex string at a specific byte offset. + * + * @param value - The hex string to extract from. + * @param offset - The byte offset to start extraction. + * @param size - The number of bytes to extract. + * @returns The extracted hex string (0x-prefixed). + */ +export const extractHex = (value: Hex, offset: number, size: number): Hex => { + const start = 2 + offset * 2; + const end = start + size * 2; + + return `0x${value.slice(start, end)}`; +}; + +/** + * Extracts the remaining hex data from a hex string starting at a specific byte offset. + * + * @param value - The hex string to extract from. + * @param offset - The byte offset to start extraction. + * @returns The extracted hex string (0x-prefixed). + */ +export const extractRemainingHex = (value: Hex, offset: number): Hex => { + const start = 2 + offset * 2; + + return `0x${value.slice(start)}`; +}; + +/** + * @param value - `0x`-prefixed hex string. + * @returns Byte length of the hex data (excluding the `0x` prefix). + */ +export function getByteLength(value: Hex): number { + return (value.length - 2) / 2; +} + +/** + * @param hexTerms - `0x`-prefixed hex string (encoded caveat terms). + * @param expectedBytes - Required payload length in bytes. + * @param errorMessage - Message for the thrown `Error` when length does not match. + * @throws Error if the payload is not exactly `expectedBytes` long. + */ +export function assertHexByteExactLength( + hexTerms: Hex, + expectedBytes: number, + errorMessage: string, +): void { + if (getByteLength(hexTerms) !== expectedBytes) { + throw new Error(errorMessage); + } +} + +/** + * @param hexTerms - `0x`-prefixed hex string (encoded caveat terms). + * @param unitBytes - Payload length must be divisible by this many bytes. + * @param errorMessage - Message for the thrown `Error` when length is not a multiple. + * @throws Error if the payload length is not (at least one) a multiple of `unitBytes`. + */ +export function assertHexByteLengthAtLeastOneMultipleOf( + hexTerms: Hex, + unitBytes: number, + errorMessage: string, +): void { + const byteLength = getByteLength(hexTerms); + if (byteLength === 0 || byteLength % unitBytes !== 0) { + throw new Error(errorMessage); + } +} + +/** + * @param hexTerms - `0x`-prefixed hex string (encoded caveat terms). + * @param minBytes - Minimum payload length in bytes (inclusive). + * @param errorMessage - Message for the thrown `Error` when payload is too short. + * @throws Error if the payload is shorter than `minBytes`. + */ +export function assertHexBytesMinLength( + hexTerms: Hex, + minBytes: number, + errorMessage: string, +): void { + if (getByteLength(hexTerms) < minBytes) { + throw new Error(errorMessage); + } +} diff --git a/packages/delegation-core/src/returns.ts b/packages/delegation-core/src/returns.ts index faf38645..0d2f1e2a 100644 --- a/packages/delegation-core/src/returns.ts +++ b/packages/delegation-core/src/returns.ts @@ -13,6 +13,13 @@ export type ResultValue = 'hex' | 'bytes'; export type ResultType = TResultValue extends 'hex' ? Hex : Uint8Array; +/** + * Concrete type for a decoded Bytes-like field when using {@link EncodingOptions}. + * Matches {@link ResultType}; alias for readability on `*Terms` generics. + */ +export type DecodedBytesLike = + ResultType; + /** * Base options interface for operations that can return hex or bytes. */ diff --git a/packages/delegation-core/test/caveats/allowedCalldata.test.ts b/packages/delegation-core/test/caveats/allowedCalldata.test.ts index 8709e653..5e13c7ad 100644 --- a/packages/delegation-core/test/caveats/allowedCalldata.test.ts +++ b/packages/delegation-core/test/caveats/allowedCalldata.test.ts @@ -1,380 +1,471 @@ import { hexToBytes } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { createAllowedCalldataTerms } from '../../src/caveats/allowedCalldata'; +import { + createAllowedCalldataTerms, + decodeAllowedCalldataTerms, +} from '../../src/caveats/allowedCalldata'; import { toHexString } from '../../src/internalUtils'; import type { Hex } from '../../src/types'; -describe('createAllowedCalldataTerms', function () { - const prefixWithIndex = (startIndex: number, value: Hex): Hex => { - const indexHex = toHexString({ value: startIndex, size: 32 }); - return `0x${indexHex}${value.slice(2)}` as Hex; - }; - - // Note: AllowedCalldata terms length varies based on input calldata length + 32-byte index prefix - it('creates valid terms for simple value', () => { - const value = '0x1234567890abcdef'; - const startIndex = 0; - const result = createAllowedCalldataTerms({ startIndex, value }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); - }); - - it('creates valid terms for empty value', () => { - const value = '0x'; - const startIndex = 5; - const result = createAllowedCalldataTerms({ startIndex, value }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); - }); +describe('AllowedCalldata', () => { + describe('createAllowedCalldataTerms', function () { + const prefixWithIndex = (startIndex: number, value: Hex): Hex => { + const indexHex = toHexString({ value: startIndex, size: 32 }); + return `0x${indexHex}${value.slice(2)}` as Hex; + }; - it('creates valid terms for function call with parameters', () => { - // Example: transfer(address,uint256) function call - const value = - '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000'; - const startIndex = 12; - const result = createAllowedCalldataTerms({ startIndex, value }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); - }); + // Note: AllowedCalldata terms length varies based on input calldata length + 32-byte index prefix + it('creates valid terms for simple value', () => { + const value = '0x1234567890abcdef'; + const startIndex = 0; + const result = createAllowedCalldataTerms({ startIndex, value }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); + }); - it('creates valid terms for complex value', () => { - const value = - '0x23b872dd000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5f0000000000000000000000000000000000000000000000000de0b6b3a7640000'; - const startIndex = 4; - const result = createAllowedCalldataTerms({ startIndex, value }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); - }); + it('creates valid terms for empty value', () => { + const value = '0x'; + const startIndex = 5; + const result = createAllowedCalldataTerms({ startIndex, value }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); + }); - it('creates valid terms for uppercase hex value', () => { - const value = '0x1234567890ABCDEF'; - const startIndex = 0; - const result = createAllowedCalldataTerms({ startIndex, value }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); - }); + it('creates valid terms for function call with parameters', () => { + // Example: transfer(address,uint256) function call + const value = + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + const startIndex = 12; + const result = createAllowedCalldataTerms({ startIndex, value }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); + }); - it('creates valid terms for mixed case hex value', () => { - const value = '0x1234567890AbCdEf'; - const startIndex = 1; - const result = createAllowedCalldataTerms({ startIndex, value }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); - }); + it('creates valid terms for complex value', () => { + const value = + '0x23b872dd000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5f0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + const startIndex = 4; + const result = createAllowedCalldataTerms({ startIndex, value }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); + }); - it('creates valid terms for very long value', () => { - const longValue: Hex = `0x${'a'.repeat(1000)}`; - const startIndex = 31; - const result = createAllowedCalldataTerms({ - startIndex, - value: longValue, + it('creates valid terms for uppercase hex value', () => { + const value = '0x1234567890ABCDEF'; + const startIndex = 0; + const result = createAllowedCalldataTerms({ startIndex, value }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, longValue)); - }); - it('throws an error for value without 0x prefix', () => { - const invalidValue = '1234567890abcdef' as Hex; - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: invalidValue, - }), - ).toThrow('Invalid value: must be a hex string starting with 0x'); - }); + it('creates valid terms for mixed case hex value', () => { + const value = '0x1234567890AbCdEf'; + const startIndex = 1; + const result = createAllowedCalldataTerms({ startIndex, value }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); + }); - it('throws an error for empty string', () => { - const invalidValue = '' as Hex; - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: invalidValue, - }), - ).toThrow('Invalid value: must be a hex string starting with 0x'); - }); + it('creates valid terms for very long value', () => { + const longValue: Hex = `0x${'a'.repeat(1000)}`; + const startIndex = 31; + const result = createAllowedCalldataTerms({ + startIndex, + value: longValue, + }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, longValue)); + }); - it('throws an error for malformed hex prefix', () => { - const invalidValue = '0X1234' as Hex; // uppercase X - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: invalidValue, - }), - ).toThrow('Invalid value: must be a hex string starting with 0x'); - }); + it('throws an error for value without 0x prefix', () => { + const invalidValue = '1234567890abcdef' as Hex; + expect(() => + createAllowedCalldataTerms({ + startIndex: 0, + value: invalidValue, + }), + ).toThrow('Invalid value: must be a hex string starting with 0x'); + }); - it('throws an error for undefined value', () => { - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: undefined as unknown as Hex, - }), - ).toThrow(); - }); + it('throws an error for empty string', () => { + const invalidValue = '' as Hex; + expect(() => + createAllowedCalldataTerms({ + startIndex: 0, + value: invalidValue, + }), + ).toThrow('Invalid value: must be a hex string starting with 0x'); + }); - it('throws an error for null value', () => { - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: null as unknown as Hex, - }), - ).toThrow(); - }); + it('throws an error for malformed hex prefix', () => { + const invalidValue = '0X1234' as Hex; // uppercase X + expect(() => + createAllowedCalldataTerms({ + startIndex: 0, + value: invalidValue, + }), + ).toThrow('Invalid value: must be a hex string starting with 0x'); + }); - it('throws an error for non-string non-Uint8Array value', () => { - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: 1234 as unknown as Hex, - }), - ).toThrow(); - }); + it('throws an error for undefined value', () => { + expect(() => + createAllowedCalldataTerms({ + startIndex: 0, + value: undefined as unknown as Hex, + }), + ).toThrow(); + }); - it('handles single function selector', () => { - const functionSelector = '0xa9059cbb'; // transfer(address,uint256) selector - const startIndex = 7; - const result = createAllowedCalldataTerms({ - startIndex, - value: functionSelector, + it('throws an error for null value', () => { + expect(() => + createAllowedCalldataTerms({ + startIndex: 0, + value: null as unknown as Hex, + }), + ).toThrow(); }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, functionSelector)); - }); - it('handles calldata with odd length', () => { - const oddLengthValue = '0x123'; - const startIndex = 0; - const result = createAllowedCalldataTerms({ - startIndex, - value: oddLengthValue, + it('throws an error for non-string non-Uint8Array value', () => { + expect(() => + createAllowedCalldataTerms({ + startIndex: 0, + value: 1234 as unknown as Hex, + }), + ).toThrow(); }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, oddLengthValue)); - }); - // Tests for bytes return type - describe('bytes return type', () => { - it('returns Uint8Array when bytes encoding is specified', () => { - const value = '0x1234567890abcdef'; - const startIndex = 2; - const result = createAllowedCalldataTerms( - { startIndex, value }, - { out: 'bytes' }, + it('handles single function selector', () => { + const functionSelector = '0xa9059cbb'; // transfer(address,uint256) selector + const startIndex = 7; + const result = createAllowedCalldataTerms({ + startIndex, + value: functionSelector, + }); + expect(result).toStrictEqual( + prefixWithIndex(startIndex, functionSelector), ); - // Expect: 32 bytes index + 8 bytes value = 40 bytes - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32 + 8); - // Verify prefix bytes equal encoded index - const expectedPrefixHex = toHexString({ value: startIndex, size: 32 }); - const expectedPrefix = Array.from(hexToBytes(expectedPrefixHex)); - expect(Array.from(result.slice(0, 32))).toEqual(expectedPrefix); - // Verify value bytes at the end - expect(Array.from(result.slice(32))).toEqual([ - 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, - ]); }); - it('returns Uint8Array for empty value with bytes encoding', () => { - const value = '0x'; + it('handles calldata with odd length', () => { + const oddLengthValue = '0x123'; const startIndex = 0; - const result = createAllowedCalldataTerms( - { startIndex, value }, - { out: 'bytes' }, - ); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); // just the index prefix + const result = createAllowedCalldataTerms({ + startIndex, + value: oddLengthValue, + }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, oddLengthValue)); }); - it('returns Uint8Array for complex value with bytes encoding', () => { - const value = - '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000'; - const startIndex = 15; - const result = createAllowedCalldataTerms( - { startIndex, value }, - { out: 'bytes' }, - ); - expect(result).toBeInstanceOf(Uint8Array); - // 32 prefix + 68 bytes calldata - expect(result).toHaveLength(32 + 68); + // Tests for bytes return type + describe('bytes return type', () => { + it('returns Uint8Array when bytes encoding is specified', () => { + const value = '0x1234567890abcdef'; + const startIndex = 2; + const result = createAllowedCalldataTerms( + { startIndex, value }, + { out: 'bytes' }, + ); + // Expect: 32 bytes index + 8 bytes value = 40 bytes + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32 + 8); + // Verify prefix bytes equal encoded index + const expectedPrefixHex = toHexString({ value: startIndex, size: 32 }); + const expectedPrefix = Array.from(hexToBytes(expectedPrefixHex)); + expect(Array.from(result.slice(0, 32))).toEqual(expectedPrefix); + // Verify value bytes at the end + expect(Array.from(result.slice(32))).toEqual([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, + ]); + }); + + it('returns Uint8Array for empty value with bytes encoding', () => { + const value = '0x'; + const startIndex = 0; + const result = createAllowedCalldataTerms( + { startIndex, value }, + { out: 'bytes' }, + ); + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32); // just the index prefix + }); + + it('returns Uint8Array for complex value with bytes encoding', () => { + const value = + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + const startIndex = 15; + const result = createAllowedCalldataTerms( + { startIndex, value }, + { out: 'bytes' }, + ); + expect(result).toBeInstanceOf(Uint8Array); + // 32 prefix + 68 bytes calldata + expect(result).toHaveLength(32 + 68); + }); + }); + + describe('startIndex validation', () => { + it('throws for negative integer startIndex', () => { + const value = '0x1234'; + expect(() => + createAllowedCalldataTerms({ startIndex: -1, value }), + ).toThrow('Invalid startIndex: must be zero or positive'); + }); + + it('throws for negative fractional startIndex', () => { + const value = '0x1234'; + expect(() => + createAllowedCalldataTerms({ startIndex: -0.1, value }), + ).toThrow('Invalid startIndex: must be zero or positive'); + }); + + it('throws for non-integer positive startIndex', () => { + const value = '0x1234'; + expect(() => + createAllowedCalldataTerms({ startIndex: 1.5, value }), + ).toThrow('Invalid startIndex: must be a whole number'); + }); + + it('throws for NaN startIndex', () => { + const value = '0x1234'; + expect(() => + createAllowedCalldataTerms({ startIndex: Number.NaN, value }), + ).toThrow('Invalid startIndex: must be a whole number'); + }); + + it('throws for Infinity startIndex', () => { + const value = '0x1234'; + expect(() => + createAllowedCalldataTerms({ startIndex: Infinity, value }), + ).toThrow('Invalid startIndex: must be a whole number'); + }); + + it('accepts zero startIndex', () => { + const value = '0xdeadbeef'; + const result = createAllowedCalldataTerms({ startIndex: 0, value }); + expect(result).toStrictEqual(prefixWithIndex(0, value)); + }); + + it('accepts large integer startIndex and encodes correctly', () => { + const value = '0x00'; + const large = Number.MAX_SAFE_INTEGER; // 2^53 - 1 + const result = createAllowedCalldataTerms({ + startIndex: large, + value, + }); + const expected = prefixWithIndex(large, value); + expect(result).toStrictEqual(expected); + // Check length: 32-byte prefix + 1-byte calldata = 33 bytes hex (66 chars) + '0x' + expect(result.length).toBe(2 + 64 + 2); + }); + }); + + // Tests for Uint8Array input parameter + describe('Uint8Array input parameter', () => { + it('accepts Uint8Array as value parameter', () => { + const valueBytes = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, + ]); + const startIndex = 3; + const result = createAllowedCalldataTerms({ + startIndex, + value: valueBytes, + }); + expect(result).toStrictEqual( + prefixWithIndex(startIndex, '0x1234567890abcdef'), + ); + }); + + it('accepts empty Uint8Array as value parameter', () => { + const callDataBytes = new Uint8Array([]); + const startIndex = 0; + const result = createAllowedCalldataTerms({ + startIndex, + value: callDataBytes, + }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, '0x')); + }); + + it('accepts Uint8Array for function call with parameters', () => { + // transfer(address,uint256) function call as bytes + const valueBytes = new Uint8Array([ + 0xa9, + 0x05, + 0x9c, + 0xbb, // transfer selector + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, // padding + 0x74, + 0x2d, + 0x35, + 0xcc, + 0x66, + 0x34, + 0xc0, + 0x53, + 0x29, + 0x25, + 0xa3, + 0xb8, + 0xd4, + 0x0e, + 0xc4, + 0x9b, + 0x0e, + 0x8b, + 0xaa, + 0x5e, // address + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0d, + 0xe0, + 0xb6, + 0xb3, + 0xa7, + 0x64, + 0x00, + 0x00, // amount + ]); + const startIndex = 8; + const result = createAllowedCalldataTerms({ + startIndex, + value: valueBytes, + }); + expect(result).toStrictEqual( + prefixWithIndex( + startIndex, + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000', + ), + ); + }); + + it('returns Uint8Array when input is Uint8Array and bytes encoding is specified', () => { + const valueBytes = new Uint8Array([0x12, 0x34, 0x56, 0x78]); + const startIndex = 0; + const result = createAllowedCalldataTerms( + { startIndex, value: valueBytes }, + { out: 'bytes' }, + ); + expect(result).toBeInstanceOf(Uint8Array); + expect(Array.from(result)).toEqual([ + // 32-byte index (all zeros) + ...new Array(32).fill(0x00), + // calldata + 0x12, + 0x34, + 0x56, + 0x78, + ]); + }); }); }); - describe('startIndex validation', () => { - it('throws for negative integer startIndex', () => { - const value = '0x1234'; - expect(() => - createAllowedCalldataTerms({ startIndex: -1, value }), - ).toThrow('Invalid startIndex: must be zero or positive'); + describe('decodeAllowedCalldataTerms', () => { + it('decodes simple value and start index', () => { + const original = { startIndex: 0, value: '0x1234567890abcdef' as Hex }; + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); - it('throws for negative fractional startIndex', () => { - const value = '0x1234'; - expect(() => - createAllowedCalldataTerms({ startIndex: -0.1, value }), - ).toThrow('Invalid startIndex: must be zero or positive'); + it('decodes empty value with non-zero start index', () => { + const original = { startIndex: 5, value: '0x' as Hex }; + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); - it('throws for non-integer positive startIndex', () => { - const value = '0x1234'; - expect(() => - createAllowedCalldataTerms({ startIndex: 1.5, value }), - ).toThrow('Invalid startIndex: must be a whole number'); + it('decodes function call shaped calldata', () => { + const value = + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000' as Hex; + const original = { startIndex: 12, value }; + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); - it('throws for NaN startIndex', () => { - const value = '0x1234'; - expect(() => - createAllowedCalldataTerms({ startIndex: Number.NaN, value }), - ).toThrow('Invalid startIndex: must be a whole number'); + it('preserves hex casing in the value slice', () => { + const original = { startIndex: 0, value: '0x1234567890ABCDEF' as Hex }; + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); - it('throws for Infinity startIndex', () => { - const value = '0x1234'; - expect(() => - createAllowedCalldataTerms({ startIndex: Infinity, value }), - ).toThrow('Invalid startIndex: must be a whole number'); + it('decodes odd-length hex value', () => { + const original = { startIndex: 0, value: '0x123' as Hex }; + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); - it('accepts zero startIndex', () => { - const value = '0xdeadbeef'; - const result = createAllowedCalldataTerms({ startIndex: 0, value }); - expect(result).toStrictEqual(prefixWithIndex(0, value)); + it('decodes very long value', () => { + const value: Hex = `0x${'a'.repeat(1000)}`; + const original = { startIndex: 31, value }; + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); - it('accepts large integer startIndex and encodes correctly', () => { - const value = '0x00'; - const large = Number.MAX_SAFE_INTEGER; // 2^53 - 1 - const result = createAllowedCalldataTerms({ - startIndex: large, - value, - }); - const expected = prefixWithIndex(large, value); - expect(result).toStrictEqual(expected); - // Check length: 32-byte prefix + 1-byte calldata = 33 bytes hex (66 chars) + '0x' - expect(result.length).toBe(2 + 64 + 2); + it('decodes large start index', () => { + const original = { + startIndex: Number.MAX_SAFE_INTEGER, + value: '0x00' as Hex, + }; + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); - }); - // Tests for Uint8Array input parameter - describe('Uint8Array input parameter', () => { - it('accepts Uint8Array as value parameter', () => { + it('decodes terms created from Uint8Array value', () => { const valueBytes = new Uint8Array([ 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, ]); const startIndex = 3; - const result = createAllowedCalldataTerms({ + const encoded = createAllowedCalldataTerms({ startIndex, value: valueBytes, }); - expect(result).toStrictEqual( - prefixWithIndex(startIndex, '0x1234567890abcdef'), - ); - }); - - it('accepts empty Uint8Array as value parameter', () => { - const callDataBytes = new Uint8Array([]); - const startIndex = 0; - const result = createAllowedCalldataTerms({ + expect(decodeAllowedCalldataTerms(encoded)).toStrictEqual({ startIndex, - value: callDataBytes, + value: '0x1234567890abcdef', }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, '0x')); }); - it('accepts Uint8Array for function call with parameters', () => { - // transfer(address,uint256) function call as bytes - const valueBytes = new Uint8Array([ - 0xa9, - 0x05, - 0x9c, - 0xbb, // transfer selector - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, // padding - 0x74, - 0x2d, - 0x35, - 0xcc, - 0x66, - 0x34, - 0xc0, - 0x53, - 0x29, - 0x25, - 0xa3, - 0xb8, - 0xd4, - 0x0e, - 0xc4, - 0x9b, - 0x0e, - 0x8b, - 0xaa, - 0x5e, // address - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0d, - 0xe0, - 0xb6, - 0xb3, - 0xa7, - 0x64, - 0x00, - 0x00, // amount - ]); - const startIndex = 8; - const result = createAllowedCalldataTerms({ - startIndex, - value: valueBytes, - }); - expect(result).toStrictEqual( - prefixWithIndex( - startIndex, - '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000', - ), - ); + it('accepts Uint8Array terms from the encoder', () => { + const original = { startIndex: 2, value: '0x1234567890abcdef' as Hex }; + const bytes = createAllowedCalldataTerms(original, { out: 'bytes' }); + expect(decodeAllowedCalldataTerms(bytes)).toStrictEqual(original); }); - it('returns Uint8Array when input is Uint8Array and bytes encoding is specified', () => { - const valueBytes = new Uint8Array([0x12, 0x34, 0x56, 0x78]); - const startIndex = 0; - const result = createAllowedCalldataTerms( - { startIndex, value: valueBytes }, - { out: 'bytes' }, + it('throws when encoded terms are shorter than 32 bytes', () => { + expect(() => decodeAllowedCalldataTerms(`0x${'00'.repeat(31)}`)).toThrow( + 'Invalid AllowedCalldata terms: must be at least 32 bytes', ); - expect(result).toBeInstanceOf(Uint8Array); - expect(Array.from(result)).toEqual([ - // 32-byte index (all zeros) - ...new Array(32).fill(0x00), - // calldata - 0x12, - 0x34, - 0x56, - 0x78, - ]); }); }); }); diff --git a/packages/delegation-core/test/caveats/allowedMethods.test.ts b/packages/delegation-core/test/caveats/allowedMethods.test.ts index e6707c50..1cf926e0 100644 --- a/packages/delegation-core/test/caveats/allowedMethods.test.ts +++ b/packages/delegation-core/test/caveats/allowedMethods.test.ts @@ -1,60 +1,100 @@ import { describe, it, expect } from 'vitest'; -import { createAllowedMethodsTerms } from '../../src/caveats/allowedMethods'; +import { + createAllowedMethodsTerms, + decodeAllowedMethodsTerms, +} from '../../src/caveats/allowedMethods'; -describe('createAllowedMethodsTerms', () => { - const selectorA = '0xa9059cbb'; - const selectorB = '0x70a08231'; +describe('AllowedMethods', () => { + describe('createAllowedMethodsTerms', () => { + const selectorA = '0xa9059cbb'; + const selectorB = '0x70a08231'; - it('creates valid terms for selectors', () => { - const result = createAllowedMethodsTerms({ - selectors: [selectorA, selectorB], + it('creates valid terms for selectors', () => { + const result = createAllowedMethodsTerms({ + selectors: [selectorA, selectorB], + }); + + expect(result).toStrictEqual('0xa9059cbb70a08231'); }); - 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 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', + ); + }); - 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 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createAllowedMethodsTerms( - { selectors: [selectorA, selectorB] }, - { out: 'bytes' }, - ); + describe('decodeAllowedMethodsTerms', () => { + const selectorA = '0xa9059cbb' as `0x${string}`; + const selectorB = '0x70a08231' as `0x${string}`; - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(8); + it('decodes multiple selectors', () => { + const original = { selectors: [selectorA, selectorB] }; + expect( + decodeAllowedMethodsTerms(createAllowedMethodsTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes a single selector', () => { + const original = { selectors: [selectorA] }; + expect( + decodeAllowedMethodsTerms(createAllowedMethodsTerms(original)), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createAllowedMethodsTerms( + { selectors: [selectorA, selectorB] }, + { out: 'bytes' }, + ); + expect(decodeAllowedMethodsTerms(bytes)).toStrictEqual({ + selectors: [selectorA, selectorB], + }); + }); + + it('throws when encoded terms length is not a multiple of 4 bytes', () => { + expect(() => decodeAllowedMethodsTerms(`0x${'00'.repeat(3)}`)).toThrow( + 'Invalid selectors: must be a multiple of 4', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/allowedTargets.test.ts b/packages/delegation-core/test/caveats/allowedTargets.test.ts index e50c6c77..87e5874c 100644 --- a/packages/delegation-core/test/caveats/allowedTargets.test.ts +++ b/packages/delegation-core/test/caveats/allowedTargets.test.ts @@ -1,48 +1,90 @@ import { describe, it, expect } from 'vitest'; -import { createAllowedTargetsTerms } from '../../src/caveats/allowedTargets'; +import { + createAllowedTargetsTerms, + decodeAllowedTargetsTerms, +} from '../../src/caveats/allowedTargets'; -describe('createAllowedTargetsTerms', () => { - const addressA = '0x0000000000000000000000000000000000000001'; - const addressB = '0x0000000000000000000000000000000000000002'; +describe('AllowedTargets', () => { + describe('createAllowedTargetsTerms', () => { + const addressA = '0x0000000000000000000000000000000000000001'; + const addressB = '0x0000000000000000000000000000000000000002'; - it('creates valid terms for multiple addresses', () => { - const result = createAllowedTargetsTerms({ targets: [addressA, addressB] }); + it('creates valid terms for multiple addresses', () => { + const result = createAllowedTargetsTerms({ + targets: [addressA, addressB], + }); - expect(result).toStrictEqual( - '0x00000000000000000000000000000000000000010000000000000000000000000000000000000002', - ); - }); + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000010000000000000000000000000000000000000002', + ); + }); - it('throws when targets is undefined', () => { - expect(() => - createAllowedTargetsTerms( - {} as Parameters[0], - ), - ).toThrow('Invalid targets: must provide at least one target address'); - }); + 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', - ); - }); + 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createAllowedTargetsTerms( - { targets: [addressA, addressB] }, - { out: 'bytes' }, - ); + describe('decodeAllowedTargetsTerms', () => { + const addressA = + '0x0000000000000000000000000000000000000001' as `0x${string}`; + const addressB = + '0x0000000000000000000000000000000000000002' as `0x${string}`; + + it('decodes multiple targets', () => { + const original = { targets: [addressA, addressB] }; + expect( + decodeAllowedTargetsTerms(createAllowedTargetsTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes a single target', () => { + const original = { targets: [addressA] }; + expect( + decodeAllowedTargetsTerms(createAllowedTargetsTerms(original)), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const original = { targets: [addressA, addressB] }; + const bytes = createAllowedTargetsTerms(original, { out: 'bytes' }); + expect(decodeAllowedTargetsTerms(bytes)).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(40); + it('throws when encoded terms length is not a multiple of 20 bytes', () => { + // 20 bytes + 19 bytes + const thirtyNineBytes = `0x${'00'.repeat(39)}` as const; + expect(() => decodeAllowedTargetsTerms(thirtyNineBytes)).toThrow( + 'Invalid targets: must be a multiple of 20', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts b/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts index e67903d2..80ecfd6c 100644 --- a/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts +++ b/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts @@ -1,32 +1,62 @@ 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'); +import { + createArgsEqualityCheckTerms, + decodeArgsEqualityCheckTerms, +} from '../../src/caveats/argsEqualityCheck'; + +describe('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); + }); }); - 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); + describe('decodeArgsEqualityCheckTerms', () => { + it('decodes arbitrary args hex', () => { + const args = '0x1234abcd' as `0x${string}`; + expect( + decodeArgsEqualityCheckTerms(createArgsEqualityCheckTerms({ args })), + ).toStrictEqual({ + args, + }); + }); + + it('decodes empty args', () => { + expect( + decodeArgsEqualityCheckTerms( + createArgsEqualityCheckTerms({ args: '0x' }), + ), + ).toStrictEqual({ args: '0x' }); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const args = '0xdeadbeef' as `0x${string}`; + const bytes = createArgsEqualityCheckTerms({ args }, { out: 'bytes' }); + expect(decodeArgsEqualityCheckTerms(bytes)).toStrictEqual({ args }); + }); }); }); diff --git a/packages/delegation-core/test/caveats/blockNumber.test.ts b/packages/delegation-core/test/caveats/blockNumber.test.ts index 0ac8a05d..340b3499 100644 --- a/packages/delegation-core/test/caveats/blockNumber.test.ts +++ b/packages/delegation-core/test/caveats/blockNumber.test.ts @@ -1,54 +1,92 @@ import { describe, it, expect } from 'vitest'; -import { createBlockNumberTerms } from '../../src/caveats/blockNumber'; +import { + createBlockNumberTerms, + decodeBlockNumberTerms, +} from '../../src/caveats/blockNumber'; -describe('createBlockNumberTerms', () => { - it('creates valid terms for thresholds', () => { - const result = createBlockNumberTerms({ - afterThreshold: 5n, - beforeThreshold: 10n, +describe('BlockNumber', () => { + describe('createBlockNumberTerms', () => { + it('creates valid terms for thresholds', () => { + const result = createBlockNumberTerms({ + afterThreshold: 5n, + beforeThreshold: 10n, + }); + + expect(result).toStrictEqual( + '0x000000000000000000000000000000050000000000000000000000000000000a', + ); }); - expect(result).toStrictEqual( - '0x000000000000000000000000000000050000000000000000000000000000000a', - ); - }); + it('throws when afterThreshold is negative', () => { + expect(() => + createBlockNumberTerms({ afterThreshold: -1n, beforeThreshold: 10n }), + ).toThrow('Invalid thresholds: block numbers must be non-negative'); + }); - 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 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 }), + ).toThrow( + 'Invalid thresholds: At least one of afterThreshold or beforeThreshold must be specified', + ); + }); - 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createBlockNumberTerms( - { afterThreshold: 1n, beforeThreshold: 2n }, - { out: 'bytes' }, - ); + describe('decodeBlockNumberTerms', () => { + it('decodes after and before thresholds', () => { + const original = { afterThreshold: 5n, beforeThreshold: 10n }; + expect( + decodeBlockNumberTerms(createBlockNumberTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes when only after is zero (open-ended upper bound encoding)', () => { + const original = { afterThreshold: 0n, beforeThreshold: 100n }; + expect( + decodeBlockNumberTerms(createBlockNumberTerms(original)), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createBlockNumberTerms( + { afterThreshold: 1n, beforeThreshold: 2n }, + { out: 'bytes' }, + ); + expect(decodeBlockNumberTerms(bytes)).toStrictEqual({ + afterThreshold: 1n, + beforeThreshold: 2n, + }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('throws when encoded terms are not exactly 32 bytes', () => { + expect(() => decodeBlockNumberTerms(`0x${'00'.repeat(31)}`)).toThrow( + 'Invalid BlockNumber terms: must be exactly 32 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/decoders.test.ts b/packages/delegation-core/test/caveats/decoders.test.ts new file mode 100644 index 00000000..9fc7738c --- /dev/null +++ b/packages/delegation-core/test/caveats/decoders.test.ts @@ -0,0 +1,545 @@ +import { describe, it, expect } from 'vitest'; + +import { + createValueLteTerms, + decodeValueLteTerms, + createTimestampTerms, + decodeTimestampTerms, + createBlockNumberTerms, + decodeBlockNumberTerms, + createLimitedCallsTerms, + decodeLimitedCallsTerms, + createIdTerms, + decodeIdTerms, + createNonceTerms, + decodeNonceTerms, + createAllowedMethodsTerms, + decodeAllowedMethodsTerms, + createAllowedTargetsTerms, + decodeAllowedTargetsTerms, + createRedeemerTerms, + decodeRedeemerTerms, + createAllowedCalldataTerms, + decodeAllowedCalldataTerms, + createArgsEqualityCheckTerms, + decodeArgsEqualityCheckTerms, + createExactCalldataTerms, + decodeExactCalldataTerms, + createExactExecutionTerms, + decodeExactExecutionTerms, + createExactCalldataBatchTerms, + decodeExactCalldataBatchTerms, + createExactExecutionBatchTerms, + decodeExactExecutionBatchTerms, + createNativeTokenTransferAmountTerms, + decodeNativeTokenTransferAmountTerms, + createNativeTokenPaymentTerms, + decodeNativeTokenPaymentTerms, + createNativeBalanceChangeTerms, + decodeNativeBalanceChangeTerms, + createNativeTokenPeriodTransferTerms, + decodeNativeTokenPeriodTransferTerms, + createNativeTokenStreamingTerms, + decodeNativeTokenStreamingTerms, + createERC20TransferAmountTerms, + decodeERC20TransferAmountTerms, + createERC20BalanceChangeTerms, + decodeERC20BalanceChangeTerms, + createERC20TokenPeriodTransferTerms, + decodeERC20TokenPeriodTransferTerms, + createERC20StreamingTerms, + decodeERC20StreamingTerms, + createERC721TransferTerms, + decodeERC721TransferTerms, + createERC721BalanceChangeTerms, + decodeERC721BalanceChangeTerms, + createERC1155BalanceChangeTerms, + decodeERC1155BalanceChangeTerms, + createDeployedTerms, + decodeDeployedTerms, + createOwnershipTransferTerms, + decodeOwnershipTransferTerms, + createMultiTokenPeriodTerms, + decodeMultiTokenPeriodTerms, + createSpecificActionERC20TransferBatchTerms, + decodeSpecificActionERC20TransferBatchTerms, +} from '../../src/caveats'; + +describe('Terms Decoders', () => { + describe('decodeValueLteTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { maxValue: 1000000000000000000n }; + const encoded = createValueLteTerms(original); + const decoded = decodeValueLteTerms(encoded); + expect(decoded).toEqual(original); + }); + + it('decodes zero value', () => { + const original = { maxValue: 0n }; + const encoded = createValueLteTerms(original); + const decoded = decodeValueLteTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeTimestampTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + afterThreshold: 1640995200, + beforeThreshold: 1672531200, + }; + const encoded = createTimestampTerms(original); + const decoded = decodeTimestampTerms(encoded); + expect(decoded).toEqual(original); + }); + + it('decodes zero thresholds', () => { + const original = { + afterThreshold: 0, + beforeThreshold: 0, + }; + const encoded = createTimestampTerms(original); + const decoded = decodeTimestampTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeBlockNumberTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { afterThreshold: 100n, beforeThreshold: 200n }; + const encoded = createBlockNumberTerms(original); + const decoded = decodeBlockNumberTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeLimitedCallsTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { limit: 5 }; + const encoded = createLimitedCallsTerms(original); + const decoded = decodeLimitedCallsTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeIdTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { id: 12345n }; + const encoded = createIdTerms(original); + const decoded = decodeIdTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeNonceTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { nonce: '0x1234' as `0x${string}` }; + const encoded = createNonceTerms(original); + const decoded = decodeNonceTerms(encoded); + expect(decoded.nonce).toEqual(encoded); + }); + }); + + describe('decodeAllowedMethodsTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + selectors: ['0x70a08231', '0xa9059cbb'] as `0x${string}`[], + }; + const encoded = createAllowedMethodsTerms(original); + const decoded = decodeAllowedMethodsTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeAllowedTargetsTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + targets: [ + '0x1234567890123456789012345678901234567890', + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + ] as `0x${string}`[], + }; + const encoded = createAllowedTargetsTerms(original); + const decoded = decodeAllowedTargetsTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeRedeemerTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + redeemers: [ + '0x1234567890123456789012345678901234567890', + ] as `0x${string}`[], + }; + const encoded = createRedeemerTerms(original); + const decoded = decodeRedeemerTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeAllowedCalldataTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { startIndex: 4, value: '0x1234' as `0x${string}` }; + const encoded = createAllowedCalldataTerms(original); + const decoded = decodeAllowedCalldataTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeArgsEqualityCheckTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { args: '0x1234567890abcdef' as `0x${string}` }; + const encoded = createArgsEqualityCheckTerms(original); + const decoded = decodeArgsEqualityCheckTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeExactCalldataTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + calldata: '0x70a08231000000000000000000000000' as `0x${string}`, + }; + const encoded = createExactCalldataTerms(original); + const decoded = decodeExactCalldataTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeExactExecutionTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + execution: { + target: '0x1234567890123456789012345678901234567890' as `0x${string}`, + value: 1000n, + callData: '0x70a08231' as `0x${string}`, + }, + }; + const encoded = createExactExecutionTerms(original); + const decoded = decodeExactExecutionTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeExactCalldataBatchTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + executions: [ + { + target: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + value: 1000n, + callData: '0x70a08231' as `0x${string}`, + }, + { + target: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + value: 2000n, + callData: '0xa9059cbb' as `0x${string}`, + }, + ], + }; + const encoded = createExactCalldataBatchTerms(original); + const decoded = decodeExactCalldataBatchTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeExactExecutionBatchTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + executions: [ + { + target: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + value: 1000n, + callData: '0x70a08231' as `0x${string}`, + }, + ], + }; + const encoded = createExactExecutionBatchTerms(original); + const decoded = decodeExactExecutionBatchTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeNativeTokenTransferAmountTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { maxAmount: 1000000000000000000n }; + const encoded = createNativeTokenTransferAmountTerms(original); + const decoded = decodeNativeTokenTransferAmountTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeNativeTokenPaymentTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + recipient: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + amount: 1000000000000000000n, + }; + const encoded = createNativeTokenPaymentTerms(original); + const decoded = decodeNativeTokenPaymentTerms(encoded); + expect((decoded.recipient as string).toLowerCase()).toEqual( + original.recipient.toLowerCase(), + ); + expect(decoded.amount).toEqual(original.amount); + }); + }); + + describe('decodeNativeBalanceChangeTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + recipient: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + balance: 1000000000000000000n, + changeType: 0, + }; + const encoded = createNativeBalanceChangeTerms(original); + const decoded = decodeNativeBalanceChangeTerms(encoded); + expect((decoded.recipient as string).toLowerCase()).toEqual( + original.recipient.toLowerCase(), + ); + expect(decoded.balance).toEqual(original.balance); + expect(decoded.changeType).toEqual(original.changeType); + }); + }); + + describe('decodeNativeTokenPeriodTransferTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + periodAmount: 1000000000000000000n, + periodDuration: 86400, + startDate: 1640995200, + }; + const encoded = createNativeTokenPeriodTransferTerms(original); + const decoded = decodeNativeTokenPeriodTransferTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeNativeTokenStreamingTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + initialAmount: 1000000000000000000n, + maxAmount: 10000000000000000000n, + amountPerSecond: 1000000000000000n, + startTime: 1640995200, + }; + const encoded = createNativeTokenStreamingTerms(original); + const decoded = decodeNativeTokenStreamingTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeERC20TransferAmountTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + maxAmount: 1000000000000000000n, + }; + const encoded = createERC20TransferAmountTerms(original); + const decoded = decodeERC20TransferAmountTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeERC20BalanceChangeTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + balance: 1000000000000000000n, + changeType: 0, + }; + const encoded = createERC20BalanceChangeTerms(original); + const decoded = decodeERC20BalanceChangeTerms(encoded); + expect((decoded.tokenAddress as string).toLowerCase()).toEqual( + original.tokenAddress.toLowerCase(), + ); + expect((decoded.recipient as string).toLowerCase()).toEqual( + original.recipient.toLowerCase(), + ); + expect(decoded.balance).toEqual(original.balance); + expect(decoded.changeType).toEqual(original.changeType); + }); + }); + + describe('decodeERC20TokenPeriodTransferTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + periodAmount: 1000000000000000000n, + periodDuration: 86400, + startDate: 1640995200, + }; + const encoded = createERC20TokenPeriodTransferTerms(original); + const decoded = decodeERC20TokenPeriodTransferTerms(encoded); + expect(decoded).toEqual(original); + }); + + it('throws when encoded terms are not exactly 116 bytes', () => { + expect(() => + decodeERC20TokenPeriodTransferTerms(`0x${'00'.repeat(115)}`), + ).toThrow( + 'Invalid ERC20TokenPeriodTransfer terms: must be exactly 116 bytes', + ); + }); + }); + + describe('decodeERC20StreamingTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + initialAmount: 1000000000000000000n, + maxAmount: 10000000000000000000n, + amountPerSecond: 1000000000000000n, + startTime: 1640995200, + }; + const encoded = createERC20StreamingTerms(original); + const decoded = decodeERC20StreamingTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeERC721TransferTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + tokenId: 123n, + }; + const encoded = createERC721TransferTerms(original); + const decoded = decodeERC721TransferTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeERC721BalanceChangeTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + amount: 5n, + changeType: 0, + }; + const encoded = createERC721BalanceChangeTerms(original); + const decoded = decodeERC721BalanceChangeTerms(encoded); + expect((decoded.tokenAddress as string).toLowerCase()).toEqual( + original.tokenAddress.toLowerCase(), + ); + expect((decoded.recipient as string).toLowerCase()).toEqual( + original.recipient.toLowerCase(), + ); + expect(decoded.amount).toEqual(original.amount); + expect(decoded.changeType).toEqual(original.changeType); + }); + }); + + describe('decodeERC1155BalanceChangeTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + tokenId: 123n, + balance: 1000n, + changeType: 0, + }; + const encoded = createERC1155BalanceChangeTerms(original); + const decoded = decodeERC1155BalanceChangeTerms(encoded); + expect((decoded.tokenAddress as string).toLowerCase()).toEqual( + original.tokenAddress.toLowerCase(), + ); + expect((decoded.recipient as string).toLowerCase()).toEqual( + original.recipient.toLowerCase(), + ); + expect(decoded.tokenId).toEqual(original.tokenId); + expect(decoded.balance).toEqual(original.balance); + expect(decoded.changeType).toEqual(original.changeType); + }); + }); + + describe('decodeDeployedTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + contractAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + salt: '0x1234' as `0x${string}`, + bytecode: '0x608060405234801561001057600080fd5b50' as `0x${string}`, + }; + const encoded = createDeployedTerms(original); + const decoded = decodeDeployedTerms(encoded); + expect(decoded.contractAddress).toEqual(original.contractAddress); + expect((decoded.salt as string).toLowerCase()).toEqual( + '0x0000000000000000000000000000000000000000000000000000000000001234', + ); + expect(decoded.bytecode).toEqual(original.bytecode); + }); + }); + + describe('decodeOwnershipTransferTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + contractAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + }; + const encoded = createOwnershipTransferTerms(original); + const decoded = decodeOwnershipTransferTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeMultiTokenPeriodTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenConfigs: [ + { + token: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + periodAmount: 1000000000000000000n, + periodDuration: 86400, + startDate: 1640995200, + }, + { + token: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + periodAmount: 2000000000000000000n, + periodDuration: 172800, + startDate: 1640995200, + }, + ], + }; + const encoded = createMultiTokenPeriodTerms(original); + const decoded = decodeMultiTokenPeriodTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeSpecificActionERC20TransferBatchTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + amount: 1000000000000000000n, + target: '0x9876543210987654321098765432109876543210' as `0x${string}`, + calldata: '0x70a08231' as `0x${string}`, + }; + const encoded = createSpecificActionERC20TransferBatchTerms(original); + const decoded = decodeSpecificActionERC20TransferBatchTerms(encoded); + expect(decoded).toEqual(original); + }); + }); +}); diff --git a/packages/delegation-core/test/caveats/deployed.test.ts b/packages/delegation-core/test/caveats/deployed.test.ts index fd59fde8..85ee74ec 100644 --- a/packages/delegation-core/test/caveats/deployed.test.ts +++ b/packages/delegation-core/test/caveats/deployed.test.ts @@ -1,59 +1,94 @@ import { describe, it, expect } from 'vitest'; -import { createDeployedTerms } from '../../src/caveats/deployed'; +import { + createDeployedTerms, + decodeDeployedTerms, +} from '../../src/caveats/deployed'; -describe('createDeployedTerms', () => { - const contractAddress = '0x00000000000000000000000000000000000000aa'; - const salt = '0x01'; - const bytecode = '0x1234'; +describe('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 }); + it('creates valid terms for deployment parameters', () => { + const result = createDeployedTerms({ contractAddress, salt, bytecode }); - expect(result).toStrictEqual( - '0x00000000000000000000000000000000000000aa' + - '0000000000000000000000000000000000000000000000000000000000000001' + - '1234', - ); - }); + 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 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 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createDeployedTerms( - { contractAddress, salt, bytecode }, - { out: 'bytes' }, - ); + describe('decodeDeployedTerms', () => { + const contractAddress = + '0x00000000000000000000000000000000000000aa' as `0x${string}`; + const salt = '0x01' as `0x${string}`; + const bytecode = '0x1234' as `0x${string}`; + + it('decodes deployment parameters with padded salt', () => { + const original = { contractAddress, salt, bytecode }; + const decoded = decodeDeployedTerms(createDeployedTerms(original)); + expect(decoded.contractAddress).toBe(contractAddress); + expect((decoded.salt as string).toLowerCase()).toBe( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + expect(decoded.bytecode).toBe(bytecode); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const original = { contractAddress, salt, bytecode }; + const bytes = createDeployedTerms(original, { out: 'bytes' }); + const decoded = decodeDeployedTerms(bytes); + expect(decoded.bytecode).toBe(bytecode); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(20 + 32 + 2); + it('throws when encoded terms are shorter than 52 bytes', () => { + expect(() => decodeDeployedTerms(`0x${'00'.repeat(51)}`)).toThrow( + 'Invalid Deployed terms: must be at least 52 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts index f1868b86..d5edc4c9 100644 --- a/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts @@ -1,67 +1,120 @@ import { describe, it, expect } from 'vitest'; -import { createERC1155BalanceChangeTerms } from '../../src/caveats/erc1155BalanceChange'; +import { + createERC1155BalanceChangeTerms, + decodeERC1155BalanceChangeTerms, +} from '../../src/caveats/erc1155BalanceChange'; import { BalanceChangeType } from '../../src/caveats/types'; -describe('createERC1155BalanceChangeTerms', () => { - const tokenAddress = '0x00000000000000000000000000000000000000cc'; - const recipient = '0x00000000000000000000000000000000000000dd'; +describe('ERC1155BalanceChange', () => { + 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, + 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', + ); }); - 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 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); + }); }); - it('throws for invalid balance', () => { - expect(() => - createERC1155BalanceChangeTerms({ + describe('decodeERC1155BalanceChangeTerms', () => { + const tokenAddress = + '0x00000000000000000000000000000000000000cc' as `0x${string}`; + const recipient = + '0x00000000000000000000000000000000000000dd' as `0x${string}`; + + it('decodes full terms', () => { + const original = { tokenAddress, recipient, - tokenId: 1n, - balance: 0n, - changeType: BalanceChangeType.Increase, - }), - ).toThrow('Invalid balance: must be a positive number'); - }); + tokenId: 7n, + balance: 3n, + changeType: BalanceChangeType.Decrease, + }; + const decoded = decodeERC1155BalanceChangeTerms( + createERC1155BalanceChangeTerms(original), + ); + expect(decoded.tokenId).toBe(7n); + expect(decoded.balance).toBe(3n); + expect(decoded.changeType).toBe(BalanceChangeType.Decrease); + expect((decoded.tokenAddress as string).toLowerCase()).toBe( + tokenAddress.toLowerCase(), + ); + }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createERC1155BalanceChangeTerms( - { + it('accepts Uint8Array terms from the encoder', () => { + const original = { tokenAddress, recipient, tokenId: 1n, balance: 1n, changeType: BalanceChangeType.Increase, - }, - { out: 'bytes' }, - ); + }; + const bytes = createERC1155BalanceChangeTerms(original, { out: 'bytes' }); + const decoded = decodeERC1155BalanceChangeTerms(bytes); + expect(decoded.tokenId).toBe(1n); + expect(decoded.balance).toBe(1n); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(105); + it('throws when encoded terms are not exactly 105 bytes', () => { + expect(() => + decodeERC1155BalanceChangeTerms(`0x${'00'.repeat(104)}`), + ).toThrow( + 'Invalid ERC1155BalanceChange terms: must be exactly 105 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts index 88115deb..88331303 100644 --- a/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts @@ -1,62 +1,126 @@ import { describe, it, expect } from 'vitest'; -import { createERC20BalanceChangeTerms } from '../../src/caveats/erc20BalanceChange'; +import { + createERC20BalanceChangeTerms, + decodeERC20BalanceChangeTerms, +} from '../../src/caveats/erc20BalanceChange'; import { BalanceChangeType } from '../../src/caveats/types'; -describe('createERC20BalanceChangeTerms', () => { - const tokenAddress = '0x00000000000000000000000000000000000000dd'; - const recipient = '0x00000000000000000000000000000000000000ee'; +describe('ERC20BalanceChange', () => { + 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, + 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'); }); - expect(result).toStrictEqual( - '0x01' + - '00000000000000000000000000000000000000dd' + - '00000000000000000000000000000000000000ee' + - '0000000000000000000000000000000000000000000000000000000000000005', - ); + 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); + }); }); - it('throws for invalid token address', () => { - expect(() => - createERC20BalanceChangeTerms({ - tokenAddress: '0x1234', + describe('decodeERC20BalanceChangeTerms', () => { + const tokenAddress = + '0x00000000000000000000000000000000000000dd' as `0x${string}`; + const recipient = + '0x00000000000000000000000000000000000000ee' as `0x${string}`; + + it('decodes decrease balance change', () => { + const original = { + tokenAddress, recipient, - balance: 1n, - changeType: BalanceChangeType.Increase, - }), - ).toThrow('Invalid tokenAddress: must be a valid address'); - }); + balance: 5n, + changeType: BalanceChangeType.Decrease, + }; + const decoded = decodeERC20BalanceChangeTerms( + createERC20BalanceChangeTerms(original), + ); + expect(decoded.changeType).toBe(original.changeType); + expect((decoded.tokenAddress as string).toLowerCase()).toBe( + tokenAddress.toLowerCase(), + ); + expect((decoded.recipient as string).toLowerCase()).toBe( + recipient.toLowerCase(), + ); + expect(decoded.balance).toBe(original.balance); + }); - it('throws for invalid balance', () => { - expect(() => - createERC20BalanceChangeTerms({ + it('decodes increase balance change', () => { + const original = { tokenAddress, recipient, - balance: 0n, + balance: 1n, changeType: BalanceChangeType.Increase, - }), - ).toThrow('Invalid balance: must be a positive number'); - }); + }; + const decoded = decodeERC20BalanceChangeTerms( + createERC20BalanceChangeTerms(original), + ); + expect(decoded.changeType).toBe(BalanceChangeType.Increase); + expect(decoded.balance).toBe(1n); + }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createERC20BalanceChangeTerms( - { + it('accepts Uint8Array terms from the encoder', () => { + const original = { tokenAddress, recipient, balance: 1n, changeType: BalanceChangeType.Increase, - }, - { out: 'bytes' }, - ); + }; + const bytes = createERC20BalanceChangeTerms(original, { out: 'bytes' }); + const decoded = decodeERC20BalanceChangeTerms(bytes); + expect(decoded.balance).toBe(1n); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(73); + it('throws when encoded terms are not exactly 73 bytes', () => { + expect(() => + decodeERC20BalanceChangeTerms(`0x${'00'.repeat(72)}`), + ).toThrow('Invalid ERC20BalanceChange terms: must be exactly 73 bytes'); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc20Streaming.test.ts b/packages/delegation-core/test/caveats/erc20Streaming.test.ts index 3c674638..36cc91ad 100644 --- a/packages/delegation-core/test/caveats/erc20Streaming.test.ts +++ b/packages/delegation-core/test/caveats/erc20Streaming.test.ts @@ -1,842 +1,557 @@ import { describe, it, expect } from 'vitest'; -import { createERC20StreamingTerms } from '../../src/caveats/erc20Streaming'; +import { + createERC20StreamingTerms, + decodeERC20StreamingTerms, +} from '../../src/caveats/erc20Streaming'; -describe('createERC20StreamingTerms', () => { - const EXPECTED_BYTE_LENGTH = 148; +describe('ERC20Streaming', () => { + describe('createERC20StreamingTerms', () => { + const EXPECTED_BYTE_LENGTH = 148; - const validTokenAddress = '0x1234567890123456789012345678901234567890'; + const validTokenAddress = '0x1234567890123456789012345678901234567890'; - it('creates valid terms for standard streaming parameters', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 1000000000000000000n; // 1 token (18 decimals) - const maxAmount = 10000000000000000000n; // 10 tokens - const amountPerSecond = 500000000000000000n; // 0.5 token per second - const startTime = 1640995200; // 2022-01-01 00:00:00 UTC - - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('creates valid terms for zero initial amount', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 0n; - const maxAmount = 5000000000000000000n; // 5 tokens - const amountPerSecond = 1000000000000000000n; // 1 token per second - const startTime = 1672531200; // 2023-01-01 00:00:00 UTC - - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000063b0cd00', - ); - }); - - it('creates valid terms for equal initial and max amounts', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 2000000000000000000n; // 2 tokens - const maxAmount = 2000000000000000000n; // 2 tokens - const amountPerSecond = 100000000000000000n; // 0.1 token per second - const startTime = 1640995200; - - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('creates valid terms for small values', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 1n; - const maxAmount = 1000n; - const amountPerSecond = 1n; - const startTime = 1; - - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x1234567890123456789012345678901234567890000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', - ); - }); - - it('creates valid terms for large values', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 100000000000000000000n; // 100 tokens - const maxAmount = 1000000000000000000000n; // 1000 tokens - const amountPerSecond = 10000000000000000000n; // 10 tokens per second - const startTime = 2000000000; // Far future - - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000077359400', - ); - }); - - it('creates valid terms for maximum allowed timestamp', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; - const amountPerSecond = 1000000000000000000n; - const startTime = 253402300799; // January 1, 10000 CE - - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000003afff4417f', - ); - }); - - it('creates valid terms for maximum safe bigint values', () => { - const maxUint256 = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; - const tokenAddress = validTokenAddress; - const initialAmount = maxUint256; - const maxAmount = maxUint256; - const amountPerSecond = maxUint256; - const startTime = 1640995200; - - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x1234567890123456789012345678901234567890ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('creates valid terms for different token addresses', () => { - const tokenAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; - - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('throws an error for invalid token address', () => { - const tokenAddress = 'invalid-address' as any; - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + it('creates valid terms for standard streaming parameters', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 1000000000000000000n; // 1 token (18 decimals) + const maxAmount = 10000000000000000000n; // 10 tokens + const amountPerSecond = 500000000000000000n; // 0.5 token per second + const startTime = 1640995200; // 2022-01-01 00:00:00 UTC - expect(() => - createERC20StreamingTerms({ + const result = createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid tokenAddress: must be a valid address'); - }); - - it('throws an error for empty token address', () => { - const tokenAddress = '' as any; - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + }); - expect(() => - createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }), - ).toThrow('Invalid tokenAddress: must be a valid address'); - }); + expect(result).toStrictEqual( + '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); - it('throws an error for token address without 0x prefix', () => { - const tokenAddress = '1234567890123456789012345678901234567890' as any; - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + it('creates valid terms for zero initial amount', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 0n; + const maxAmount = 5000000000000000000n; // 5 tokens + const amountPerSecond = 1000000000000000000n; // 1 token per second + const startTime = 1672531200; // 2023-01-01 00:00:00 UTC - expect(() => - createERC20StreamingTerms({ + const result = createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid tokenAddress: must be a valid address'); - }); - - it('throws an error for negative initial amount', () => { - const tokenAddress = validTokenAddress; - const initialAmount = -1n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + }); - expect(() => - createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }), - ).toThrow('Invalid initialAmount: must be greater than zero'); - }); + expect(result).toStrictEqual( + '0x123456789012345678901234567890123456789000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000063b0cd00', + ); + }); - it('throws an error for zero max amount', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 0n; - const maxAmount = 0n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + it('creates valid terms for equal initial and max amounts', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 2000000000000000000n; // 2 tokens + const maxAmount = 2000000000000000000n; // 2 tokens + const amountPerSecond = 100000000000000000n; // 0.1 token per second + const startTime = 1640995200; - expect(() => - createERC20StreamingTerms({ + const result = createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid maxAmount: must be a positive number'); - }); - - it('throws an error for negative max amount', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 0n; - const maxAmount = -1n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + }); - expect(() => - createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }), - ).toThrow('Invalid maxAmount: must be a positive number'); - }); + expect(result).toStrictEqual( + '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); - it('throws an error when max amount is less than initial amount', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 1000000000000000000n; // 1 token - const maxAmount = 500000000000000000n; // 0.5 token - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + it('creates valid terms for small values', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 1n; + const maxAmount = 1000n; + const amountPerSecond = 1n; + const startTime = 1; - expect(() => - createERC20StreamingTerms({ + const result = createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid maxAmount: must be greater than initialAmount'); - }); - - it('throws an error for zero amount per second', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 0n; - const startTime = 1640995200; + }); - expect(() => - createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }), - ).toThrow('Invalid amountPerSecond: must be a positive number'); - }); + expect(result).toStrictEqual( + '0x1234567890123456789012345678901234567890000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', + ); + }); - it('throws an error for negative amount per second', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = -1n; - const startTime = 1640995200; + it('creates valid terms for large values', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 100000000000000000000n; // 100 tokens + const maxAmount = 1000000000000000000000n; // 1000 tokens + const amountPerSecond = 10000000000000000000n; // 10 tokens per second + const startTime = 2000000000; // Far future - expect(() => - createERC20StreamingTerms({ + const result = createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid amountPerSecond: must be a positive number'); - }); + }); - it('throws an error for zero start time', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 0; + expect(result).toStrictEqual( + '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000077359400', + ); + }); - expect(() => - createERC20StreamingTerms({ + it('creates valid terms for maximum allowed timestamp', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 1000000000000000000n; + const startTime = 253402300799; // January 1, 10000 CE + + const result = createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid startTime: must be a positive number'); - }); + }); - it('throws an error for negative start time', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = -1; + expect(result).toStrictEqual( + '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000003afff4417f', + ); + }); - expect(() => - createERC20StreamingTerms({ + it('creates valid terms for maximum safe bigint values', () => { + const maxUint256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + const tokenAddress = validTokenAddress; + const initialAmount = maxUint256; + const maxAmount = maxUint256; + const amountPerSecond = maxUint256; + const startTime = 1640995200; + + const result = createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid startTime: must be a positive number'); - }); + }); + + expect(result).toStrictEqual( + '0x1234567890123456789012345678901234567890ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); - it('throws an error for start time exceeding upper bound', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 253402300800; // One second past January 1, 10000 CE + it('creates valid terms for different token addresses', () => { + const tokenAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - expect(() => - createERC20StreamingTerms({ + const result = createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid startTime: must be less than or equal to 253402300799'); - }); - - it('throws an error for undefined tokenAddress', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: undefined as any, - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow('Invalid tokenAddress: must be a valid address'); - }); - - it('throws an error for null tokenAddress', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: null as any, - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow('Invalid tokenAddress: must be a valid address'); - }); - - it('throws an error for undefined initialAmount', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: undefined as any, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow(); - }); - - it('throws an error for null initialAmount', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: null as any, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow(); - }); - - it('throws an error for undefined maxAmount', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: 0n, - maxAmount: undefined as any, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow(); - }); - - it('throws an error for null maxAmount', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: 0n, - maxAmount: null as any, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow(); - }); + }); - it('throws an error for undefined amountPerSecond', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: undefined as any, - startTime: 1640995200, - }), - ).toThrow(); - }); + expect(result).toStrictEqual( + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); - it('throws an error for null amountPerSecond', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: null as any, - startTime: 1640995200, - }), - ).toThrow(); - }); + it('throws an error for invalid token address', () => { + const tokenAddress = 'invalid-address' as any; + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - it('throws an error for undefined startTime', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: undefined as any, - }), - ).toThrow(); - }); + expect(() => + createERC20StreamingTerms({ + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); + }); - it('throws an error for null startTime', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: null as any, - }), - ).toThrow(); - }); + it('throws an error for empty token address', () => { + const tokenAddress = '' as any; + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - it('throws an error for Infinity startTime', () => { - expect(() => - createERC20StreamingTerms({ - tokenAddress: validTokenAddress, - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: Infinity, - }), - ).toThrow(); - }); + expect(() => + createERC20StreamingTerms({ + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); + }); - it('handles edge case with very large initial amount and small max amount difference', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 999999999999999999n; - const maxAmount = 1000000000000000000n; // Just 1 wei more - const amountPerSecond = 1n; - const startTime = 1640995200; + it('throws an error for token address without 0x prefix', () => { + const tokenAddress = '1234567890123456789012345678901234567890' as any; + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, + expect(() => + createERC20StreamingTerms({ + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); }); - expect(result).toStrictEqual( - '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000000de0b6b3a763ffff0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('handles streaming with minimum viable parameters', () => { - const tokenAddress = validTokenAddress; - const initialAmount = 1n; - const maxAmount = 2n; - const amountPerSecond = 1n; - const startTime = 1; + it('throws an error for negative initial amount', () => { + const tokenAddress = validTokenAddress; + const initialAmount = -1n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - const result = createERC20StreamingTerms({ - tokenAddress, - initialAmount, - maxAmount, - amountPerSecond, - startTime, + expect(() => + createERC20StreamingTerms({ + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }), + ).toThrow('Invalid initialAmount: must be greater than zero'); }); - expect(result).toStrictEqual( - '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', - ); - }); - - // Tests for bytes return type - describe('bytes return type', () => { - it('returns Uint8Array when bytes encoding is specified', () => { + it('throws an error for zero max amount', () => { const tokenAddress = validTokenAddress; - const initialAmount = 1000000000000000000n; // 1 token (18 decimals) - const maxAmount = 10000000000000000000n; // 10 tokens - const amountPerSecond = 500000000000000000n; // 0.5 token per second - const startTime = 1640995200; // 2022-01-01 00:00:00 UTC - const result = createERC20StreamingTerms( - { + const initialAmount = 0n; + const maxAmount = 0n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; + + expect(() => + createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); // 20 bytes for token address + 32 bytes for each of the 4 parameters + }), + ).toThrow('Invalid maxAmount: must be a positive number'); }); - it('returns Uint8Array for zero initial amount with bytes encoding', () => { + it('throws an error for negative max amount', () => { const tokenAddress = validTokenAddress; const initialAmount = 0n; - const maxAmount = 5000000000000000000n; // 5 tokens - const amountPerSecond = 1000000000000000000n; // 1 token per second - const startTime = 1672531200; // 2023-01-01 00:00:00 UTC - const result = createERC20StreamingTerms( - { + const maxAmount = -1n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; + + expect(() => + createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); + }), + ).toThrow('Invalid maxAmount: must be a positive number'); + }); - // this is the validTokenAddress represented as bytes - const tokenAddressBytes = [ - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, - 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, - ]; - // initial amount is 32 0x00 bytes - const initialAmountBytes = new Array(32).fill(0); - - // 5000000000000000000n == 0x4563918244f40000 (padded to 32 bytes) - const maxAmountBytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x45, 0x63, 0x91, 0x82, 0x44, 0xf4, 0x00, 0x00, - ]; - - // 1000000000000000000n == 0x0de0b6b3a7640000 (padded to 32 bytes) - const amountPerSecondBytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x0d, 0xe0, 0xb6, 0xb3, 0xa7, 0x64, 0x00, 0x00, - ]; - - // 1672531200 == 0x63b0cd00 - const startTimeBytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x63, 0xb0, 0xcd, 0x00, - ]; - - const expectedBytes = new Uint8Array([ - ...tokenAddressBytes, - ...initialAmountBytes, - ...maxAmountBytes, - ...amountPerSecondBytes, - ...startTimeBytes, - ]); - - expect(result).toEqual(expectedBytes); - }); - - it('returns Uint8Array for equal initial and max amounts with bytes encoding', () => { + it('throws an error when max amount is less than initial amount', () => { const tokenAddress = validTokenAddress; - const initialAmount = 2000000000000000000n; // 2 tokens - const maxAmount = 2000000000000000000n; // 2 tokens - const amountPerSecond = 100000000000000000n; // 0.1 token per second + const initialAmount = 1000000000000000000n; // 1 token + const maxAmount = 500000000000000000n; // 0.5 token + const amountPerSecond = 100000000000000000n; const startTime = 1640995200; - const result = createERC20StreamingTerms( - { + + expect(() => + createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }), + ).toThrow('Invalid maxAmount: must be greater than initialAmount'); }); - it('returns Uint8Array for small values with bytes encoding', () => { + it('throws an error for zero amount per second', () => { const tokenAddress = validTokenAddress; - const initialAmount = 1n; - const maxAmount = 1000n; - const amountPerSecond = 1n; - const startTime = 1; - const result = createERC20StreamingTerms( - { + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = 0n; + const startTime = 1640995200; + + expect(() => + createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); + }), + ).toThrow('Invalid amountPerSecond: must be a positive number'); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // this is the validTokenAddress represented as bytes - const tokenAddressBytes = [ - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, - 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, - ]; - - // 1n is padded to 32 bytes - const initialAmountBytes = new Array(32).fill(0); - initialAmountBytes[31] = 0x01; - - // 1000n == 0x03e8 (padded to 32 bytes) - const maxAmountBytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, - ]; - - // 1n is padded to 32 bytes - const amountPerSecondBytes = new Array(32).fill(0); - amountPerSecondBytes[31] = 0x01; - - // 1 is padded to 32 bytes - const startTimeBytes = new Array(32).fill(0); - startTimeBytes[31] = 0x01; - - const expectedBytes = new Uint8Array([ - ...tokenAddressBytes, - ...initialAmountBytes, - ...maxAmountBytes, - ...amountPerSecondBytes, - ...startTimeBytes, - ]); - expect(result).toStrictEqual(expectedBytes); - }); - - it('returns Uint8Array for large values with bytes encoding', () => { + it('throws an error for negative amount per second', () => { const tokenAddress = validTokenAddress; - const initialAmount = 100000000000000000000n; // 100 tokens - const maxAmount = 1000000000000000000000n; // 1000 tokens - const amountPerSecond = 10000000000000000000n; // 10 tokens per second - const startTime = 2000000000; // Far future - const result = createERC20StreamingTerms( - { + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = -1n; + const startTime = 1640995200; + + expect(() => + createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }), + ).toThrow('Invalid amountPerSecond: must be a positive number'); }); - it('returns Uint8Array for different token addresses with bytes encoding', () => { - const tokenAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; + it('throws an error for zero start time', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; - const result = createERC20StreamingTerms( - { + const startTime = 0; + + expect(() => + createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Check that the token address is correctly encoded - const addressBytes = Array.from(result.slice(0, 20)); - const expectedAddressBytes = [ - 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, - 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, - ]; - expect(addressBytes).toEqual(expectedAddressBytes); + }), + ).toThrow('Invalid startTime: must be a positive number'); }); - it('returns Uint8Array for maximum allowed timestamp with bytes encoding', () => { + it('throws an error for negative start time', () => { const tokenAddress = validTokenAddress; - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; - const amountPerSecond = 1000000000000000000n; - const startTime = 253402300799; // January 1, 10000 CE - const result = createERC20StreamingTerms( - { + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = -1; + + expect(() => + createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }), + ).toThrow('Invalid startTime: must be a positive number'); }); - it('returns Uint8Array for maximum safe bigint values with bytes encoding', () => { - const maxUint256 = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + it('throws an error for start time exceeding upper bound', () => { const tokenAddress = validTokenAddress; - const initialAmount = maxUint256; - const maxAmount = maxUint256; - const amountPerSecond = maxUint256; - const startTime = 1640995200; - const result = createERC20StreamingTerms( - { + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 253402300800; // One second past January 1, 10000 CE + + expect(() => + createERC20StreamingTerms({ tokenAddress, initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, + }), + ).toThrow( + 'Invalid startTime: must be less than or equal to 253402300799', ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Token address first 20 bytes - const addressBytes = Array.from(result.slice(0, 20)); - const expectedAddressBytes = [ - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, - 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, - ]; - expect(addressBytes).toEqual(expectedAddressBytes); - // Next three 32-byte chunks should be all 0xff for the bigint values - expect(Array.from(result.slice(20, 52))).toEqual( - new Array(32).fill(0xff), - ); - expect(Array.from(result.slice(52, 84))).toEqual( - new Array(32).fill(0xff), - ); - expect(Array.from(result.slice(84, 116))).toEqual( - new Array(32).fill(0xff), + it('throws an error for undefined tokenAddress', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: undefined as any, + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); + }); + + it('throws an error for null tokenAddress', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: null as any, + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow('Invalid tokenAddress: must be a valid address'); + }); + + it('throws an error for undefined initialAmount', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: undefined as any, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for null initialAmount', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: null as any, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for undefined maxAmount', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: 0n, + maxAmount: undefined as any, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for null maxAmount', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: 0n, + maxAmount: null as any, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for undefined amountPerSecond', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: undefined as any, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for null amountPerSecond', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: null as any, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for undefined startTime', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: undefined as any, + }), + ).toThrow(); + }); + + it('throws an error for null startTime', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: null as any, + }), + ).toThrow(); + }); + + it('throws an error for Infinity startTime', () => { + expect(() => + createERC20StreamingTerms({ + tokenAddress: validTokenAddress, + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: Infinity, + }), + ).toThrow(); + }); + + it('handles edge case with very large initial amount and small max amount difference', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 999999999999999999n; + const maxAmount = 1000000000000000000n; // Just 1 wei more + const amountPerSecond = 1n; + const startTime = 1640995200; + + const result = createERC20StreamingTerms({ + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }); + + expect(result).toStrictEqual( + '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000000de0b6b3a763ffff0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000061cf9980', ); }); - }); - // Tests for Uint8Array input parameter - describe('Uint8Array input parameter', () => { - it('accepts Uint8Array as tokenAddress parameter', () => { - const tokenAddressBytes = new Uint8Array([ - 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, - 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, - ]); - const initialAmount = 1000000000000000000n; // 1 token (18 decimals) - const maxAmount = 10000000000000000000n; // 10 tokens - const amountPerSecond = 500000000000000000n; // 0.5 token per second - const startTime = 1640995200; // 2022-01-01 00:00:00 UTC + it('handles streaming with minimum viable parameters', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 1n; + const maxAmount = 2n; + const amountPerSecond = 1n; + const startTime = 1; const result = createERC20StreamingTerms({ - tokenAddress: tokenAddressBytes, + tokenAddress, initialAmount, maxAmount, amountPerSecond, @@ -844,15 +559,423 @@ describe('createERC20StreamingTerms', () => { }); expect(result).toStrictEqual( - '0x1234567890123456789012345678901234567890' + - // 1000000000000000000n == 0xde0b6b3a76400000 padded to 32 bytes - '0000000000000000000000000000000000000000000000000de0b6b3a7640000' + - // 10000000000000000000n == 0x8ac7230489e80000 padded to 32 bytes - '0000000000000000000000000000000000000000000000008ac7230489e80000' + - // 500000000000000000n == 0x06f05b59d3b20000 padded to 32 bytes - '00000000000000000000000000000000000000000000000006f05b59d3b20000' + - // 1640995200 == 0x61cf9980 - '0000000000000000000000000000000000000000000000000000000061cf9980', + '0x12345678901234567890123456789012345678900000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', + ); + }); + + // Tests for bytes return type + describe('bytes return type', () => { + it('returns Uint8Array when bytes encoding is specified', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 1000000000000000000n; // 1 token (18 decimals) + const maxAmount = 10000000000000000000n; // 10 tokens + const amountPerSecond = 500000000000000000n; // 0.5 token per second + const startTime = 1640995200; // 2022-01-01 00:00:00 UTC + const result = createERC20StreamingTerms( + { + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); // 20 bytes for token address + 32 bytes for each of the 4 parameters + }); + + it('returns Uint8Array for zero initial amount with bytes encoding', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 0n; + const maxAmount = 5000000000000000000n; // 5 tokens + const amountPerSecond = 1000000000000000000n; // 1 token per second + const startTime = 1672531200; // 2023-01-01 00:00:00 UTC + const result = createERC20StreamingTerms( + { + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + // this is the validTokenAddress represented as bytes + const tokenAddressBytes = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, + 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, + ]; + // initial amount is 32 0x00 bytes + const initialAmountBytes = new Array(32).fill(0); + + // 5000000000000000000n == 0x4563918244f40000 (padded to 32 bytes) + const maxAmountBytes = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x45, 0x63, 0x91, 0x82, 0x44, 0xf4, 0x00, 0x00, + ]; + + // 1000000000000000000n == 0x0de0b6b3a7640000 (padded to 32 bytes) + const amountPerSecondBytes = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0d, 0xe0, 0xb6, 0xb3, 0xa7, 0x64, 0x00, 0x00, + ]; + + // 1672531200 == 0x63b0cd00 + const startTimeBytes = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0xb0, 0xcd, 0x00, + ]; + + const expectedBytes = new Uint8Array([ + ...tokenAddressBytes, + ...initialAmountBytes, + ...maxAmountBytes, + ...amountPerSecondBytes, + ...startTimeBytes, + ]); + + expect(result).toEqual(expectedBytes); + }); + + it('returns Uint8Array for equal initial and max amounts with bytes encoding', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 2000000000000000000n; // 2 tokens + const maxAmount = 2000000000000000000n; // 2 tokens + const amountPerSecond = 100000000000000000n; // 0.1 token per second + const startTime = 1640995200; + const result = createERC20StreamingTerms( + { + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for small values with bytes encoding', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 1n; + const maxAmount = 1000n; + const amountPerSecond = 1n; + const startTime = 1; + const result = createERC20StreamingTerms( + { + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // this is the validTokenAddress represented as bytes + const tokenAddressBytes = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, + 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, + ]; + + // 1n is padded to 32 bytes + const initialAmountBytes = new Array(32).fill(0); + initialAmountBytes[31] = 0x01; + + // 1000n == 0x03e8 (padded to 32 bytes) + const maxAmountBytes = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, + ]; + + // 1n is padded to 32 bytes + const amountPerSecondBytes = new Array(32).fill(0); + amountPerSecondBytes[31] = 0x01; + + // 1 is padded to 32 bytes + const startTimeBytes = new Array(32).fill(0); + startTimeBytes[31] = 0x01; + + const expectedBytes = new Uint8Array([ + ...tokenAddressBytes, + ...initialAmountBytes, + ...maxAmountBytes, + ...amountPerSecondBytes, + ...startTimeBytes, + ]); + expect(result).toStrictEqual(expectedBytes); + }); + + it('returns Uint8Array for large values with bytes encoding', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 100000000000000000000n; // 100 tokens + const maxAmount = 1000000000000000000000n; // 1000 tokens + const amountPerSecond = 10000000000000000000n; // 10 tokens per second + const startTime = 2000000000; // Far future + const result = createERC20StreamingTerms( + { + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for different token addresses with bytes encoding', () => { + const tokenAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; + const result = createERC20StreamingTerms( + { + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Check that the token address is correctly encoded + const addressBytes = Array.from(result.slice(0, 20)); + const expectedAddressBytes = [ + 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, + 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, + ]; + expect(addressBytes).toEqual(expectedAddressBytes); + }); + + it('returns Uint8Array for maximum allowed timestamp with bytes encoding', () => { + const tokenAddress = validTokenAddress; + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 1000000000000000000n; + const startTime = 253402300799; // January 1, 10000 CE + const result = createERC20StreamingTerms( + { + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for maximum safe bigint values with bytes encoding', () => { + const maxUint256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + const tokenAddress = validTokenAddress; + const initialAmount = maxUint256; + const maxAmount = maxUint256; + const amountPerSecond = maxUint256; + const startTime = 1640995200; + const result = createERC20StreamingTerms( + { + tokenAddress, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Token address first 20 bytes + const addressBytes = Array.from(result.slice(0, 20)); + const expectedAddressBytes = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, + 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, + ]; + expect(addressBytes).toEqual(expectedAddressBytes); + // Next three 32-byte chunks should be all 0xff for the bigint values + expect(Array.from(result.slice(20, 52))).toEqual( + new Array(32).fill(0xff), + ); + expect(Array.from(result.slice(52, 84))).toEqual( + new Array(32).fill(0xff), + ); + expect(Array.from(result.slice(84, 116))).toEqual( + new Array(32).fill(0xff), + ); + }); + }); + + // Tests for Uint8Array input parameter + describe('Uint8Array input parameter', () => { + it('accepts Uint8Array as tokenAddress parameter', () => { + const tokenAddressBytes = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, + 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, + ]); + const initialAmount = 1000000000000000000n; // 1 token (18 decimals) + const maxAmount = 10000000000000000000n; // 10 tokens + const amountPerSecond = 500000000000000000n; // 0.5 token per second + const startTime = 1640995200; // 2022-01-01 00:00:00 UTC + + const result = createERC20StreamingTerms({ + tokenAddress: tokenAddressBytes, + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }); + + expect(result).toStrictEqual( + '0x1234567890123456789012345678901234567890' + + // 1000000000000000000n == 0xde0b6b3a76400000 padded to 32 bytes + '0000000000000000000000000000000000000000000000000de0b6b3a7640000' + + // 10000000000000000000n == 0x8ac7230489e80000 padded to 32 bytes + '0000000000000000000000000000000000000000000000008ac7230489e80000' + + // 500000000000000000n == 0x06f05b59d3b20000 padded to 32 bytes + '00000000000000000000000000000000000000000000000006f05b59d3b20000' + + // 1640995200 == 0x61cf9980 + '0000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); + }); + }); + + describe('decodeERC20StreamingTerms', () => { + const tokenAddress = + '0x1234567890123456789012345678901234567890' as `0x${string}`; + + it('decodes standard streaming parameters', () => { + const original = { + tokenAddress, + initialAmount: 1000000000000000000n, + maxAmount: 10000000000000000000n, + amountPerSecond: 500000000000000000n, + startTime: 1640995200, + }; + expect( + decodeERC20StreamingTerms(createERC20StreamingTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes zero initial amount', () => { + const original = { + tokenAddress, + initialAmount: 0n, + maxAmount: 5000000000000000000n, + amountPerSecond: 1000000000000000000n, + startTime: 1672531200, + }; + expect( + decodeERC20StreamingTerms(createERC20StreamingTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes equal initial and max amounts', () => { + const original = { + tokenAddress, + initialAmount: 2000000000000000000n, + maxAmount: 2000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }; + expect( + decodeERC20StreamingTerms(createERC20StreamingTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes small values', () => { + const original = { + tokenAddress, + initialAmount: 1n, + maxAmount: 1000n, + amountPerSecond: 1n, + startTime: 1, + }; + expect( + decodeERC20StreamingTerms(createERC20StreamingTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes large values', () => { + const original = { + tokenAddress, + initialAmount: 100000000000000000000n, + maxAmount: 1000000000000000000000n, + amountPerSecond: 10000000000000000000n, + startTime: 2000000000, + }; + expect( + decodeERC20StreamingTerms(createERC20StreamingTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes maximum allowed timestamp', () => { + const original = { + tokenAddress, + initialAmount: 1000000000000000000n, + maxAmount: 2000000000000000000n, + amountPerSecond: 1000000000000000000n, + startTime: 253402300799, + }; + expect( + decodeERC20StreamingTerms(createERC20StreamingTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes maximum uint256 amounts', () => { + const maxUint256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + const original = { + tokenAddress, + initialAmount: maxUint256, + maxAmount: maxUint256, + amountPerSecond: maxUint256, + startTime: 1640995200, + }; + expect( + decodeERC20StreamingTerms(createERC20StreamingTerms(original)), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const original = { + tokenAddress, + initialAmount: 1000000000000000000n, + maxAmount: 10000000000000000000n, + amountPerSecond: 500000000000000000n, + startTime: 1640995200, + }; + const bytes = createERC20StreamingTerms(original, { out: 'bytes' }); + expect(decodeERC20StreamingTerms(bytes)).toStrictEqual(original); + }); + + it('throws when encoded terms are not exactly 148 bytes', () => { + expect(() => decodeERC20StreamingTerms(`0x${'00'.repeat(147)}`)).toThrow( + 'Invalid ERC20Streaming terms: must be exactly 148 bytes', ); }); }); diff --git a/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts index 10d4bccf..43beca7a 100644 --- a/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts +++ b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts @@ -1,47 +1,78 @@ import { describe, it, expect } from 'vitest'; -import { createERC20TransferAmountTerms } from '../../src/caveats/erc20TransferAmount'; +import { + createERC20TransferAmountTerms, + decodeERC20TransferAmountTerms, +} from '../../src/caveats/erc20TransferAmount'; -describe('createERC20TransferAmountTerms', () => { - const tokenAddress = '0x0000000000000000000000000000000000000011'; +describe('ERC20TransferAmount', () => { + describe('createERC20TransferAmountTerms', () => { + const tokenAddress = '0x0000000000000000000000000000000000000011'; - it('creates valid terms for token and amount', () => { - const result = createERC20TransferAmountTerms({ - tokenAddress, - maxAmount: 10n, + it('creates valid terms for token and amount', () => { + const result = createERC20TransferAmountTerms({ + tokenAddress, + maxAmount: 10n, + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000011' + + '000000000000000000000000000000000000000000000000000000000000000a', + ); }); - 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 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createERC20TransferAmountTerms( - { tokenAddress, maxAmount: 1n }, - { out: 'bytes' }, - ); + describe('decodeERC20TransferAmountTerms', () => { + const tokenAddress = + '0x0000000000000000000000000000000000000011' as `0x${string}`; + + it('decodes token address and max amount', () => { + const original = { tokenAddress, maxAmount: 10n }; + expect( + decodeERC20TransferAmountTerms( + createERC20TransferAmountTerms(original), + ), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const original = { tokenAddress, maxAmount: 1n }; + const bytes = createERC20TransferAmountTerms(original, { out: 'bytes' }); + expect(decodeERC20TransferAmountTerms(bytes)).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(52); + it('throws when encoded terms are not exactly 52 bytes', () => { + expect(() => + decodeERC20TransferAmountTerms(`0x${'00'.repeat(51)}`), + ).toThrow('Invalid ERC20TransferAmount terms: must be exactly 52 bytes'); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts index 52b4b305..724bc822 100644 --- a/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts @@ -1,62 +1,125 @@ import { describe, it, expect } from 'vitest'; -import { createERC721BalanceChangeTerms } from '../../src/caveats/erc721BalanceChange'; +import { + createERC721BalanceChangeTerms, + decodeERC721BalanceChangeTerms, +} from '../../src/caveats/erc721BalanceChange'; import { BalanceChangeType } from '../../src/caveats/types'; -describe('createERC721BalanceChangeTerms', () => { - const tokenAddress = '0x00000000000000000000000000000000000000aa'; - const recipient = '0x00000000000000000000000000000000000000bb'; +describe('ERC721BalanceChange', () => { + 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, + it('creates valid terms for balance increase', () => { + const result = createERC721BalanceChangeTerms({ + tokenAddress, + recipient, + amount: 1n, + changeType: BalanceChangeType.Increase, + }); + + expect(result).toStrictEqual( + '0x00' + + '00000000000000000000000000000000000000aa' + + '00000000000000000000000000000000000000bb' + + '0000000000000000000000000000000000000000000000000000000000000001', + ); }); - 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); + }); }); - it('throws for invalid recipient', () => { - expect(() => - createERC721BalanceChangeTerms({ + describe('decodeERC721BalanceChangeTerms', () => { + const tokenAddress = + '0x00000000000000000000000000000000000000aa' as `0x${string}`; + const recipient = + '0x00000000000000000000000000000000000000bb' as `0x${string}`; + + it('decodes increase', () => { + const original = { tokenAddress, - recipient: '0x1234', + recipient, amount: 1n, changeType: BalanceChangeType.Increase, - }), - ).toThrow('Invalid recipient: must be a valid address'); - }); + }; + const decoded = decodeERC721BalanceChangeTerms( + createERC721BalanceChangeTerms(original), + ); + expect(decoded.changeType).toBe(BalanceChangeType.Increase); + expect((decoded.tokenAddress as string).toLowerCase()).toBe( + tokenAddress.toLowerCase(), + ); + expect((decoded.recipient as string).toLowerCase()).toBe( + recipient.toLowerCase(), + ); + expect(decoded.amount).toBe(1n); + }); - it('throws for invalid amount', () => { - expect(() => - createERC721BalanceChangeTerms({ + it('decodes decrease', () => { + const original = { tokenAddress, recipient, - amount: 0n, + amount: 2n, changeType: BalanceChangeType.Decrease, - }), - ).toThrow('Invalid balance: must be a positive number'); - }); + }; + const decoded = decodeERC721BalanceChangeTerms( + createERC721BalanceChangeTerms(original), + ); + expect(decoded.changeType).toBe(BalanceChangeType.Decrease); + expect(decoded.amount).toBe(2n); + }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createERC721BalanceChangeTerms( - { + it('accepts Uint8Array terms from the encoder', () => { + const original = { tokenAddress, recipient, amount: 2n, changeType: BalanceChangeType.Decrease, - }, - { out: 'bytes' }, - ); + }; + const bytes = createERC721BalanceChangeTerms(original, { out: 'bytes' }); + expect(decodeERC721BalanceChangeTerms(bytes).amount).toBe(2n); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(73); + it('throws when encoded terms are not exactly 73 bytes', () => { + expect(() => + decodeERC721BalanceChangeTerms(`0x${'00'.repeat(72)}`), + ).toThrow('Invalid ERC721BalanceChange terms: must be exactly 73 bytes'); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc721Transfer.test.ts b/packages/delegation-core/test/caveats/erc721Transfer.test.ts index 30b61766..f02e6522 100644 --- a/packages/delegation-core/test/caveats/erc721Transfer.test.ts +++ b/packages/delegation-core/test/caveats/erc721Transfer.test.ts @@ -1,47 +1,83 @@ import { describe, it, expect } from 'vitest'; -import { createERC721TransferTerms } from '../../src/caveats/erc721Transfer'; +import { + createERC721TransferTerms, + decodeERC721TransferTerms, +} from '../../src/caveats/erc721Transfer'; -describe('createERC721TransferTerms', () => { - const tokenAddress = '0x00000000000000000000000000000000000000aa'; +describe('ERC721Transfer', () => { + describe('createERC721TransferTerms', () => { + const tokenAddress = '0x00000000000000000000000000000000000000aa'; - it('creates valid terms for token and tokenId', () => { - const result = createERC721TransferTerms({ - tokenAddress, - tokenId: 42n, + it('creates valid terms for token and tokenId', () => { + const result = createERC721TransferTerms({ + tokenAddress, + tokenId: 42n, + }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000aa' + + '000000000000000000000000000000000000000000000000000000000000002a', + ); }); - 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 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createERC721TransferTerms( - { tokenAddress, tokenId: 1n }, - { out: 'bytes' }, - ); + describe('decodeERC721TransferTerms', () => { + const tokenAddress = + '0x00000000000000000000000000000000000000aa' as `0x${string}`; + + it('decodes token address and token id', () => { + const original = { tokenAddress, tokenId: 42n }; + expect( + decodeERC721TransferTerms(createERC721TransferTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes token id zero', () => { + const original = { tokenAddress, tokenId: 0n }; + expect( + decodeERC721TransferTerms(createERC721TransferTerms(original)), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const original = { tokenAddress, tokenId: 1n }; + const bytes = createERC721TransferTerms(original, { out: 'bytes' }); + expect(decodeERC721TransferTerms(bytes)).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(52); + it('throws when encoded terms are not exactly 52 bytes', () => { + expect(() => decodeERC721TransferTerms(`0x${'00'.repeat(51)}`)).toThrow( + 'Invalid ERC721Transfer terms: must be exactly 52 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/exactCalldata.test.ts b/packages/delegation-core/test/caveats/exactCalldata.test.ts index 0a3c8677..dc2dff02 100644 --- a/packages/delegation-core/test/caveats/exactCalldata.test.ts +++ b/packages/delegation-core/test/caveats/exactCalldata.test.ts @@ -1,252 +1,341 @@ import { describe, it, expect } from 'vitest'; -import { createExactCalldataTerms } from '../../src/caveats/exactCalldata'; +import { + createExactCalldataTerms, + decodeExactCalldataTerms, +} from '../../src/caveats/exactCalldata'; import type { Hex } from '../../src/types'; -describe('createExactCalldataTerms', () => { - // Note: ExactCalldata terms length varies based on input calldata length - it('creates valid terms for simple calldata', () => { - const calldata = '0x1234567890abcdef'; - const result = createExactCalldataTerms({ calldata }); +describe('ExactCalldata', () => { + describe('createExactCalldataTerms', () => { + // Note: ExactCalldata terms length varies based on input calldata length + it('creates valid terms for simple calldata', () => { + const calldata = '0x1234567890abcdef'; + const result = createExactCalldataTerms({ calldata }); - expect(result).toStrictEqual(calldata); - }); + expect(result).toStrictEqual(calldata); + }); - it('creates valid terms for empty calldata', () => { - const calldata = '0x'; - const result = createExactCalldataTerms({ calldata }); + it('creates valid terms for empty calldata', () => { + const calldata = '0x'; + const result = createExactCalldataTerms({ calldata }); - expect(result).toStrictEqual('0x'); - }); + expect(result).toStrictEqual('0x'); + }); - it('creates valid terms for function call with parameters', () => { - // Example: transfer(address,uint256) function call - const calldata = - '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000'; - const result = createExactCalldataTerms({ calldata }); + it('creates valid terms for function call with parameters', () => { + // Example: transfer(address,uint256) function call + const calldata = + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + const result = createExactCalldataTerms({ calldata }); - expect(result).toStrictEqual(calldata); - }); + expect(result).toStrictEqual(calldata); + }); - it('creates valid terms for complex calldata', () => { - const calldata = - '0x23b872dd000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5f0000000000000000000000000000000000000000000000000de0b6b3a7640000'; - const result = createExactCalldataTerms({ calldata }); + it('creates valid terms for complex calldata', () => { + const calldata = + '0x23b872dd000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5f0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + const result = createExactCalldataTerms({ calldata }); - expect(result).toStrictEqual(calldata); - }); + expect(result).toStrictEqual(calldata); + }); - it('creates valid terms for uppercase hex calldata', () => { - const calldata = '0x1234567890ABCDEF'; - const result = createExactCalldataTerms({ calldata }); + it('creates valid terms for uppercase hex calldata', () => { + const calldata = '0x1234567890ABCDEF'; + const result = createExactCalldataTerms({ calldata }); - expect(result).toStrictEqual(calldata); - }); + expect(result).toStrictEqual(calldata); + }); - it('creates valid terms for mixed case hex calldata', () => { - const calldata = '0x1234567890AbCdEf'; - const result = createExactCalldataTerms({ calldata }); + it('creates valid terms for mixed case hex calldata', () => { + const calldata = '0x1234567890AbCdEf'; + const result = createExactCalldataTerms({ calldata }); - expect(result).toStrictEqual(calldata); - }); + expect(result).toStrictEqual(calldata); + }); - it('creates valid terms for very long calldata', () => { - const longCalldata: Hex = `0x${'a'.repeat(1000)}`; - const result = createExactCalldataTerms({ calldata: longCalldata }); + it('creates valid terms for very long calldata', () => { + const longCalldata: Hex = `0x${'a'.repeat(1000)}`; + const result = createExactCalldataTerms({ calldata: longCalldata }); - expect(result).toStrictEqual(longCalldata); - }); + expect(result).toStrictEqual(longCalldata); + }); - it('throws an error for calldata without 0x prefix', () => { - const invalidCallData = '1234567890abcdef'; + it('throws an error for calldata without 0x prefix', () => { + const invalidCallData = '1234567890abcdef'; - expect(() => - createExactCalldataTerms({ calldata: invalidCallData as Hex }), - ).toThrow('Invalid calldata: must be a hex string starting with 0x'); - }); + expect(() => + createExactCalldataTerms({ calldata: invalidCallData as Hex }), + ).toThrow('Invalid calldata: must be a hex string starting with 0x'); + }); - it('throws an error for empty string', () => { - const invalidCallData = ''; + it('throws an error for empty string', () => { + const invalidCallData = ''; - expect(() => - createExactCalldataTerms({ calldata: invalidCallData as Hex }), - ).toThrow('Invalid calldata: must be a hex string starting with 0x'); - }); + expect(() => + createExactCalldataTerms({ calldata: invalidCallData as Hex }), + ).toThrow('Invalid calldata: must be a hex string starting with 0x'); + }); - it('throws an error for malformed hex prefix', () => { - const invalidCallData = '0X1234'; // uppercase X + it('throws an error for malformed hex prefix', () => { + const invalidCallData = '0X1234'; // uppercase X - expect(() => - createExactCalldataTerms({ calldata: invalidCallData as Hex }), - ).toThrow('Invalid calldata: must be a hex string starting with 0x'); - }); + expect(() => + createExactCalldataTerms({ calldata: invalidCallData as Hex }), + ).toThrow('Invalid calldata: must be a hex string starting with 0x'); + }); - it('throws an error for undefined callData', () => { - expect(() => - createExactCalldataTerms({ calldata: undefined as any }), - ).toThrow('Invalid calldata: calldata is required'); - }); + it('throws an error for undefined callData', () => { + expect(() => + createExactCalldataTerms({ calldata: undefined as any }), + ).toThrow('Invalid calldata: calldata is required'); + }); - it('throws an error for null callData', () => { - expect(() => createExactCalldataTerms({ calldata: null as any })).toThrow( - 'Invalid calldata: calldata is required', - ); - }); + it('throws an error for null callData', () => { + expect(() => createExactCalldataTerms({ calldata: null as any })).toThrow( + 'Invalid calldata: calldata is required', + ); + }); - it('throws an error for non-string non-Uint8Array callData', () => { - expect(() => createExactCalldataTerms({ calldata: 1234 as any })).toThrow(); - }); + it('throws an error for non-string non-Uint8Array callData', () => { + expect(() => + createExactCalldataTerms({ calldata: 1234 as any }), + ).toThrow(); + }); - it('handles single function selector', () => { - const functionSelector = '0xa9059cbb'; // transfer(address,uint256) selector - const result = createExactCalldataTerms({ calldata: functionSelector }); + it('handles single function selector', () => { + const functionSelector = '0xa9059cbb'; // transfer(address,uint256) selector + const result = createExactCalldataTerms({ calldata: functionSelector }); - expect(result).toStrictEqual(functionSelector); - }); + expect(result).toStrictEqual(functionSelector); + }); - it('handles calldata with odd length', () => { - const oddLengthCalldata = '0x123'; - const result = createExactCalldataTerms({ calldata: oddLengthCalldata }); + it('handles calldata with odd length', () => { + const oddLengthCalldata = '0x123'; + const result = createExactCalldataTerms({ calldata: oddLengthCalldata }); - expect(result).toStrictEqual(oddLengthCalldata); - }); + expect(result).toStrictEqual(oddLengthCalldata); + }); - // Tests for bytes return type - describe('bytes return type', () => { - it('returns Uint8Array when bytes encoding is specified', () => { - const calldata = '0x1234567890abcdef'; - const result = createExactCalldataTerms({ calldata }, { out: 'bytes' }); + // Tests for bytes return type + describe('bytes return type', () => { + it('returns Uint8Array when bytes encoding is specified', () => { + const calldata = '0x1234567890abcdef'; + const result = createExactCalldataTerms({ calldata }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(Array.from(result)).toEqual([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, + ]); + }); + + it('returns Uint8Array for empty calldata with bytes encoding', () => { + const calldata = '0x'; + const result = createExactCalldataTerms({ calldata }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(0); + }); + + it('returns Uint8Array for complex calldata with bytes encoding', () => { + const calldata = + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + const result = createExactCalldataTerms({ calldata }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(68); // 4 bytes selector + 32 bytes address + 32 bytes amount + }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(Array.from(result)).toEqual([ - 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, - ]); + // Tests for Uint8Array input parameter + describe('Uint8Array input parameter', () => { + it('accepts Uint8Array as calldata parameter', () => { + const callDataBytes = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, + ]); + const result = createExactCalldataTerms({ calldata: callDataBytes }); + + expect(result).toStrictEqual('0x1234567890abcdef'); + }); + + it('accepts empty Uint8Array as calldata parameter', () => { + const callDataBytes = new Uint8Array([]); + const result = createExactCalldataTerms({ calldata: callDataBytes }); + + expect(result).toStrictEqual('0x'); + }); + + it('accepts Uint8Array for function call with parameters', () => { + // transfer(address,uint256) function call as bytes + const callDataBytes = new Uint8Array([ + 0xa9, + 0x05, + 0x9c, + 0xbb, // transfer selector + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, // padding + 0x74, + 0x2d, + 0x35, + 0xcc, + 0x66, + 0x34, + 0xc0, + 0x53, + 0x29, + 0x25, + 0xa3, + 0xb8, + 0xd4, + 0x0e, + 0xc4, + 0x9b, + 0x0e, + 0x8b, + 0xaa, + 0x5e, // address + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0d, + 0xe0, + 0xb6, + 0xb3, + 0xa7, + 0x64, + 0x00, + 0x00, // amount + ]); + const result = createExactCalldataTerms({ calldata: callDataBytes }); + + expect(result).toStrictEqual( + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000', + ); + }); + + it('returns Uint8Array when input is Uint8Array and bytes encoding is specified', () => { + const callDataBytes = new Uint8Array([0x12, 0x34, 0x56, 0x78]); + const result = createExactCalldataTerms( + { calldata: callDataBytes }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(Array.from(result)).toEqual([0x12, 0x34, 0x56, 0x78]); + }); }); + }); - it('returns Uint8Array for empty calldata with bytes encoding', () => { - const calldata = '0x'; - const result = createExactCalldataTerms({ calldata }, { out: 'bytes' }); + describe('decodeExactCalldataTerms', () => { + it('decodes simple calldata', () => { + const calldata = '0x1234567890abcdef' as Hex; + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ + calldata, + }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(0); + it('decodes empty calldata', () => { + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata: '0x' })), + ).toStrictEqual({ + calldata: '0x', + }); }); - it('returns Uint8Array for complex calldata with bytes encoding', () => { + it('decodes transfer calldata', () => { const calldata = - '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000'; - const result = createExactCalldataTerms({ calldata }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(68); // 4 bytes selector + 32 bytes address + 32 bytes amount + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000' as Hex; + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ + calldata, + }); }); - }); - // Tests for Uint8Array input parameter - describe('Uint8Array input parameter', () => { - it('accepts Uint8Array as calldata parameter', () => { - const callDataBytes = new Uint8Array([ - 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, - ]); - const result = createExactCalldataTerms({ calldata: callDataBytes }); + it('preserves hex casing', () => { + const calldata = '0x1234567890AbCdEf' as Hex; + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ + calldata, + }); + }); - expect(result).toStrictEqual('0x1234567890abcdef'); + it('decodes very long calldata', () => { + const calldata: Hex = `0x${'a'.repeat(1000)}`; + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ + calldata, + }); }); - it('accepts empty Uint8Array as calldata parameter', () => { - const callDataBytes = new Uint8Array([]); - const result = createExactCalldataTerms({ calldata: callDataBytes }); + it('decodes selector-only calldata', () => { + const calldata = '0xa9059cbb' as Hex; + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ + calldata, + }); + }); - expect(result).toStrictEqual('0x'); + it('decodes odd-length calldata', () => { + const calldata = '0x123' as Hex; + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ + calldata, + }); }); - it('accepts Uint8Array for function call with parameters', () => { - // transfer(address,uint256) function call as bytes + it('decodes terms created from Uint8Array calldata', () => { const callDataBytes = new Uint8Array([ - 0xa9, - 0x05, - 0x9c, - 0xbb, // transfer selector - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, // padding - 0x74, - 0x2d, - 0x35, - 0xcc, - 0x66, - 0x34, - 0xc0, - 0x53, - 0x29, - 0x25, - 0xa3, - 0xb8, - 0xd4, - 0x0e, - 0xc4, - 0x9b, - 0x0e, - 0x8b, - 0xaa, - 0x5e, // address - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x0d, - 0xe0, - 0xb6, - 0xb3, - 0xa7, - 0x64, - 0x00, - 0x00, // amount + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, ]); - const result = createExactCalldataTerms({ calldata: callDataBytes }); - - expect(result).toStrictEqual( - '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000', - ); + expect( + decodeExactCalldataTerms( + createExactCalldataTerms({ calldata: callDataBytes }), + ), + ).toStrictEqual({ calldata: '0x1234567890abcdef' }); }); - it('returns Uint8Array when input is Uint8Array and bytes encoding is specified', () => { - const callDataBytes = new Uint8Array([0x12, 0x34, 0x56, 0x78]); - const result = createExactCalldataTerms( - { calldata: callDataBytes }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(Array.from(result)).toEqual([0x12, 0x34, 0x56, 0x78]); + it('accepts Uint8Array terms from the encoder', () => { + const calldata = '0x1234567890abcdef' as Hex; + const bytes = createExactCalldataTerms({ calldata }, { out: 'bytes' }); + expect(decodeExactCalldataTerms(bytes)).toStrictEqual({ calldata }); }); }); }); diff --git a/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts b/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts index 89544c95..af52aa45 100644 --- a/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts +++ b/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts @@ -2,46 +2,74 @@ import { encodeSingle } from '@metamask/abi-utils'; import { bytesToHex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { createExactCalldataBatchTerms } from '../../src/caveats/exactCalldataBatch'; +import { + createExactCalldataBatchTerms, + decodeExactCalldataBatchTerms, +} 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); - }); +describe('ExactCalldataBatch', () => { + 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createExactCalldataBatchTerms( - { executions }, - { out: 'bytes' }, - ); + describe('decodeExactCalldataBatchTerms', () => { + const targetA: `0x${string}` = '0x0000000000000000000000000000000000000003'; + const targetB: `0x${string}` = '0x0000000000000000000000000000000000000004'; + + const executions = [ + { target: targetA, value: 0n, callData: '0xdeadbeef' as `0x${string}` }, + { target: targetB, value: 5n, callData: '0x' as `0x${string}` }, + ]; + + it('decodes multiple executions', () => { + const original = { executions }; + expect( + decodeExactCalldataBatchTerms(createExactCalldataBatchTerms(original)), + ).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); + it('accepts Uint8Array terms from the encoder', () => { + const original = { executions }; + const bytes = createExactCalldataBatchTerms(original, { out: 'bytes' }); + expect(decodeExactCalldataBatchTerms(bytes)).toStrictEqual(original); + }); }); }); diff --git a/packages/delegation-core/test/caveats/exactExecution.test.ts b/packages/delegation-core/test/caveats/exactExecution.test.ts index 90a3692c..e7ce0b08 100644 --- a/packages/delegation-core/test/caveats/exactExecution.test.ts +++ b/packages/delegation-core/test/caveats/exactExecution.test.ts @@ -1,75 +1,129 @@ import { describe, it, expect } from 'vitest'; -import { createExactExecutionTerms } from '../../src/caveats/exactExecution'; +import { + createExactExecutionTerms, + decodeExactExecutionTerms, +} from '../../src/caveats/exactExecution'; -describe('createExactExecutionTerms', () => { - const target = '0x00000000000000000000000000000000000000ab'; +describe('ExactExecution', () => { + describe('createExactExecutionTerms', () => { + const target = '0x00000000000000000000000000000000000000ab'; - it('creates valid terms for execution', () => { - const result = createExactExecutionTerms({ - execution: { - target, - value: 1n, - callData: '0x1234', - }, + it('creates valid terms for execution', () => { + const result = createExactExecutionTerms({ + execution: { + target, + value: 1n, + callData: '0x1234', + }, + }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000ab' + + '0000000000000000000000000000000000000000000000000000000000000001' + + '1234', + ); }); - 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 invalid target', () => { - expect(() => - createExactExecutionTerms({ - execution: { - target: '0x1234', - value: 1n, - callData: '0x', + 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', + }, }, - }), - ).toThrow('Invalid target: must be a valid address'); + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(20 + 32 + 2); + }); }); - it('throws for negative value', () => { - expect(() => - createExactExecutionTerms({ + describe('decodeExactExecutionTerms', () => { + const target = + '0x00000000000000000000000000000000000000ab' as `0x${string}`; + + it('decodes target, value, and calldata', () => { + const original = { execution: { target, - value: -1n, - callData: '0x', + value: 1n, + callData: '0x1234' as `0x${string}`, }, - }), - ).toThrow('Invalid value: must be a non-negative number'); - }); + }; + expect( + decodeExactExecutionTerms(createExactExecutionTerms(original)), + ).toStrictEqual(original); + }); - it('throws for invalid calldata', () => { - expect(() => - createExactExecutionTerms({ + it('decodes zero value and empty calldata', () => { + const original = { execution: { target, value: 0n, - callData: '1234' as any, + callData: '0x' as `0x${string}`, }, - }), - ).toThrow('Invalid calldata: must be a hex string starting with 0x'); - }); + }; + expect( + decodeExactExecutionTerms(createExactExecutionTerms(original)), + ).toStrictEqual(original); + }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createExactExecutionTerms( - { + it('accepts Uint8Array terms from the encoder', () => { + const original = { execution: { target, value: 1n, - callData: '0x1234', + callData: '0x1234' as `0x${string}`, }, - }, - { out: 'bytes' }, - ); + }; + const bytes = createExactExecutionTerms(original, { out: 'bytes' }); + expect(decodeExactExecutionTerms(bytes)).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(20 + 32 + 2); + it('throws when encoded terms are shorter than 52 bytes', () => { + expect(() => decodeExactExecutionTerms(`0x${'00'.repeat(51)}`)).toThrow( + 'Invalid ExactExecution terms: must be at least 52 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts b/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts index a68e4b45..03f1db68 100644 --- a/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts +++ b/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts @@ -2,50 +2,80 @@ import { encodeSingle } from '@metamask/abi-utils'; import { bytesToHex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { createExactExecutionBatchTerms } from '../../src/caveats/exactExecutionBatch'; +import { + createExactExecutionBatchTerms, + decodeExactExecutionBatchTerms, +} 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); - }); +describe('ExactExecutionBatch', () => { + describe('createExactExecutionBatchTerms', () => { + const targetA: Hex = '0x0000000000000000000000000000000000000001'; + const targetB: Hex = '0x0000000000000000000000000000000000000002'; - it('throws for empty executions array', () => { - expect(() => createExactExecutionBatchTerms({ executions: [] })).toThrow( - 'Invalid executions: array cannot be empty', - ); - }); + 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createExactExecutionBatchTerms( - { executions }, - { out: 'bytes' }, - ); + describe('decodeExactExecutionBatchTerms', () => { + const targetA: `0x${string}` = '0x0000000000000000000000000000000000000001'; + const targetB: `0x${string}` = '0x0000000000000000000000000000000000000002'; + + const executions = [ + { target: targetA, value: 1n, callData: '0x1234' as `0x${string}` }, + { target: targetB, value: 2n, callData: '0x' as `0x${string}` }, + ]; + + it('decodes multiple executions', () => { + const original = { executions }; + expect( + decodeExactExecutionBatchTerms( + createExactExecutionBatchTerms(original), + ), + ).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); + it('accepts Uint8Array terms from the encoder', () => { + const original = { executions }; + const bytes = createExactExecutionBatchTerms(original, { out: 'bytes' }); + expect(decodeExactExecutionBatchTerms(bytes)).toStrictEqual(original); + }); }); }); diff --git a/packages/delegation-core/test/caveats/id.test.ts b/packages/delegation-core/test/caveats/id.test.ts index 382a344d..f8364107 100644 --- a/packages/delegation-core/test/caveats/id.test.ts +++ b/packages/delegation-core/test/caveats/id.test.ts @@ -1,46 +1,86 @@ import { describe, it, expect } from 'vitest'; -import { createIdTerms } from '../../src/caveats/id'; +import { createIdTerms, decodeIdTerms } from '../../src/caveats/id'; -describe('createIdTerms', () => { - it('creates valid terms for number id', () => { - const result = createIdTerms({ id: 1 }); +describe('Id', () => { + describe('createIdTerms', () => { + it('creates valid terms for number id', () => { + const result = createIdTerms({ id: 1 }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000001', - ); - }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + }); - it('creates valid terms for bigint id', () => { - const result = createIdTerms({ id: 255n }); + it('creates valid terms for bigint id', () => { + const result = createIdTerms({ id: 255n }); - expect(result).toStrictEqual( - '0x00000000000000000000000000000000000000000000000000000000000000ff', - ); - }); + 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 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 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createIdTerms({ id: 2 }, { out: 'bytes' }); + describe('decodeIdTerms', () => { + it('decodes zero id', () => { + expect(decodeIdTerms(createIdTerms({ id: 0n }))).toStrictEqual({ + id: 0n, + }); + }); + + it('decodes bigint id matching encoder output', () => { + const original = { id: 255n }; + expect(decodeIdTerms(createIdTerms(original))).toStrictEqual(original); + }); + + it('decodes terms produced from a number id as bigint', () => { + expect(decodeIdTerms(createIdTerms({ id: 1 }))).toStrictEqual({ + id: 1n, + }); + }); + + it('decodes maximum uint256', () => { + const max = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + expect(decodeIdTerms(createIdTerms({ id: max }))).toStrictEqual({ + id: max, + }); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createIdTerms({ id: 42n }, { out: 'bytes' }); + expect(decodeIdTerms(bytes)).toStrictEqual({ id: 42n }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('throws when encoded terms are not exactly 32 bytes', () => { + expect(() => decodeIdTerms(`0x${'00'.repeat(31)}`)).toThrow( + 'Invalid Id terms: must be exactly 32 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/limitedCalls.test.ts b/packages/delegation-core/test/caveats/limitedCalls.test.ts index 4cad5e60..19bafc66 100644 --- a/packages/delegation-core/test/caveats/limitedCalls.test.ts +++ b/packages/delegation-core/test/caveats/limitedCalls.test.ts @@ -1,32 +1,64 @@ import { describe, it, expect } from 'vitest'; -import { createLimitedCallsTerms } from '../../src/caveats/limitedCalls'; +import { + createLimitedCallsTerms, + decodeLimitedCallsTerms, +} from '../../src/caveats/limitedCalls'; -describe('createLimitedCallsTerms', () => { - it('creates valid terms for a positive limit', () => { - const result = createLimitedCallsTerms({ limit: 5 }); +describe('LimitedCalls', () => { + describe('createLimitedCallsTerms', () => { + it('creates valid terms for a positive limit', () => { + const result = createLimitedCallsTerms({ limit: 5 }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000005', - ); - }); + 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-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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createLimitedCallsTerms({ limit: 3 }, { out: 'bytes' }); + describe('decodeLimitedCallsTerms', () => { + it('decodes a positive limit', () => { + const original = { limit: 5 }; + expect( + decodeLimitedCallsTerms(createLimitedCallsTerms(original)), + ).toStrictEqual(original); + }); + + it('decodes a larger limit', () => { + const original = { limit: 999_999 }; + expect( + decodeLimitedCallsTerms(createLimitedCallsTerms(original)), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createLimitedCallsTerms({ limit: 3 }, { out: 'bytes' }); + expect(decodeLimitedCallsTerms(bytes)).toStrictEqual({ limit: 3 }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('throws when encoded terms are not exactly 32 bytes', () => { + expect(() => decodeLimitedCallsTerms(`0x${'00'.repeat(31)}`)).toThrow( + 'Invalid LimitedCalls terms: must be exactly 32 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts index bb3d9131..a344fbb6 100644 --- a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts +++ b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts @@ -1,99 +1,165 @@ import { describe, it, expect } from 'vitest'; -import { createMultiTokenPeriodTerms } from '../../src/caveats/multiTokenPeriod'; +import { + createMultiTokenPeriodTerms, + decodeMultiTokenPeriodTerms, +} from '../../src/caveats/multiTokenPeriod'; -describe('createMultiTokenPeriodTerms', () => { - const token = '0x0000000000000000000000000000000000000011'; +describe('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({ + it('creates valid terms for a single token config', () => { + const result = createMultiTokenPeriodTerms({ tokenConfigs: [ { - token: '0x1234', - periodAmount: 1n, - periodDuration: 1, - startDate: 1, + token, + periodAmount: 100n, + periodDuration: 60, + startDate: 10, }, ], - }), - ).toThrow('Invalid token address: 0x1234'); + }); + + 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('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( + { + tokenConfigs: [ + { + token, + periodAmount: 1n, + periodDuration: 1, + startDate: 1, + }, + ], + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(116); + }); }); - it('throws for invalid period amount', () => { - expect(() => - createMultiTokenPeriodTerms({ + describe('decodeMultiTokenPeriodTerms', () => { + const token = '0x0000000000000000000000000000000000000011' as `0x${string}`; + const token2 = + '0x0000000000000000000000000000000000000022' as `0x${string}`; + + it('decodes a single token config', () => { + const original = { tokenConfigs: [ { token, - periodAmount: 0n, - periodDuration: 1, - startDate: 1, + periodAmount: 100n, + periodDuration: 60, + startDate: 10, }, ], - }), - ).toThrow('Invalid period amount: must be greater than 0'); - }); + }; + expect( + decodeMultiTokenPeriodTerms(createMultiTokenPeriodTerms(original)), + ).toStrictEqual(original); + }); - it('throws for invalid start date (zero)', () => { - expect(() => - createMultiTokenPeriodTerms({ + it('decodes multiple token configs', () => { + const original = { tokenConfigs: [ { token, - periodAmount: 1n, - periodDuration: 1, - startDate: 0, + periodAmount: 100n, + periodDuration: 60, + startDate: 10, }, - ], - }), - ).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, + token: token2, + periodAmount: 200n, + periodDuration: 120, + startDate: 20, }, ], - }), - ).toThrow('Invalid start date: must be greater than 0'); - }); + }; + expect( + decodeMultiTokenPeriodTerms(createMultiTokenPeriodTerms(original)), + ).toStrictEqual(original); + }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createMultiTokenPeriodTerms( - { + it('accepts Uint8Array terms from the encoder', () => { + const original = { tokenConfigs: [ { token, @@ -102,11 +168,17 @@ describe('createMultiTokenPeriodTerms', () => { startDate: 1, }, ], - }, - { out: 'bytes' }, - ); + }; + const bytes = createMultiTokenPeriodTerms(original, { out: 'bytes' }); + expect(decodeMultiTokenPeriodTerms(bytes)).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(116); + it('throws when encoded terms length is not a multiple of 116 bytes', () => { + expect(() => + decodeMultiTokenPeriodTerms(`0x${'00'.repeat(115)}`), + ).toThrow( + 'Invalid MultiTokenPeriod terms: must be a multiple of 116 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts index 877eb06b..e6d46f81 100644 --- a/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts @@ -1,66 +1,123 @@ import { describe, it, expect } from 'vitest'; -import { createNativeBalanceChangeTerms } from '../../src/caveats/nativeBalanceChange'; +import { + createNativeBalanceChangeTerms, + decodeNativeBalanceChangeTerms, +} from '../../src/caveats/nativeBalanceChange'; import { BalanceChangeType } from '../../src/caveats/types'; -describe('createNativeBalanceChangeTerms', () => { - const recipient = '0x00000000000000000000000000000000000000cc'; +describe('NativeBalanceChange', () => { + describe('createNativeBalanceChangeTerms', () => { + const recipient = '0x00000000000000000000000000000000000000cc'; - it('creates valid terms for balance increase', () => { - const result = createNativeBalanceChangeTerms({ - recipient, - balance: 1n, - changeType: BalanceChangeType.Increase, + it('creates valid terms for balance increase', () => { + const result = createNativeBalanceChangeTerms({ + recipient, + balance: 1n, + changeType: BalanceChangeType.Increase, + }); + + expect(result).toStrictEqual( + '0x00' + + '00000000000000000000000000000000000000cc' + + '0000000000000000000000000000000000000000000000000000000000000001', + ); }); - 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 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); + }); }); - it('throws for invalid balance', () => { - expect(() => - createNativeBalanceChangeTerms({ + describe('decodeNativeBalanceChangeTerms', () => { + const recipient = + '0x00000000000000000000000000000000000000cc' as `0x${string}`; + + it('decodes increase balance change', () => { + const original = { recipient, - balance: 0n, + balance: 1n, changeType: BalanceChangeType.Increase, - }), - ).toThrow('Invalid balance: must be a positive number'); - }); + }; + const decoded = decodeNativeBalanceChangeTerms( + createNativeBalanceChangeTerms(original), + ); + expect(decoded.changeType).toBe(original.changeType); + expect((decoded.recipient as string).toLowerCase()).toBe( + recipient.toLowerCase(), + ); + expect(decoded.balance).toBe(original.balance); + }); - it('throws for invalid changeType', () => { - expect(() => - createNativeBalanceChangeTerms({ + it('decodes decrease balance change', () => { + const original = { recipient, - balance: 1n, - changeType: 2 as any, - }), - ).toThrow('Invalid changeType: must be either Increase or Decrease'); - }); + balance: 2n, + changeType: BalanceChangeType.Decrease, + }; + const decoded = decodeNativeBalanceChangeTerms( + createNativeBalanceChangeTerms(original), + ); + expect(decoded.changeType).toBe(original.changeType); + expect(decoded.balance).toBe(original.balance); + }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createNativeBalanceChangeTerms( - { + it('accepts Uint8Array terms from the encoder', () => { + const original = { recipient, balance: 2n, changeType: BalanceChangeType.Decrease, - }, - { out: 'bytes' }, - ); + }; + const bytes = createNativeBalanceChangeTerms(original, { out: 'bytes' }); + const decoded = decodeNativeBalanceChangeTerms(bytes); + expect(decoded.balance).toBe(2n); + expect(decoded.changeType).toBe(BalanceChangeType.Decrease); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(53); + it('throws when encoded terms are not exactly 53 bytes', () => { + expect(() => + decodeNativeBalanceChangeTerms(`0x${'00'.repeat(52)}`), + ).toThrow('Invalid NativeBalanceChange terms: must be exactly 53 bytes'); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts index c52d6e7d..afe68b43 100644 --- a/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts @@ -1,47 +1,84 @@ import { describe, it, expect } from 'vitest'; -import { createNativeTokenPaymentTerms } from '../../src/caveats/nativeTokenPayment'; +import { + createNativeTokenPaymentTerms, + decodeNativeTokenPaymentTerms, +} from '../../src/caveats/nativeTokenPayment'; -describe('createNativeTokenPaymentTerms', () => { - const recipient = '0x00000000000000000000000000000000000000bb'; +describe('NativeTokenPayment', () => { + describe('createNativeTokenPaymentTerms', () => { + const recipient = '0x00000000000000000000000000000000000000bb'; - it('creates valid terms for recipient and amount', () => { - const result = createNativeTokenPaymentTerms({ - recipient, - amount: 10n, + it('creates valid terms for recipient and amount', () => { + const result = createNativeTokenPaymentTerms({ + recipient, + amount: 10n, + }); + + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000bb' + + '000000000000000000000000000000000000000000000000000000000000000a', + ); }); - 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 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createNativeTokenPaymentTerms( - { recipient, amount: 5n }, - { out: 'bytes' }, - ); + describe('decodeNativeTokenPaymentTerms', () => { + const recipient = + '0x00000000000000000000000000000000000000bb' as `0x${string}`; + + it('decodes recipient and amount', () => { + const original = { recipient, amount: 10n }; + const decoded = decodeNativeTokenPaymentTerms( + createNativeTokenPaymentTerms(original), + ); + expect((decoded.recipient as string).toLowerCase()).toBe( + recipient.toLowerCase(), + ); + expect(decoded.amount).toBe(original.amount); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(52); + it('accepts Uint8Array terms from the encoder', () => { + const original = { recipient, amount: 5n }; + const bytes = createNativeTokenPaymentTerms(original, { out: 'bytes' }); + const decoded = decodeNativeTokenPaymentTerms(bytes); + expect((decoded.recipient as string).toLowerCase()).toBe( + recipient.toLowerCase(), + ); + expect(decoded.amount).toBe(5n); + }); + + it('throws when encoded terms are not exactly 52 bytes', () => { + expect(() => + decodeNativeTokenPaymentTerms(`0x${'00'.repeat(51)}`), + ).toThrow('Invalid NativeTokenPayment terms: must be exactly 52 bytes'); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts b/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts index d65bc1f7..08f74064 100644 --- a/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts @@ -1,313 +1,382 @@ import { isStrictHexString } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { createNativeTokenPeriodTransferTerms } from '../../src/caveats/nativeTokenPeriodTransfer'; - -describe('createNativeTokenPeriodTransferTerms', () => { - const EXPECTED_BYTE_LENGTH = 96; // 32 bytes for each of the 3 parameters - it('creates valid terms for standard parameters', () => { - const periodAmount = 1000000000000000000n; // 1 ETH in wei - const periodDuration = 3600; // 1 hour in seconds - const startDate = 1640995200; // 2022-01-01 00:00:00 UTC - const result = createNativeTokenPeriodTransferTerms({ - periodAmount, - periodDuration, - startDate, - }); - - expect(result).toHaveLength(194); - - const expectedPeriodAmount = - '0000000000000000000000000000000000000000000000000de0b6b3a7640000'; - const expectedPeriodDuration = - '0000000000000000000000000000000000000000000000000000000000000e10'; - const expectedStartDate = - '0000000000000000000000000000000000000000000000000000000061cf9980'; - expect(result).toStrictEqual( - `0x${expectedPeriodAmount}${expectedPeriodDuration}${expectedStartDate}`, - ); - }); - - it('creates valid terms for small values', () => { - const periodAmount = 1n; - const periodDuration = 1; - const startDate = 1; - const result = createNativeTokenPeriodTransferTerms({ - periodAmount, - periodDuration, - startDate, - }); - - expect(result).toStrictEqual( - '0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', - ); - }); - - it('creates valid terms for large values', () => { - const periodAmount = 100000000000000000000n; // 100 ETH in wei - const periodDuration = 86400; // 1 day in seconds - const startDate = 2000000000; // Far future timestamp - const result = createNativeTokenPeriodTransferTerms({ - periodAmount, - periodDuration, - startDate, - }); - - expect(result).toHaveLength(194); - expect(isStrictHexString(result)).toBe(true); - expect(result.length).toBe(194); // Additional length validation - }); - - it('creates valid terms for maximum safe values', () => { - const periodAmount = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; // max uint256 - const periodDuration = Number.MAX_SAFE_INTEGER; - const startDate = Number.MAX_SAFE_INTEGER; - const result = createNativeTokenPeriodTransferTerms({ - periodAmount, - periodDuration, - startDate, - }); - - expect(result).toHaveLength(194); - expect(isStrictHexString(result)).toBe(true); - expect(result.length).toBe(194); // Additional length validation - }); - - it('throws an error for zero period amount', () => { - const periodAmount = 0n; - const periodDuration = 3600; - const startDate = 1640995200; - - expect(() => - createNativeTokenPeriodTransferTerms({ +import { + createNativeTokenPeriodTransferTerms, + decodeNativeTokenPeriodTransferTerms, +} from '../../src/caveats/nativeTokenPeriodTransfer'; + +describe('NativeTokenPeriodTransfer', () => { + describe('createNativeTokenPeriodTransferTerms', () => { + const EXPECTED_BYTE_LENGTH = 96; // 32 bytes for each of the 3 parameters + it('creates valid terms for standard parameters', () => { + const periodAmount = 1000000000000000000n; // 1 ETH in wei + const periodDuration = 3600; // 1 hour in seconds + const startDate = 1640995200; // 2022-01-01 00:00:00 UTC + const result = createNativeTokenPeriodTransferTerms({ periodAmount, periodDuration, startDate, - }), - ).toThrow('Invalid periodAmount: must be a positive number'); - }); - - it('throws an error for negative period amount', () => { - const periodAmount = -1n; - const periodDuration = 3600; - const startDate = 1640995200; + }); + + expect(result).toHaveLength(194); + + const expectedPeriodAmount = + '0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + const expectedPeriodDuration = + '0000000000000000000000000000000000000000000000000000000000000e10'; + const expectedStartDate = + '0000000000000000000000000000000000000000000000000000000061cf9980'; + expect(result).toStrictEqual( + `0x${expectedPeriodAmount}${expectedPeriodDuration}${expectedStartDate}`, + ); + }); - expect(() => - createNativeTokenPeriodTransferTerms({ + it('creates valid terms for small values', () => { + const periodAmount = 1n; + const periodDuration = 1; + const startDate = 1; + const result = createNativeTokenPeriodTransferTerms({ periodAmount, periodDuration, startDate, - }), - ).toThrow('Invalid periodAmount: must be a positive number'); - }); + }); - it('throws an error for zero period duration', () => { - const periodAmount = 1000000000000000000n; - const periodDuration = 0; - const startDate = 1640995200; + expect(result).toStrictEqual( + '0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', + ); + }); - expect(() => - createNativeTokenPeriodTransferTerms({ + it('creates valid terms for large values', () => { + const periodAmount = 100000000000000000000n; // 100 ETH in wei + const periodDuration = 86400; // 1 day in seconds + const startDate = 2000000000; // Far future timestamp + const result = createNativeTokenPeriodTransferTerms({ periodAmount, periodDuration, startDate, - }), - ).toThrow('Invalid periodDuration: must be a positive number'); - }); + }); - it('throws an error for negative period duration', () => { - const periodAmount = 1000000000000000000n; - const periodDuration = -1; - const startDate = 1640995200; + expect(result).toHaveLength(194); + expect(isStrictHexString(result)).toBe(true); + expect(result.length).toBe(194); // Additional length validation + }); - expect(() => - createNativeTokenPeriodTransferTerms({ + it('creates valid terms for maximum safe values', () => { + const periodAmount = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; // max uint256 + const periodDuration = Number.MAX_SAFE_INTEGER; + const startDate = Number.MAX_SAFE_INTEGER; + const result = createNativeTokenPeriodTransferTerms({ periodAmount, periodDuration, startDate, - }), - ).toThrow('Invalid periodDuration: must be a positive number'); - }); + }); - it('throws an error for zero start date', () => { - const periodAmount = 1000000000000000000n; - const periodDuration = 3600; - const startDate = 0; + expect(result).toHaveLength(194); + expect(isStrictHexString(result)).toBe(true); + expect(result.length).toBe(194); // Additional length validation + }); - expect(() => - createNativeTokenPeriodTransferTerms({ - periodAmount, - periodDuration, - startDate, - }), - ).toThrow('Invalid startDate: must be a positive number'); - }); + it('throws an error for zero period amount', () => { + const periodAmount = 0n; + const periodDuration = 3600; + const startDate = 1640995200; - it('throws an error for negative start date', () => { - const periodAmount = 1000000000000000000n; - const periodDuration = 3600; - const startDate = -1; + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount, + periodDuration, + startDate, + }), + ).toThrow('Invalid periodAmount: must be a positive number'); + }); - expect(() => - createNativeTokenPeriodTransferTerms({ - periodAmount, - periodDuration, - startDate, - }), - ).toThrow('Invalid startDate: must be a positive number'); - }); + it('throws an error for negative period amount', () => { + const periodAmount = -1n; + const periodDuration = 3600; + const startDate = 1640995200; - it('throws an error for undefined periodAmount', () => { - expect(() => - createNativeTokenPeriodTransferTerms({ - periodAmount: undefined as any, - periodDuration: 3600, - startDate: 1640995200, - }), - ).toThrow(); - }); + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount, + periodDuration, + startDate, + }), + ).toThrow('Invalid periodAmount: must be a positive number'); + }); - it('throws an error for null periodAmount', () => { - expect(() => - createNativeTokenPeriodTransferTerms({ - periodAmount: null as any, - periodDuration: 3600, - startDate: 1640995200, - }), - ).toThrow(); - }); + it('throws an error for zero period duration', () => { + const periodAmount = 1000000000000000000n; + const periodDuration = 0; + const startDate = 1640995200; - it('throws an error for undefined periodDuration', () => { - expect(() => - createNativeTokenPeriodTransferTerms({ - periodAmount: 1000000000000000000n, - periodDuration: undefined as any, - startDate: 1640995200, - }), - ).toThrow(); - }); + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount, + periodDuration, + startDate, + }), + ).toThrow('Invalid periodDuration: must be a positive number'); + }); - it('throws an error for null periodDuration', () => { - expect(() => - createNativeTokenPeriodTransferTerms({ - periodAmount: 1000000000000000000n, - periodDuration: null as any, - startDate: 1640995200, - }), - ).toThrow(); - }); + it('throws an error for negative period duration', () => { + const periodAmount = 1000000000000000000n; + const periodDuration = -1; + const startDate = 1640995200; - it('throws an error for undefined startDate', () => { - expect(() => - createNativeTokenPeriodTransferTerms({ - periodAmount: 1000000000000000000n, - periodDuration: 3600, - startDate: undefined as any, - }), - ).toThrow(); - }); + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount, + periodDuration, + startDate, + }), + ).toThrow('Invalid periodDuration: must be a positive number'); + }); - it('throws an error for null startDate', () => { - expect(() => - createNativeTokenPeriodTransferTerms({ - periodAmount: 1000000000000000000n, - periodDuration: 3600, - startDate: null as any, - }), - ).toThrow(); - }); + it('throws an error for zero start date', () => { + const periodAmount = 1000000000000000000n; + const periodDuration = 3600; + const startDate = 0; - it('handles edge case with very small period amount', () => { - const periodAmount = 1n; // 1 wei - const periodDuration = 1; - const startDate = 1; - const result = createNativeTokenPeriodTransferTerms({ - periodAmount, - periodDuration, - startDate, + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount, + periodDuration, + startDate, + }), + ).toThrow('Invalid startDate: must be a positive number'); }); - expect(isStrictHexString(result)).toBe(true); - expect(result).toHaveLength(194); - expect(result.length).toBe(194); // Additional length validation - }); + it('throws an error for negative start date', () => { + const periodAmount = 1000000000000000000n; + const periodDuration = 3600; + const startDate = -1; - // Tests for bytes return type - describe('bytes return type', () => { - it('returns Uint8Array when bytes encoding is specified', () => { - const periodAmount = 1000000000000000000n; // 1 ETH in wei - const periodDuration = 3600; // 1 hour in seconds - const startDate = 1640995200; // 2022-01-01 00:00:00 UTC - const result = createNativeTokenPeriodTransferTerms( - { + expect(() => + createNativeTokenPeriodTransferTerms({ periodAmount, periodDuration, startDate, - }, - { out: 'bytes' }, - ); + }), + ).toThrow('Invalid startDate: must be a positive number'); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + it('throws an error for undefined periodAmount', () => { + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount: undefined as any, + periodDuration: 3600, + startDate: 1640995200, + }), + ).toThrow(); }); - it('returns Uint8Array for small values with bytes encoding', () => { - const periodAmount = 1n; + it('throws an error for null periodAmount', () => { + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount: null as any, + periodDuration: 3600, + startDate: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for undefined periodDuration', () => { + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount: 1000000000000000000n, + periodDuration: undefined as any, + startDate: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for null periodDuration', () => { + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount: 1000000000000000000n, + periodDuration: null as any, + startDate: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for undefined startDate', () => { + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount: 1000000000000000000n, + periodDuration: 3600, + startDate: undefined as any, + }), + ).toThrow(); + }); + + it('throws an error for null startDate', () => { + expect(() => + createNativeTokenPeriodTransferTerms({ + periodAmount: 1000000000000000000n, + periodDuration: 3600, + startDate: null as any, + }), + ).toThrow(); + }); + + it('handles edge case with very small period amount', () => { + const periodAmount = 1n; // 1 wei const periodDuration = 1; const startDate = 1; - const result = createNativeTokenPeriodTransferTerms( - { - periodAmount, - periodDuration, - startDate, - }, - { out: 'bytes' }, - ); + const result = createNativeTokenPeriodTransferTerms({ + periodAmount, + periodDuration, + startDate, + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(96); - // Each parameter should be 32 bytes, with the value at the end - const expectedBytes = new Array(96).fill(0); - expectedBytes[31] = 1; // periodAmount = 1 - expectedBytes[63] = 1; // periodDuration = 1 - expectedBytes[95] = 1; // startDate = 1 - expect(Array.from(result)).toEqual(expectedBytes); + expect(isStrictHexString(result)).toBe(true); + expect(result).toHaveLength(194); + expect(result.length).toBe(194); // Additional length validation }); - it('returns Uint8Array for large values with bytes encoding', () => { - const periodAmount = 100000000000000000000n; // 100 ETH in wei - const periodDuration = 86400; // 1 day in seconds - const startDate = 2000000000; // Far future timestamp - const result = createNativeTokenPeriodTransferTerms( - { - periodAmount, - periodDuration, - startDate, - }, - { out: 'bytes' }, - ); + // Tests for bytes return type + describe('bytes return type', () => { + it('returns Uint8Array when bytes encoding is specified', () => { + const periodAmount = 1000000000000000000n; // 1 ETH in wei + const periodDuration = 3600; // 1 hour in seconds + const startDate = 1640995200; // 2022-01-01 00:00:00 UTC + const result = createNativeTokenPeriodTransferTerms( + { + periodAmount, + periodDuration, + startDate, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for small values with bytes encoding', () => { + const periodAmount = 1n; + const periodDuration = 1; + const startDate = 1; + const result = createNativeTokenPeriodTransferTerms( + { + periodAmount, + periodDuration, + startDate, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(96); + // Each parameter should be 32 bytes, with the value at the end + const expectedBytes = new Array(96).fill(0); + expectedBytes[31] = 1; // periodAmount = 1 + expectedBytes[63] = 1; // periodDuration = 1 + expectedBytes[95] = 1; // startDate = 1 + expect(Array.from(result)).toEqual(expectedBytes); + }); + + it('returns Uint8Array for large values with bytes encoding', () => { + const periodAmount = 100000000000000000000n; // 100 ETH in wei + const periodDuration = 86400; // 1 day in seconds + const startDate = 2000000000; // Far future timestamp + const result = createNativeTokenPeriodTransferTerms( + { + periodAmount, + periodDuration, + startDate, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(96); + }); + + it('returns Uint8Array for maximum safe values with bytes encoding', () => { + const periodAmount = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; // max uint256 + const periodDuration = Number.MAX_SAFE_INTEGER; + const startDate = Number.MAX_SAFE_INTEGER; + const result = createNativeTokenPeriodTransferTerms( + { + periodAmount, + periodDuration, + startDate, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(96); + }); + }); + }); + + describe('decodeNativeTokenPeriodTransferTerms', () => { + it('decodes standard parameters', () => { + const original = { + periodAmount: 1000000000000000000n, + periodDuration: 3600, + startDate: 1640995200, + }; + expect( + decodeNativeTokenPeriodTransferTerms( + createNativeTokenPeriodTransferTerms(original), + ), + ).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(96); + it('decodes small values', () => { + const original = { + periodAmount: 1n, + periodDuration: 1, + startDate: 1, + }; + expect( + decodeNativeTokenPeriodTransferTerms( + createNativeTokenPeriodTransferTerms(original), + ), + ).toStrictEqual(original); }); - it('returns Uint8Array for maximum safe values with bytes encoding', () => { - const periodAmount = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; // max uint256 - const periodDuration = Number.MAX_SAFE_INTEGER; - const startDate = Number.MAX_SAFE_INTEGER; - const result = createNativeTokenPeriodTransferTerms( - { - periodAmount, - periodDuration, - startDate, - }, - { out: 'bytes' }, + it('decodes maximum uint256 and max safe integers', () => { + const original = { + periodAmount: + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + periodDuration: Number.MAX_SAFE_INTEGER, + startDate: Number.MAX_SAFE_INTEGER, + }; + expect( + decodeNativeTokenPeriodTransferTerms( + createNativeTokenPeriodTransferTerms(original), + ), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const original = { + periodAmount: 1000000000000000000n, + periodDuration: 3600, + startDate: 1640995200, + }; + const bytes = createNativeTokenPeriodTransferTerms(original, { + out: 'bytes', + }); + expect(decodeNativeTokenPeriodTransferTerms(bytes)).toStrictEqual( + original, ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(96); + it('throws when encoded terms are not exactly 96 bytes', () => { + expect(() => + decodeNativeTokenPeriodTransferTerms(`0x${'00'.repeat(95)}`), + ).toThrow( + 'Invalid NativeTokenPeriodTransfer terms: must be exactly 96 bytes', + ); }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts b/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts index 49ebc22c..0308c02b 100644 --- a/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts @@ -1,569 +1,685 @@ import { describe, it, expect } from 'vitest'; -import { createNativeTokenStreamingTerms } from '../../src/caveats/nativeTokenStreaming'; - -describe('createNativeTokenStreamingTerms', () => { - const EXPECTED_BYTE_LENGTH = 128; // 32 bytes for each of the 4 parameters - it('creates valid terms for standard streaming parameters', () => { - const initialAmount = 1000000000000000000n; // 1 ETH in wei - const maxAmount = 10000000000000000000n; // 10 ETH in wei - const amountPerSecond = 500000000000000000n; // 0.5 ETH per second - const startTime = 1640995200; // 2022-01-01 00:00:00 UTC - - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('creates valid terms for zero initial amount', () => { - const initialAmount = 0n; - const maxAmount = 5000000000000000000n; // 5 ETH in wei - const amountPerSecond = 1000000000000000000n; // 1 ETH per second - const startTime = 1672531200; // 2023-01-01 00:00:00 UTC - - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000063b0cd00', - ); - }); - - it('creates valid terms for equal initial and max amounts', () => { - const initialAmount = 2000000000000000000n; // 2 ETH in wei - const maxAmount = 2000000000000000000n; // 2 ETH in wei - const amountPerSecond = 100000000000000000n; // 0.1 ETH per second - const startTime = 1640995200; - - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('creates valid terms for small values', () => { - const initialAmount = 1n; - const maxAmount = 1000n; - const amountPerSecond = 1n; - const startTime = 1; - - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', - ); - }); - - it('creates valid terms for large values', () => { - const initialAmount = 100000000000000000000n; // 100 ETH - const maxAmount = 1000000000000000000000n; // 1000 ETH - const amountPerSecond = 10000000000000000000n; // 10 ETH per second - const startTime = 2000000000; // Far future - - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000077359400', - ); - }); - - it('creates valid terms for maximum allowed timestamp', () => { - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; - const amountPerSecond = 1000000000000000000n; - const startTime = 253402300799; // January 1, 10000 CE - - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000003afff4417f', - ); - }); - - it('creates valid terms for maximum safe bigint values', () => { - const maxUint256 = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; - const initialAmount = maxUint256; - const maxAmount = maxUint256; - const amountPerSecond = maxUint256; - const startTime = 1640995200; - - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }); - - expect(result).toStrictEqual( - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('throws an error for negative initial amount', () => { - const initialAmount = -1n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; +import { + createNativeTokenStreamingTerms, + decodeNativeTokenStreamingTerms, +} from '../../src/caveats/nativeTokenStreaming'; + +describe('NativeTokenStreaming', () => { + describe('createNativeTokenStreamingTerms', () => { + const EXPECTED_BYTE_LENGTH = 128; // 32 bytes for each of the 4 parameters + it('creates valid terms for standard streaming parameters', () => { + const initialAmount = 1000000000000000000n; // 1 ETH in wei + const maxAmount = 10000000000000000000n; // 10 ETH in wei + const amountPerSecond = 500000000000000000n; // 0.5 ETH per second + const startTime = 1640995200; // 2022-01-01 00:00:00 UTC - expect(() => - createNativeTokenStreamingTerms({ + const result = createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid initialAmount: must be greater than zero'); - }); + }); - it('throws an error for zero max amount', () => { - const initialAmount = 0n; - const maxAmount = 0n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; - - expect(() => - createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }), - ).toThrow('Invalid maxAmount: must be a positive number'); - }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); - it('throws an error for negative max amount', () => { - const initialAmount = 0n; - const maxAmount = -1n; - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + it('creates valid terms for zero initial amount', () => { + const initialAmount = 0n; + const maxAmount = 5000000000000000000n; // 5 ETH in wei + const amountPerSecond = 1000000000000000000n; // 1 ETH per second + const startTime = 1672531200; // 2023-01-01 00:00:00 UTC - expect(() => - createNativeTokenStreamingTerms({ + const result = createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid maxAmount: must be a positive number'); - }); - - it('throws an error when max amount is less than initial amount', () => { - const initialAmount = 1000000000000000000n; // 1 ETH - const maxAmount = 500000000000000000n; // 0.5 ETH - const amountPerSecond = 100000000000000000n; - const startTime = 1640995200; + }); - expect(() => - createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }), - ).toThrow('Invalid maxAmount: must be greater than initialAmount'); - }); + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000063b0cd00', + ); + }); - it('throws an error for zero amount per second', () => { - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 0n; - const startTime = 1640995200; + it('creates valid terms for equal initial and max amounts', () => { + const initialAmount = 2000000000000000000n; // 2 ETH in wei + const maxAmount = 2000000000000000000n; // 2 ETH in wei + const amountPerSecond = 100000000000000000n; // 0.1 ETH per second + const startTime = 1640995200; - expect(() => - createNativeTokenStreamingTerms({ + const result = createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid amountPerSecond: must be a positive number'); - }); + }); - it('throws an error for negative amount per second', () => { - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = -1n; - const startTime = 1640995200; + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); + + it('creates valid terms for small values', () => { + const initialAmount = 1n; + const maxAmount = 1000n; + const amountPerSecond = 1n; + const startTime = 1; - expect(() => - createNativeTokenStreamingTerms({ + const result = createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid amountPerSecond: must be a positive number'); - }); + }); - it('throws an error for zero start time', () => { - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 0; + expect(result).toStrictEqual( + '0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', + ); + }); - expect(() => - createNativeTokenStreamingTerms({ + it('creates valid terms for large values', () => { + const initialAmount = 100000000000000000000n; // 100 ETH + const maxAmount = 1000000000000000000000n; // 1000 ETH + const amountPerSecond = 10000000000000000000n; // 10 ETH per second + const startTime = 2000000000; // Far future + + const result = createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid startTime: must be a positive number'); - }); + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea000000000000000000000000000000000000000000000000000008ac7230489e800000000000000000000000000000000000000000000000000000000000077359400', + ); + }); - it('throws an error for negative start time', () => { - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = -1; + it('creates valid terms for maximum allowed timestamp', () => { + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 1000000000000000000n; + const startTime = 253402300799; // January 1, 10000 CE - expect(() => - createNativeTokenStreamingTerms({ + const result = createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid startTime: must be a positive number'); - }); + }); - it('throws an error for start time exceeding upper bound', () => { - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = 100000000000000000n; - const startTime = 253402300800; // One second past January 1, 10000 CE + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000003afff4417f', + ); + }); - expect(() => - createNativeTokenStreamingTerms({ + it('creates valid terms for maximum safe bigint values', () => { + const maxUint256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + const initialAmount = maxUint256; + const maxAmount = maxUint256; + const amountPerSecond = maxUint256; + const startTime = 1640995200; + + const result = createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }), - ).toThrow('Invalid startTime: must be less than or equal to 253402300799'); - }); - - it('throws an error for undefined initialAmount', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: undefined as any, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow(); - }); + }); - it('throws an error for null initialAmount', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: null as any, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow(); - }); - - it('throws an error for undefined maxAmount', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: 0n, - maxAmount: undefined as any, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow(); - }); - - it('throws an error for null maxAmount', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: 0n, - maxAmount: null as any, - amountPerSecond: 100000000000000000n, - startTime: 1640995200, - }), - ).toThrow(); - }); - - it('throws an error for undefined amountPerSecond', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: undefined as any, - startTime: 1640995200, - }), - ).toThrow(); - }); - - it('throws an error for null amountPerSecond', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: null as any, - startTime: 1640995200, - }), - ).toThrow(); - }); - - it('throws an error for undefined startTime', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: undefined as any, - }), - ).toThrow(); - }); + expect(result).toStrictEqual( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); - it('throws an error for null startTime', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: null as any, - }), - ).toThrow(); - }); + it('throws an error for negative initial amount', () => { + const initialAmount = -1n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - it('throws an error for Infinity startTime', () => { - expect(() => - createNativeTokenStreamingTerms({ - initialAmount: 0n, - maxAmount: 1000000000000000000n, - amountPerSecond: 100000000000000000n, - startTime: Infinity, - }), - ).toThrow(); - }); + expect(() => + createNativeTokenStreamingTerms({ + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }), + ).toThrow('Invalid initialAmount: must be greater than zero'); + }); - it('handles edge case with very large initial amount and small max amount difference', () => { - const initialAmount = 999999999999999999n; - const maxAmount = 1000000000000000000n; // Just 1 wei more - const amountPerSecond = 1n; - const startTime = 1640995200; + it('throws an error for zero max amount', () => { + const initialAmount = 0n; + const maxAmount = 0n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, + expect(() => + createNativeTokenStreamingTerms({ + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }), + ).toThrow('Invalid maxAmount: must be a positive number'); }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000de0b6b3a763ffff0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000061cf9980', - ); - }); - - it('handles streaming with minimum viable parameters', () => { - const initialAmount = 1n; - const maxAmount = 2n; - const amountPerSecond = 1n; - const startTime = 1; + it('throws an error for negative max amount', () => { + const initialAmount = 0n; + const maxAmount = -1n; + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - const result = createNativeTokenStreamingTerms({ - initialAmount, - maxAmount, - amountPerSecond, - startTime, + expect(() => + createNativeTokenStreamingTerms({ + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }), + ).toThrow('Invalid maxAmount: must be a positive number'); }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', - ); - }); + it('throws an error when max amount is less than initial amount', () => { + const initialAmount = 1000000000000000000n; // 1 ETH + const maxAmount = 500000000000000000n; // 0.5 ETH + const amountPerSecond = 100000000000000000n; + const startTime = 1640995200; - // Tests for bytes return type - describe('bytes return type', () => { - it('returns Uint8Array when bytes encoding is specified', () => { - const initialAmount = 1000000000000000000n; // 1 ETH in wei - const maxAmount = 10000000000000000000n; // 10 ETH in wei - const amountPerSecond = 500000000000000000n; // 0.5 ETH per second - const startTime = 1640995200; // 2022-01-01 00:00:00 UTC - const result = createNativeTokenStreamingTerms( - { + expect(() => + createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }), + ).toThrow('Invalid maxAmount: must be greater than initialAmount'); }); - it('returns Uint8Array for zero initial amount with bytes encoding', () => { + it('throws an error for zero amount per second', () => { const initialAmount = 0n; - const maxAmount = 5000000000000000000n; // 5 ETH in wei - const amountPerSecond = 1000000000000000000n; // 1 ETH per second - const startTime = 1672531200; // 2023-01-01 00:00:00 UTC - const result = createNativeTokenStreamingTerms( - { + const maxAmount = 1000000000000000000n; + const amountPerSecond = 0n; + const startTime = 1640995200; + + expect(() => + createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(128); - // First 32 bytes should be zero (initialAmount = 0) - const firstParam = Array.from(result.slice(0, 32)); - expect(firstParam).toEqual(new Array(32).fill(0)); + }), + ).toThrow('Invalid amountPerSecond: must be a positive number'); }); - it('returns Uint8Array for equal initial and max amounts with bytes encoding', () => { - const initialAmount = 2000000000000000000n; // 2 ETH in wei - const maxAmount = 2000000000000000000n; // 2 ETH in wei - const amountPerSecond = 100000000000000000n; // 0.1 ETH per second + it('throws an error for negative amount per second', () => { + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = -1n; const startTime = 1640995200; - const result = createNativeTokenStreamingTerms( - { + + expect(() => + createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(128); + }), + ).toThrow('Invalid amountPerSecond: must be a positive number'); }); - it('returns Uint8Array for small values with bytes encoding', () => { - const initialAmount = 1n; - const maxAmount = 1000n; - const amountPerSecond = 1n; - const startTime = 1; - const result = createNativeTokenStreamingTerms( - { + it('throws an error for zero start time', () => { + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 0; + + expect(() => + createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(128); - // Each parameter should be 32 bytes, with the value at the end - const expectedBytes = new Array(128).fill(0); - expectedBytes[31] = 1; // initialAmount = 1 - expectedBytes[62] = 0x03; // maxAmount = 1000 = 0x03e8 - expectedBytes[63] = 0xe8; - expectedBytes[95] = 1; // amountPerSecond = 1 - expectedBytes[127] = 1; // startTime = 1 - expect(Array.from(result)).toEqual(expectedBytes); + }), + ).toThrow('Invalid startTime: must be a positive number'); }); - it('returns Uint8Array for large values with bytes encoding', () => { - const initialAmount = 100000000000000000000n; // 100 ETH - const maxAmount = 1000000000000000000000n; // 1000 ETH - const amountPerSecond = 10000000000000000000n; // 10 ETH per second - const startTime = 2000000000; // Far future - const result = createNativeTokenStreamingTerms( - { + it('throws an error for negative start time', () => { + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = -1; + + expect(() => + createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(128); + }), + ).toThrow('Invalid startTime: must be a positive number'); }); - it('returns Uint8Array for maximum allowed timestamp with bytes encoding', () => { - const initialAmount = 1000000000000000000n; - const maxAmount = 2000000000000000000n; - const amountPerSecond = 1000000000000000000n; - const startTime = 253402300799; // January 1, 10000 CE - const result = createNativeTokenStreamingTerms( - { + it('throws an error for start time exceeding upper bound', () => { + const initialAmount = 0n; + const maxAmount = 1000000000000000000n; + const amountPerSecond = 100000000000000000n; + const startTime = 253402300800; // One second past January 1, 10000 CE + + expect(() => + createNativeTokenStreamingTerms({ initialAmount, maxAmount, amountPerSecond, startTime, - }, - { out: 'bytes' }, + }), + ).toThrow( + 'Invalid startTime: must be less than or equal to 253402300799', ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(128); + it('throws an error for undefined initialAmount', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: undefined as any, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow(); }); - it('returns Uint8Array for maximum safe bigint values with bytes encoding', () => { - const maxUint256 = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; - const initialAmount = maxUint256; - const maxAmount = maxUint256; - const amountPerSecond = maxUint256; + it('throws an error for null initialAmount', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: null as any, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for undefined maxAmount', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: 0n, + maxAmount: undefined as any, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for null maxAmount', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: 0n, + maxAmount: null as any, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for undefined amountPerSecond', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: undefined as any, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for null amountPerSecond', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: null as any, + startTime: 1640995200, + }), + ).toThrow(); + }); + + it('throws an error for undefined startTime', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: undefined as any, + }), + ).toThrow(); + }); + + it('throws an error for null startTime', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: null as any, + }), + ).toThrow(); + }); + + it('throws an error for Infinity startTime', () => { + expect(() => + createNativeTokenStreamingTerms({ + initialAmount: 0n, + maxAmount: 1000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: Infinity, + }), + ).toThrow(); + }); + + it('handles edge case with very large initial amount and small max amount difference', () => { + const initialAmount = 999999999999999999n; + const maxAmount = 1000000000000000000n; // Just 1 wei more + const amountPerSecond = 1n; const startTime = 1640995200; - const result = createNativeTokenStreamingTerms( - { - initialAmount, - maxAmount, - amountPerSecond, - startTime, - }, - { out: 'bytes' }, + + const result = createNativeTokenStreamingTerms({ + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000de0b6b3a763ffff0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000061cf9980', ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(128); - // First three 32-byte chunks should be all 0xff - expect(Array.from(result.slice(0, 32))).toEqual(new Array(32).fill(0xff)); - expect(Array.from(result.slice(32, 64))).toEqual( - new Array(32).fill(0xff), + it('handles streaming with minimum viable parameters', () => { + const initialAmount = 1n; + const maxAmount = 2n; + const amountPerSecond = 1n; + const startTime = 1; + + const result = createNativeTokenStreamingTerms({ + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001', ); - expect(Array.from(result.slice(64, 96))).toEqual( - new Array(32).fill(0xff), + }); + + // Tests for bytes return type + describe('bytes return type', () => { + it('returns Uint8Array when bytes encoding is specified', () => { + const initialAmount = 1000000000000000000n; // 1 ETH in wei + const maxAmount = 10000000000000000000n; // 10 ETH in wei + const amountPerSecond = 500000000000000000n; // 0.5 ETH per second + const startTime = 1640995200; // 2022-01-01 00:00:00 UTC + const result = createNativeTokenStreamingTerms( + { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for zero initial amount with bytes encoding', () => { + const initialAmount = 0n; + const maxAmount = 5000000000000000000n; // 5 ETH in wei + const amountPerSecond = 1000000000000000000n; // 1 ETH per second + const startTime = 1672531200; // 2023-01-01 00:00:00 UTC + const result = createNativeTokenStreamingTerms( + { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(128); + // First 32 bytes should be zero (initialAmount = 0) + const firstParam = Array.from(result.slice(0, 32)); + expect(firstParam).toEqual(new Array(32).fill(0)); + }); + + it('returns Uint8Array for equal initial and max amounts with bytes encoding', () => { + const initialAmount = 2000000000000000000n; // 2 ETH in wei + const maxAmount = 2000000000000000000n; // 2 ETH in wei + const amountPerSecond = 100000000000000000n; // 0.1 ETH per second + const startTime = 1640995200; + const result = createNativeTokenStreamingTerms( + { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(128); + }); + + it('returns Uint8Array for small values with bytes encoding', () => { + const initialAmount = 1n; + const maxAmount = 1000n; + const amountPerSecond = 1n; + const startTime = 1; + const result = createNativeTokenStreamingTerms( + { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(128); + // Each parameter should be 32 bytes, with the value at the end + const expectedBytes = new Array(128).fill(0); + expectedBytes[31] = 1; // initialAmount = 1 + expectedBytes[62] = 0x03; // maxAmount = 1000 = 0x03e8 + expectedBytes[63] = 0xe8; + expectedBytes[95] = 1; // amountPerSecond = 1 + expectedBytes[127] = 1; // startTime = 1 + expect(Array.from(result)).toEqual(expectedBytes); + }); + + it('returns Uint8Array for large values with bytes encoding', () => { + const initialAmount = 100000000000000000000n; // 100 ETH + const maxAmount = 1000000000000000000000n; // 1000 ETH + const amountPerSecond = 10000000000000000000n; // 10 ETH per second + const startTime = 2000000000; // Far future + const result = createNativeTokenStreamingTerms( + { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(128); + }); + + it('returns Uint8Array for maximum allowed timestamp with bytes encoding', () => { + const initialAmount = 1000000000000000000n; + const maxAmount = 2000000000000000000n; + const amountPerSecond = 1000000000000000000n; + const startTime = 253402300799; // January 1, 10000 CE + const result = createNativeTokenStreamingTerms( + { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(128); + }); + + it('returns Uint8Array for maximum safe bigint values with bytes encoding', () => { + const maxUint256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + const initialAmount = maxUint256; + const maxAmount = maxUint256; + const amountPerSecond = maxUint256; + const startTime = 1640995200; + const result = createNativeTokenStreamingTerms( + { + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(128); + // First three 32-byte chunks should be all 0xff + expect(Array.from(result.slice(0, 32))).toEqual( + new Array(32).fill(0xff), + ); + expect(Array.from(result.slice(32, 64))).toEqual( + new Array(32).fill(0xff), + ); + expect(Array.from(result.slice(64, 96))).toEqual( + new Array(32).fill(0xff), + ); + }); + }); + }); + + describe('decodeNativeTokenStreamingTerms', () => { + it('decodes standard streaming parameters', () => { + const original = { + initialAmount: 1000000000000000000n, + maxAmount: 10000000000000000000n, + amountPerSecond: 500000000000000000n, + startTime: 1640995200, + }; + expect( + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), + ).toStrictEqual(original); + }); + + it('decodes zero initial amount', () => { + const original = { + initialAmount: 0n, + maxAmount: 5000000000000000000n, + amountPerSecond: 1000000000000000000n, + startTime: 1672531200, + }; + expect( + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), + ).toStrictEqual(original); + }); + + it('decodes equal initial and max amounts', () => { + const original = { + initialAmount: 2000000000000000000n, + maxAmount: 2000000000000000000n, + amountPerSecond: 100000000000000000n, + startTime: 1640995200, + }; + expect( + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), + ).toStrictEqual(original); + }); + + it('decodes small values', () => { + const original = { + initialAmount: 1n, + maxAmount: 1000n, + amountPerSecond: 1n, + startTime: 1, + }; + expect( + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), + ).toStrictEqual(original); + }); + + it('decodes maximum allowed timestamp', () => { + const original = { + initialAmount: 1000000000000000000n, + maxAmount: 2000000000000000000n, + amountPerSecond: 1000000000000000000n, + startTime: 253402300799, + }; + expect( + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), + ).toStrictEqual(original); + }); + + it('decodes maximum uint256 amounts', () => { + const maxUint256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + const original = { + initialAmount: maxUint256, + maxAmount: maxUint256, + amountPerSecond: maxUint256, + startTime: 1640995200, + }; + expect( + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), + ).toStrictEqual(original); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const original = { + initialAmount: 1000000000000000000n, + maxAmount: 10000000000000000000n, + amountPerSecond: 500000000000000000n, + startTime: 1640995200, + }; + const bytes = createNativeTokenStreamingTerms(original, { out: 'bytes' }); + expect(decodeNativeTokenStreamingTerms(bytes)).toStrictEqual(original); + }); + + it('throws when encoded terms are not exactly 128 bytes', () => { + expect(() => + decodeNativeTokenStreamingTerms(`0x${'00'.repeat(127)}`), + ).toThrow( + 'Invalid NativeTokenStreaming terms: must be exactly 128 bytes', ); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts index ff8b9039..30568404 100644 --- a/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts @@ -1,37 +1,78 @@ import { describe, it, expect } from 'vitest'; -import { createNativeTokenTransferAmountTerms } from '../../src/caveats/nativeTokenTransferAmount'; +import { + createNativeTokenTransferAmountTerms, + decodeNativeTokenTransferAmountTerms, +} from '../../src/caveats/nativeTokenTransferAmount'; -describe('createNativeTokenTransferAmountTerms', () => { - it('creates valid terms for zero amount', () => { - const result = createNativeTokenTransferAmountTerms({ maxAmount: 0n }); +describe('NativeTokenTransferAmount', () => { + describe('createNativeTokenTransferAmountTerms', () => { + it('creates valid terms for zero amount', () => { + const result = createNativeTokenTransferAmountTerms({ maxAmount: 0n }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000000', - ); - }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); + }); - it('creates valid terms for positive amount', () => { - const result = createNativeTokenTransferAmountTerms({ maxAmount: 100n }); + it('creates valid terms for positive amount', () => { + const result = createNativeTokenTransferAmountTerms({ maxAmount: 100n }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000064', - ); - }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000064', + ); + }); + + it('throws for negative amount', () => { + expect(() => + createNativeTokenTransferAmountTerms({ maxAmount: -1n }), + ).toThrow('Invalid maxAmount: must be zero or positive'); + }); - 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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createNativeTokenTransferAmountTerms( - { maxAmount: 1n }, - { out: 'bytes' }, - ); + describe('decodeNativeTokenTransferAmountTerms', () => { + it('decodes zero amount', () => { + expect( + decodeNativeTokenTransferAmountTerms( + createNativeTokenTransferAmountTerms({ maxAmount: 0n }), + ), + ).toStrictEqual({ maxAmount: 0n }); + }); + + it('decodes positive amount', () => { + expect( + decodeNativeTokenTransferAmountTerms( + createNativeTokenTransferAmountTerms({ maxAmount: 100n }), + ), + ).toStrictEqual({ maxAmount: 100n }); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createNativeTokenTransferAmountTerms( + { maxAmount: 1n }, + { out: 'bytes' }, + ); + expect(decodeNativeTokenTransferAmountTerms(bytes)).toStrictEqual({ + maxAmount: 1n, + }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('throws when encoded terms are not exactly 32 bytes', () => { + expect(() => + decodeNativeTokenTransferAmountTerms(`0x${'00'.repeat(31)}`), + ).toThrow( + 'Invalid NativeTokenTransferAmount terms: must be exactly 32 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nonce.test.ts b/packages/delegation-core/test/caveats/nonce.test.ts index 18815336..839408e3 100644 --- a/packages/delegation-core/test/caveats/nonce.test.ts +++ b/packages/delegation-core/test/caveats/nonce.test.ts @@ -1,152 +1,14 @@ import { describe, it, expect } from 'vitest'; -import { createNonceTerms } from '../../src/caveats/nonce'; +import { createNonceTerms, decodeNonceTerms } from '../../src/caveats/nonce'; import type { Hex } from '../../src/types'; -describe('createNonceTerms', () => { - const EXPECTED_BYTE_LENGTH = 32; // 32 bytes for nonce +describe('Nonce', () => { + describe('createNonceTerms', () => { + const EXPECTED_BYTE_LENGTH = 32; // 32 bytes for nonce - it('creates valid terms for simple nonce', () => { - const nonce = '0x1234567890abcdef'; - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000001234567890abcdef', - ); - }); - - it('creates valid terms for zero nonce', () => { - const nonce = '0x0'; - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000000', - ); - }); - - it('creates valid terms for minimal nonce', () => { - const nonce = '0x1'; - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000001', - ); - }); - - it('creates valid terms for full 32-byte nonce', () => { - const nonce = - '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual(nonce); - }); - - it('creates valid terms for uppercase hex nonce', () => { - const nonce = '0x1234567890ABCDEF'; - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000001234567890ABCDEF', - ); - }); - - it('creates valid terms for mixed case hex nonce', () => { - const nonce = '0x1234567890AbCdEf'; - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000001234567890AbCdEf', - ); - }); - - it('pads shorter hex values with leading zeros', () => { - const nonce = '0xff'; - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual( - '0x00000000000000000000000000000000000000000000000000000000000000ff', - ); - }); - - it('throws an error for empty nonce', () => { - const nonce = '0x'; - - expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must not be empty', - ); - }); - - it('throws an error for undefined nonce', () => { - expect(() => createNonceTerms({ nonce: undefined as any })).toThrow( - 'Value must be a Uint8Array', - ); - }); - - it('throws an error for null nonce', () => { - expect(() => createNonceTerms({ nonce: null as any })).toThrow( - 'Value must be a Uint8Array', - ); - }); - - it('throws an error for hex nonce without 0x prefix', () => { - const nonce = '1234567890abcdef' as any; - - expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: string must have 0x prefix', - ); - }); - - it('throws an error for invalid hex characters', () => { - const nonce = '0x1234567890abcdefg' as any; - - expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must be a valid BytesLike value', - ); - }); - - it('throws an error for non-BytesLike nonce', () => { - const nonce = 123456 as any; - - expect(() => createNonceTerms({ nonce })).toThrow( - 'Value must be a Uint8Array', - ); - }); - - it('throws an error for nonce longer than 32 bytes', () => { - // 33 bytes (66 hex chars + 0x prefix = 68 chars total, which exceeds 66) - const nonce = - '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12' as any; - - expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must be 32 bytes or less in length', - ); - }); - - it('accepts nonce with exactly 32 bytes', () => { - // 32 bytes (64 hex chars + 0x prefix = 66 chars total) - const nonce = - '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual(nonce); - }); - - it('throws an error for string that looks like hex but has odd length', () => { - const nonce = '0x123' as any; - // This should still work as we pad it - const result = createNonceTerms({ nonce }); - - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000123', - ); - }); - - // Tests for Uint8Array inputs - describe('Uint8Array inputs', () => { - it('creates valid terms for simple Uint8Array nonce', () => { - const nonce = new Uint8Array([ - 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, - ]); + it('creates valid terms for simple nonce', () => { + const nonce = '0x1234567890abcdef'; const result = createNonceTerms({ nonce }); expect(result).toStrictEqual( @@ -154,281 +16,481 @@ describe('createNonceTerms', () => { ); }); - it('creates valid terms for single byte Uint8Array', () => { - const nonce = new Uint8Array([0x42]); + it('creates valid terms for zero nonce', () => { + const nonce = '0x0'; const result = createNonceTerms({ nonce }); expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000042', + '0x0000000000000000000000000000000000000000000000000000000000000000', ); }); - it('creates valid terms for full 32-byte Uint8Array', () => { - const nonce = new Uint8Array([ - 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, - 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, - 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, - ]); + it('creates valid terms for minimal nonce', () => { + const nonce = '0x1'; const result = createNonceTerms({ nonce }); expect(result).toStrictEqual( - '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + '0x0000000000000000000000000000000000000000000000000000000000000001', ); }); - it('creates valid terms for zero-filled Uint8Array', () => { - const nonce = new Uint8Array([0x00, 0x00, 0x00, 0x01]); + it('creates valid terms for full 32-byte nonce', () => { + const nonce = + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; const result = createNonceTerms({ nonce }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000001', - ); + expect(result).toStrictEqual(nonce); }); - it('throws an error for empty Uint8Array', () => { - const nonce = new Uint8Array([]); + it('creates valid terms for uppercase hex nonce', () => { + const nonce = '0x1234567890ABCDEF'; + const result = createNonceTerms({ nonce }); - expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: Uint8Array must not be empty', + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000001234567890ABCDEF', ); }); - it('throws an error for Uint8Array longer than 32 bytes', () => { - const nonce = new Uint8Array(33).fill(0x42); // 33 bytes + it('creates valid terms for mixed case hex nonce', () => { + const nonce = '0x1234567890AbCdEf'; + const result = createNonceTerms({ nonce }); - expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must be 32 bytes or less in length', + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000001234567890AbCdEf', ); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const nonce = new Uint8Array([0x12, 0x34, 0x56, 0x78]); - const result = createNonceTerms({ nonce }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Check the last 4 bytes contain our input - const resultArray = Array.from(result); - expect(resultArray.slice(-4)).toEqual([0x12, 0x34, 0x56, 0x78]); - }); - }); - - // Tests for hex string inputs with bytes return type - describe('hex string bytes return type', () => { - it('returns Uint8Array when bytes encoding is specified', () => { - const nonce = '0x1234567890abcdef'; - const result = createNonceTerms({ nonce }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - }); - - it('returns Uint8Array for minimal nonce with bytes encoding', () => { - const nonce = '0x1'; - const result = createNonceTerms({ nonce }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Should be 31 zeros followed by 1 - const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); - expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 1; - expect(Array.from(result)).toEqual(expectedBytes); - }); - - it('returns Uint8Array for zero nonce with bytes encoding', () => { - const nonce = '0x0'; - const result = createNonceTerms({ nonce }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Should be all zeros - const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); - expect(Array.from(result)).toEqual(expectedBytes); - }); - - it('returns Uint8Array for full nonce with bytes encoding', () => { - const nonce = - '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; - const result = createNonceTerms({ nonce }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Convert expected hex to bytes for comparison - const expectedBytes = [ - 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, - 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, - 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, - ]; - expect(Array.from(result)).toEqual(expectedBytes); - }); - - it('returns Uint8Array for padded hex values with bytes encoding', () => { + it('pads shorter hex values with leading zeros', () => { const nonce = '0xff'; - const result = createNonceTerms({ nonce }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Should be 31 zeros followed by 0xff - const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); - expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 0xff; - expect(Array.from(result)).toEqual(expectedBytes); - }); - }); - - // Tests for edge cases and additional validation - describe('edge cases and additional validation', () => { - it('handles mixed case hex strings correctly', () => { - const nonce = '0xaBcDeF123456'; const result = createNonceTerms({ nonce }); expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000aBcDeF123456', + '0x00000000000000000000000000000000000000000000000000000000000000ff', ); }); - it('handles different BytesLike types consistently', () => { - // Same data in different formats should produce same result - const hexNonce = '0x123456789abcdef0'; - const uint8Nonce = new Uint8Array([ - 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, - ]); - - const hexResult = createNonceTerms({ nonce: hexNonce as Hex }); - const uint8Result = createNonceTerms({ nonce: uint8Nonce }); - - expect(hexResult).toStrictEqual(uint8Result); - }); - - it('handles very small values correctly', () => { - const hexNonce = '0x01'; - const uint8Nonce = new Uint8Array([0x01]); - - const hexResult = createNonceTerms({ nonce: hexNonce as Hex }); - const uint8Result = createNonceTerms({ nonce: uint8Nonce }); - - const expected = - '0x0000000000000000000000000000000000000000000000000000000000000001'; - expect(hexResult).toStrictEqual(expected); - expect(uint8Result).toStrictEqual(expected); - }); - - it('handles maximum size values correctly', () => { - const maxBytes = new Array(32).fill(0xff); - const hexNonce = `0x${maxBytes.map((b) => b.toString(16)).join('')}`; - const uint8Nonce = new Uint8Array(maxBytes); - - const hexResult = createNonceTerms({ nonce: hexNonce as Hex }); - const uint8Result = createNonceTerms({ nonce: uint8Nonce }); - - const expected = - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; - expect(hexResult).toStrictEqual(expected); - expect(uint8Result).toStrictEqual(expected); - }); + it('throws an error for empty nonce', () => { + const nonce = '0x'; - it('handles boolean false correctly', () => { - expect(() => createNonceTerms({ nonce: false as any })).toThrow( - 'Value must be a Uint8Array', + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: must not be empty', ); }); - it('handles empty object correctly', () => { - expect(() => createNonceTerms({ nonce: {} as any })).toThrow( + it('throws an error for undefined nonce', () => { + expect(() => createNonceTerms({ nonce: undefined as any })).toThrow( 'Value must be a Uint8Array', ); }); - it('handles empty array correctly', () => { - expect(() => createNonceTerms({ nonce: [] as any })).toThrow( + it('throws an error for null nonce', () => { + expect(() => createNonceTerms({ nonce: null as any })).toThrow( 'Value must be a Uint8Array', ); }); - it('handles string with only 0x prefix correctly', () => { - const nonce = '0x'; + it('throws an error for hex nonce without 0x prefix', () => { + const nonce = '1234567890abcdef' as any; + expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must not be empty', + 'Invalid nonce: string must have 0x prefix', ); }); - it('handles non-hex string correctly', () => { - expect(() => - createNonceTerms({ nonce: 'not-hex-string' as any }), - ).toThrow('Invalid nonce: string must have 0x prefix'); - }); + it('throws an error for invalid hex characters', () => { + const nonce = '0x1234567890abcdefg' as any; - it('handles hex string with invalid characters correctly', () => { - expect(() => createNonceTerms({ nonce: '0x123g' as any })).toThrow( + expect(() => createNonceTerms({ nonce })).toThrow( 'Invalid nonce: must be a valid BytesLike value', ); }); - it('validates specific error message for Uint8Array empty case', () => { - const nonce = new Uint8Array([]); + it('throws an error for non-BytesLike nonce', () => { + const nonce = 123456 as any; + expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: Uint8Array must not be empty', + 'Value must be a Uint8Array', ); }); - it('validates specific error message for oversized input', () => { - const nonce = new Uint8Array(33).fill(0xff); + it('throws an error for nonce longer than 32 bytes', () => { + // 33 bytes (66 hex chars + 0x prefix = 68 chars total, which exceeds 66) + const nonce = + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12' as any; + expect(() => createNonceTerms({ nonce })).toThrow( 'Invalid nonce: must be 32 bytes or less in length', ); }); - it('handles zero byte values correctly', () => { - const nonce = new Uint8Array([0x00]); + it('accepts nonce with exactly 32 bytes', () => { + // 32 bytes (64 hex chars + 0x prefix = 66 chars total) + const nonce = + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; const result = createNonceTerms({ nonce }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000000', - ); + + expect(result).toStrictEqual(nonce); }); - it('handles maximum valid single byte value correctly', () => { - const nonce = new Uint8Array([0xff]); + it('throws an error for string that looks like hex but has odd length', () => { + const nonce = '0x123' as any; + // This should still work as we pad it const result = createNonceTerms({ nonce }); + expect(result).toStrictEqual( - '0x00000000000000000000000000000000000000000000000000000000000000ff', + '0x0000000000000000000000000000000000000000000000000000000000000123', ); }); - it('preserves exact byte order for full-size inputs', () => { - const bytes = Array.from({ length: 32 }, (_, i) => i % 256); - const nonce = new Uint8Array(bytes); - const result = createNonceTerms({ nonce }); + // Tests for Uint8Array inputs + describe('Uint8Array inputs', () => { + it('creates valid terms for simple Uint8Array nonce', () => { + const nonce = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, + ]); + const result = createNonceTerms({ nonce }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000001234567890abcdef', + ); + }); + + it('creates valid terms for single byte Uint8Array', () => { + const nonce = new Uint8Array([0x42]); + const result = createNonceTerms({ nonce }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000042', + ); + }); + + it('creates valid terms for full 32-byte Uint8Array', () => { + const nonce = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, + 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, + 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, + ]); + const result = createNonceTerms({ nonce }); + + expect(result).toStrictEqual( + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + ); + }); + + it('creates valid terms for zero-filled Uint8Array', () => { + const nonce = new Uint8Array([0x00, 0x00, 0x00, 0x01]); + const result = createNonceTerms({ nonce }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + }); + + it('throws an error for empty Uint8Array', () => { + const nonce = new Uint8Array([]); + + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: Uint8Array must not be empty', + ); + }); + + it('throws an error for Uint8Array longer than 32 bytes', () => { + const nonce = new Uint8Array(33).fill(0x42); // 33 bytes + + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: must be 32 bytes or less in length', + ); + }); + + it('returns Uint8Array when bytes encoding is specified', () => { + const nonce = new Uint8Array([0x12, 0x34, 0x56, 0x78]); + const result = createNonceTerms({ nonce }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Check the last 4 bytes contain our input + const resultArray = Array.from(result); + expect(resultArray.slice(-4)).toEqual([0x12, 0x34, 0x56, 0x78]); + }); + }); - // Should preserve exact byte order without padding - const expectedHex = `0x${bytes - .map((b) => b.toString(16).padStart(2, '0')) - .join('')}`; - expect(result).toStrictEqual(expectedHex); + // Tests for hex string inputs with bytes return type + describe('hex string bytes return type', () => { + it('returns Uint8Array when bytes encoding is specified', () => { + const nonce = '0x1234567890abcdef'; + const result = createNonceTerms({ nonce }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for minimal nonce with bytes encoding', () => { + const nonce = '0x1'; + const result = createNonceTerms({ nonce }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Should be 31 zeros followed by 1 + const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); + expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 1; + expect(Array.from(result)).toEqual(expectedBytes); + }); + + it('returns Uint8Array for zero nonce with bytes encoding', () => { + const nonce = '0x0'; + const result = createNonceTerms({ nonce }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Should be all zeros + const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); + expect(Array.from(result)).toEqual(expectedBytes); + }); + + it('returns Uint8Array for full nonce with bytes encoding', () => { + const nonce = + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + const result = createNonceTerms({ nonce }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Convert expected hex to bytes for comparison + const expectedBytes = [ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, + 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, + 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, + ]; + expect(Array.from(result)).toEqual(expectedBytes); + }); + + it('returns Uint8Array for padded hex values with bytes encoding', () => { + const nonce = '0xff'; + const result = createNonceTerms({ nonce }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Should be 31 zeros followed by 0xff + const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); + expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 0xff; + expect(Array.from(result)).toEqual(expectedBytes); + }); }); - it('handles very large hex strings correctly', () => { - // Test exactly at the 32-byte boundary - const maxHex = `0x${'ff'.repeat(32)}`; - const result = createNonceTerms({ nonce: maxHex as Hex }); - expect(result).toStrictEqual(maxHex); + // Tests for edge cases and additional validation + describe('edge cases and additional validation', () => { + it('handles mixed case hex strings correctly', () => { + const nonce = '0xaBcDeF123456'; + const result = createNonceTerms({ nonce }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000aBcDeF123456', + ); + }); + + it('handles different BytesLike types consistently', () => { + // Same data in different formats should produce same result + const hexNonce = '0x123456789abcdef0'; + const uint8Nonce = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, + ]); + + const hexResult = createNonceTerms({ nonce: hexNonce as Hex }); + const uint8Result = createNonceTerms({ nonce: uint8Nonce }); + + expect(hexResult).toStrictEqual(uint8Result); + }); + + it('handles very small values correctly', () => { + const hexNonce = '0x01'; + const uint8Nonce = new Uint8Array([0x01]); + + const hexResult = createNonceTerms({ nonce: hexNonce as Hex }); + const uint8Result = createNonceTerms({ nonce: uint8Nonce }); + + const expected = + '0x0000000000000000000000000000000000000000000000000000000000000001'; + expect(hexResult).toStrictEqual(expected); + expect(uint8Result).toStrictEqual(expected); + }); + + it('handles maximum size values correctly', () => { + const maxBytes = new Array(32).fill(0xff); + const hexNonce = `0x${maxBytes.map((b) => b.toString(16)).join('')}`; + const uint8Nonce = new Uint8Array(maxBytes); + + const hexResult = createNonceTerms({ nonce: hexNonce as Hex }); + const uint8Result = createNonceTerms({ nonce: uint8Nonce }); + + const expected = + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + expect(hexResult).toStrictEqual(expected); + expect(uint8Result).toStrictEqual(expected); + }); + + it('handles boolean false correctly', () => { + expect(() => createNonceTerms({ nonce: false as any })).toThrow( + 'Value must be a Uint8Array', + ); + }); + + it('handles empty object correctly', () => { + expect(() => createNonceTerms({ nonce: {} as any })).toThrow( + 'Value must be a Uint8Array', + ); + }); + + it('handles empty array correctly', () => { + expect(() => createNonceTerms({ nonce: [] as any })).toThrow( + 'Value must be a Uint8Array', + ); + }); + + it('handles string with only 0x prefix correctly', () => { + const nonce = '0x'; + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: must not be empty', + ); + }); + + it('handles non-hex string correctly', () => { + expect(() => + createNonceTerms({ nonce: 'not-hex-string' as any }), + ).toThrow('Invalid nonce: string must have 0x prefix'); + }); + + it('handles hex string with invalid characters correctly', () => { + expect(() => createNonceTerms({ nonce: '0x123g' as any })).toThrow( + 'Invalid nonce: must be a valid BytesLike value', + ); + }); + + it('validates specific error message for Uint8Array empty case', () => { + const nonce = new Uint8Array([]); + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: Uint8Array must not be empty', + ); + }); + + it('validates specific error message for oversized input', () => { + const nonce = new Uint8Array(33).fill(0xff); + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: must be 32 bytes or less in length', + ); + }); + + it('handles zero byte values correctly', () => { + const nonce = new Uint8Array([0x00]); + const result = createNonceTerms({ nonce }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); + }); + + it('handles maximum valid single byte value correctly', () => { + const nonce = new Uint8Array([0xff]); + const result = createNonceTerms({ nonce }); + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000000000000000000000000000ff', + ); + }); + + it('preserves exact byte order for full-size inputs', () => { + const bytes = Array.from({ length: 32 }, (_, i) => i % 256); + const nonce = new Uint8Array(bytes); + const result = createNonceTerms({ nonce }); + + // Should preserve exact byte order without padding + const expectedHex = `0x${bytes + .map((b) => b.toString(16).padStart(2, '0')) + .join('')}`; + expect(result).toStrictEqual(expectedHex); + }); + + it('handles very large hex strings correctly', () => { + // Test exactly at the 32-byte boundary + const maxHex = `0x${'ff'.repeat(32)}`; + const result = createNonceTerms({ nonce: maxHex as Hex }); + expect(result).toStrictEqual(maxHex); + }); + + it('handles odd-length hex strings by padding correctly', () => { + const nonce = '0x123'; + const result = createNonceTerms({ nonce }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000123', + ); + }); + + it('distinguishes between empty nonce and invalid hex characters', () => { + // Empty nonce gets specific "must not be empty" error + expect(() => createNonceTerms({ nonce: '0x' })).toThrow( + 'Invalid nonce: must not be empty', + ); + + // Invalid hex characters get "must be a valid BytesLike value" error + expect(() => createNonceTerms({ nonce: '0x123g' as any })).toThrow( + 'Invalid nonce: must be a valid BytesLike value', + ); + }); }); + }); - it('handles odd-length hex strings by padding correctly', () => { - const nonce = '0x123'; - const result = createNonceTerms({ nonce }); - expect(result).toStrictEqual( + describe('decodeNonceTerms', () => { + it('returns the padded hex nonce as BytesLike', () => { + const encoded = createNonceTerms({ nonce: '0x1234567890abcdef' }); + expect(decodeNonceTerms(encoded).nonce).toStrictEqual(encoded); + }); + + it('decodes minimal and zero-equivalent hex forms', () => { + expect( + decodeNonceTerms(createNonceTerms({ nonce: '0x1' })).nonce, + ).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + expect( + decodeNonceTerms(createNonceTerms({ nonce: '0x0' })).nonce, + ).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); + }); + + it('decodes full 32-byte nonce unchanged', () => { + const nonce = + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + const encoded = createNonceTerms({ nonce }); + expect(decodeNonceTerms(encoded).nonce).toStrictEqual(encoded); + }); + + it('decodes padded short nonce (0xff)', () => { + const encoded = createNonceTerms({ nonce: '0xff' }); + expect(decodeNonceTerms(encoded).nonce).toStrictEqual( + '0x00000000000000000000000000000000000000000000000000000000000000ff', + ); + }); + + it('decodes odd-length hex after encoder padding', () => { + const encoded = createNonceTerms({ nonce: '0x123' }); + expect(decodeNonceTerms(encoded).nonce).toStrictEqual( '0x0000000000000000000000000000000000000000000000000000000000000123', ); }); - it('distinguishes between empty nonce and invalid hex characters', () => { - // Empty nonce gets specific "must not be empty" error - expect(() => createNonceTerms({ nonce: '0x' })).toThrow( - 'Invalid nonce: must not be empty', + it('decodes terms from Uint8Array nonce input', () => { + const nonce = new Uint8Array([0x12, 0x34, 0x56, 0x78]); + const encoded = createNonceTerms({ nonce }); + expect(decodeNonceTerms(encoded).nonce).toStrictEqual(encoded); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createNonceTerms({ nonce: '0xabcd' }, { out: 'bytes' }); + expect(decodeNonceTerms(bytes).nonce).toStrictEqual( + '0x000000000000000000000000000000000000000000000000000000000000abcd', ); + }); - // Invalid hex characters get "must be a valid BytesLike value" error - expect(() => createNonceTerms({ nonce: '0x123g' as any })).toThrow( - 'Invalid nonce: must be a valid BytesLike value', + it('throws when encoded terms are not exactly 32 bytes', () => { + expect(() => decodeNonceTerms(`0x${'00'.repeat(31)}`)).toThrow( + 'Invalid Nonce terms: must be exactly 32 bytes', ); }); }); diff --git a/packages/delegation-core/test/caveats/ownershipTransfer.test.ts b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts index 61305b75..bbcd7c5e 100644 --- a/packages/delegation-core/test/caveats/ownershipTransfer.test.ts +++ b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts @@ -1,29 +1,63 @@ import { describe, it, expect } from 'vitest'; -import { createOwnershipTransferTerms } from '../../src/caveats/ownershipTransfer'; +import { + createOwnershipTransferTerms, + decodeOwnershipTransferTerms, +} from '../../src/caveats/ownershipTransfer'; -describe('createOwnershipTransferTerms', () => { - const contractAddress = '0x00000000000000000000000000000000000000ff'; +describe('OwnershipTransfer', () => { + describe('createOwnershipTransferTerms', () => { + const contractAddress = '0x00000000000000000000000000000000000000ff'; - it('creates valid terms for contract address', () => { - const result = createOwnershipTransferTerms({ contractAddress }); + it('creates valid terms for contract address', () => { + const result = createOwnershipTransferTerms({ contractAddress }); - expect(result).toStrictEqual(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' }, + ); - it('throws for invalid contract address', () => { - expect(() => - createOwnershipTransferTerms({ contractAddress: '0x1234' }), - ).toThrow('Invalid contractAddress: must be a valid address'); + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(20); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createOwnershipTransferTerms( - { contractAddress }, - { out: 'bytes' }, - ); + describe('decodeOwnershipTransferTerms', () => { + const contractAddress = + '0x00000000000000000000000000000000000000ff' as `0x${string}`; + + it('decodes contract address', () => { + expect( + decodeOwnershipTransferTerms( + createOwnershipTransferTerms({ contractAddress }), + ), + ).toStrictEqual({ contractAddress }); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createOwnershipTransferTerms( + { contractAddress }, + { out: 'bytes' }, + ); + expect(decodeOwnershipTransferTerms(bytes)).toStrictEqual({ + contractAddress, + }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(20); + it('throws when encoded terms are not exactly 20 bytes', () => { + expect(() => + decodeOwnershipTransferTerms(`0x${'00'.repeat(19)}`), + ).toThrow('Invalid OwnershipTransfer terms: must be exactly 20 bytes'); + }); }); }); diff --git a/packages/delegation-core/test/caveats/redeemer.test.ts b/packages/delegation-core/test/caveats/redeemer.test.ts index 2d598c31..4b8efc00 100644 --- a/packages/delegation-core/test/caveats/redeemer.test.ts +++ b/packages/delegation-core/test/caveats/redeemer.test.ts @@ -1,44 +1,84 @@ import { describe, it, expect } from 'vitest'; -import { createRedeemerTerms } from '../../src/caveats/redeemer'; +import { + createRedeemerTerms, + decodeRedeemerTerms, +} from '../../src/caveats/redeemer'; -describe('createRedeemerTerms', () => { - const redeemerA = '0x0000000000000000000000000000000000000001'; - const redeemerB = '0x0000000000000000000000000000000000000002'; +describe('Redeemer', () => { + describe('createRedeemerTerms', () => { + const redeemerA = '0x0000000000000000000000000000000000000001'; + const redeemerB = '0x0000000000000000000000000000000000000002'; - it('creates valid terms for redeemers', () => { - const result = createRedeemerTerms({ redeemers: [redeemerA, redeemerB] }); + it('creates valid terms for redeemers', () => { + const result = createRedeemerTerms({ redeemers: [redeemerA, redeemerB] }); - expect(result).toStrictEqual( - '0x00000000000000000000000000000000000000010000000000000000000000000000000000000002', - ); - }); + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000010000000000000000000000000000000000000002', + ); + }); - it('throws when redeemers is undefined', () => { - expect(() => - createRedeemerTerms({} as Parameters[0]), - ).toThrow('Invalid redeemers: must specify at least one redeemer address'); - }); + 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', - ); - }); + 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('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); + }); }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createRedeemerTerms( - { redeemers: [redeemerA, redeemerB] }, - { out: 'bytes' }, - ); + describe('decodeRedeemerTerms', () => { + const redeemerA = + '0x0000000000000000000000000000000000000001' as `0x${string}`; + const redeemerB = + '0x0000000000000000000000000000000000000002' as `0x${string}`; + + it('decodes multiple redeemers', () => { + const original = { redeemers: [redeemerA, redeemerB] }; + expect(decodeRedeemerTerms(createRedeemerTerms(original))).toStrictEqual( + original, + ); + }); + + it('decodes a single redeemer', () => { + const original = { redeemers: [redeemerA] }; + expect(decodeRedeemerTerms(createRedeemerTerms(original))).toStrictEqual( + original, + ); + }); + + it('accepts Uint8Array terms from the encoder', () => { + const original = { redeemers: [redeemerA, redeemerB] }; + const bytes = createRedeemerTerms(original, { out: 'bytes' }); + expect(decodeRedeemerTerms(bytes)).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(40); + it('throws when encoded terms length is not a multiple of 20 bytes', () => { + expect(() => decodeRedeemerTerms(`0x${'00'.repeat(19)}`)).toThrow( + 'Invalid redeemers: must be a multiple of 20', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts index 5543432b..a619a8e2 100644 --- a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts +++ b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts @@ -1,79 +1,132 @@ import { describe, it, expect } from 'vitest'; -import { createSpecificActionERC20TransferBatchTerms } from '../../src/caveats/specificActionERC20TransferBatch'; +import { + createSpecificActionERC20TransferBatchTerms, + decodeSpecificActionERC20TransferBatchTerms, +} from '../../src/caveats/specificActionERC20TransferBatch'; -describe('createSpecificActionERC20TransferBatchTerms', () => { - const tokenAddress = '0x0000000000000000000000000000000000000011'; - const recipient = '0x0000000000000000000000000000000000000022'; - const target = '0x0000000000000000000000000000000000000033'; +describe('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', + it('creates valid terms for specific action batch', () => { + const result = createSpecificActionERC20TransferBatchTerms({ + tokenAddress, recipient, amount: 1n, target, - calldata: '0x', - }), - ).toThrow('Invalid tokenAddress: must be a valid address'); - }); + calldata: '0x1234', + }); - it('throws for invalid amount', () => { - expect(() => - createSpecificActionERC20TransferBatchTerms({ - tokenAddress, - recipient, - amount: 0n, - target, - calldata: '0x', - }), - ).toThrow('Invalid amount: must be a positive number'); + 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('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( + { + tokenAddress, + recipient, + amount: 2n, + target, + calldata: '0x1234', + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(94); + }); }); - it('throws when calldata string does not start with 0x', () => { - expect(() => - createSpecificActionERC20TransferBatchTerms({ + describe('decodeSpecificActionERC20TransferBatchTerms', () => { + const tokenAddress = + '0x0000000000000000000000000000000000000011' as `0x${string}`; + const recipient = + '0x0000000000000000000000000000000000000022' as `0x${string}`; + const target = + '0x0000000000000000000000000000000000000033' as `0x${string}`; + + it('decodes all fields', () => { + const original = { tokenAddress, recipient, amount: 1n, target, - calldata: '1234' as `0x${string}`, - }), - ).toThrow('Invalid calldata: must be a hex string starting with 0x'); - }); + calldata: '0x1234' as `0x${string}`, + }; + expect( + decodeSpecificActionERC20TransferBatchTerms( + createSpecificActionERC20TransferBatchTerms(original), + ), + ).toStrictEqual(original); + }); - it('returns Uint8Array when bytes encoding is specified', () => { - const result = createSpecificActionERC20TransferBatchTerms( - { + it('accepts Uint8Array terms from the encoder', () => { + const original = { tokenAddress, recipient, amount: 2n, target, - calldata: '0x1234', - }, - { out: 'bytes' }, - ); + calldata: '0x1234' as `0x${string}`, + }; + const bytes = createSpecificActionERC20TransferBatchTerms(original, { + out: 'bytes', + }); + expect(decodeSpecificActionERC20TransferBatchTerms(bytes)).toStrictEqual( + original, + ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(94); + it('throws when encoded terms are shorter than 92 bytes', () => { + expect(() => + decodeSpecificActionERC20TransferBatchTerms(`0x${'00'.repeat(91)}`), + ).toThrow( + 'Invalid SpecificActionERC20TransferBatch terms: must be at least 92 bytes', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/timestamp.test.ts b/packages/delegation-core/test/caveats/timestamp.test.ts index 6c286dd5..e1f69490 100644 --- a/packages/delegation-core/test/caveats/timestamp.test.ts +++ b/packages/delegation-core/test/caveats/timestamp.test.ts @@ -1,299 +1,382 @@ import { describe, it, expect } from 'vitest'; -import { createTimestampTerms } from '../../src/caveats/timestamp'; - -describe('createTimestampTerms', () => { - const EXPECTED_BYTE_LENGTH = 32; // 16 bytes for each timestamp (2 timestamps) - it('creates valid terms for valid timestamp range', () => { - const timestampAfterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC - const timestampBeforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, +import { + createTimestampTerms, + decodeTimestampTerms, +} from '../../src/caveats/timestamp'; + +describe('Timestamp', () => { + describe('createTimestampTerms', () => { + const EXPECTED_BYTE_LENGTH = 32; // 16 bytes for each timestamp (2 timestamps) + it('creates valid terms for valid timestamp range', () => { + const afterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC + const beforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const result = createTimestampTerms({ + afterThreshold, + beforeThreshold, + }); + + expect(result).toStrictEqual( + '0x00000000000000000000000061cf998000000000000000000000000063b0cd00', + ); }); - expect(result).toStrictEqual( - '0x00000000000000000000000061cf998000000000000000000000000063b0cd00', - ); - }); + it('creates valid terms for zero thresholds', () => { + const afterThreshold = 0; + const beforeThreshold = 0; + const result = createTimestampTerms({ + afterThreshold, + beforeThreshold, + }); - it('creates valid terms for zero thresholds', () => { - const timestampAfterThreshold = 0; - const timestampBeforeThreshold = 0; - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000000', - ); - }); + it('creates valid terms when only after threshold is set', () => { + const afterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC + const beforeThreshold = 0; + const result = createTimestampTerms({ + afterThreshold, + beforeThreshold, + }); - it('creates valid terms when only after threshold is set', () => { - const timestampAfterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC - const timestampBeforeThreshold = 0; - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + expect(result).toStrictEqual( + '0x00000000000000000000000061cf998000000000000000000000000000000000', + ); }); - expect(result).toStrictEqual( - '0x00000000000000000000000061cf998000000000000000000000000000000000', - ); - }); + it('creates valid terms when only before threshold is set', () => { + const afterThreshold = 0; + const beforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const result = createTimestampTerms({ + afterThreshold, + beforeThreshold, + }); - it('creates valid terms when only before threshold is set', () => { - const timestampAfterThreshold = 0; - const timestampBeforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000063b0cd00', + ); }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000063b0cd00', - ); - }); + it('creates valid terms for small timestamp values', () => { + const afterThreshold = 1; + const beforeThreshold = 2; + const result = createTimestampTerms({ + afterThreshold, + beforeThreshold, + }); - it('creates valid terms for small timestamp values', () => { - const timestampAfterThreshold = 1; - const timestampBeforeThreshold = 2; - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + expect(result).toStrictEqual( + '0x0000000000000000000000000000000100000000000000000000000000000002', + ); }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000100000000000000000000000000000002', - ); - }); + it('creates valid terms for maximum allowed timestamps', () => { + const maxTimestamp = 253402300799; // January 1, 10000 CE + const result = createTimestampTerms({ + afterThreshold: maxTimestamp, + beforeThreshold: 0, + }); - it('creates valid terms for maximum allowed timestamps', () => { - const maxTimestamp = 253402300799; // January 1, 10000 CE - const result = createTimestampTerms({ - timestampAfterThreshold: maxTimestamp, - timestampBeforeThreshold: 0, + expect(result).toStrictEqual( + '0x00000000000000000000003afff4417f00000000000000000000000000000000', + ); }); - expect(result).toStrictEqual( - '0x00000000000000000000003afff4417f00000000000000000000000000000000', - ); - }); - - it('throws an error for negative after threshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: -1, - timestampBeforeThreshold: 0, - }), - ).toThrow('Invalid timestampAfterThreshold: must be zero or positive'); - }); + it('throws an error for negative after threshold', () => { + expect(() => + createTimestampTerms({ + afterThreshold: -1, + beforeThreshold: 0, + }), + ).toThrow('Invalid afterThreshold: must be zero or positive'); + }); - it('throws an error for negative before threshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: -1, - }), - ).toThrow('Invalid timestampBeforeThreshold: must be zero or positive'); - }); + it('throws an error for negative before threshold', () => { + expect(() => + createTimestampTerms({ + afterThreshold: 0, + beforeThreshold: -1, + }), + ).toThrow('Invalid beforeThreshold: must be zero or positive'); + }); - it('throws an error for before threshold exceeding upper bound', () => { - const overBound = 253402300800; // One second past January 1, 10000 CE - expect(() => - createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: overBound, - }), - ).toThrow( - 'Invalid timestampBeforeThreshold: must be less than or equal to 253402300799', - ); - }); + it('throws an error for before threshold exceeding upper bound', () => { + const overBound = 253402300800; // One second past January 1, 10000 CE + expect(() => + createTimestampTerms({ + afterThreshold: 0, + beforeThreshold: overBound, + }), + ).toThrow( + 'Invalid beforeThreshold: must be less than or equal to 253402300799', + ); + }); - it('throws an error for after threshold exceeding upper bound', () => { - const overBound = 253402300800; // One second past January 1, 10000 CE - expect(() => - createTimestampTerms({ - timestampAfterThreshold: overBound, - timestampBeforeThreshold: 0, - }), - ).toThrow( - 'Invalid timestampAfterThreshold: must be less than or equal to 253402300799', - ); - }); + it('throws an error for after threshold exceeding upper bound', () => { + const overBound = 253402300800; // One second past January 1, 10000 CE + expect(() => + createTimestampTerms({ + afterThreshold: overBound, + beforeThreshold: 0, + }), + ).toThrow( + 'Invalid afterThreshold: must be less than or equal to 253402300799', + ); + }); - it('throws an error when after threshold equals before threshold', () => { - const timestamp = 1640995200; - expect(() => - createTimestampTerms({ - timestampAfterThreshold: timestamp, - timestampBeforeThreshold: timestamp, - }), - ).toThrow( - 'Invalid thresholds: timestampBeforeThreshold must be greater than timestampAfterThreshold when both are specified', - ); - }); + it('throws an error when after threshold equals before threshold', () => { + const timestamp = 1640995200; + expect(() => + createTimestampTerms({ + afterThreshold: timestamp, + beforeThreshold: timestamp, + }), + ).toThrow( + 'Invalid thresholds: beforeThreshold must be greater than afterThreshold when both are specified', + ); + }); - it('throws an error when after threshold is greater than before threshold', () => { - const timestampAfterThreshold = 1672531200; // 2023-01-01 00:00:00 UTC - const timestampBeforeThreshold = 1640995200; // 2022-01-01 00:00:00 UTC - expect(() => - createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, - }), - ).toThrow( - 'Invalid thresholds: timestampBeforeThreshold must be greater than timestampAfterThreshold when both are specified', - ); - }); + it('throws an error when after threshold is greater than before threshold', () => { + const afterThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const beforeThreshold = 1640995200; // 2022-01-01 00:00:00 UTC + expect(() => + createTimestampTerms({ + afterThreshold, + beforeThreshold, + }), + ).toThrow( + 'Invalid thresholds: beforeThreshold must be greater than afterThreshold when both are specified', + ); + }); - it('throws an error for undefined timestampAfterThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: undefined as any, - timestampBeforeThreshold: 0, - }), - ).toThrow(); - }); + it('throws an error for undefined afterThreshold', () => { + expect(() => + createTimestampTerms({ + afterThreshold: undefined as any, + beforeThreshold: 0, + }), + ).toThrow(); + }); - it('throws an error for null timestampAfterThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: null as any, - timestampBeforeThreshold: 0, - }), - ).toThrow(); - }); + it('throws an error for null afterThreshold', () => { + expect(() => + createTimestampTerms({ + afterThreshold: null as any, + beforeThreshold: 0, + }), + ).toThrow(); + }); - it('throws an error for undefined timestampBeforeThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: undefined as any, - }), - ).toThrow(); - }); + it('throws an error for undefined beforeThreshold', () => { + expect(() => + createTimestampTerms({ + afterThreshold: 0, + beforeThreshold: undefined as any, + }), + ).toThrow(); + }); - it('throws an error for null timestampBeforeThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: null as any, - }), - ).toThrow(); - }); + it('throws an error for null beforeThreshold', () => { + expect(() => + createTimestampTerms({ + afterThreshold: 0, + beforeThreshold: null as any, + }), + ).toThrow(); + }); - it('throws an error for Infinity timestampAfterThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: Infinity, - timestampBeforeThreshold: 0, - }), - ).toThrow(); - }); + it('throws an error for Infinity afterThreshold', () => { + expect(() => + createTimestampTerms({ + afterThreshold: Infinity, + beforeThreshold: 0, + }), + ).toThrow(); + }); - it('throws an error for Infinity timestampBeforeThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: Infinity, - }), - ).toThrow(); - }); + it('throws an error for Infinity beforeThreshold', () => { + expect(() => + createTimestampTerms({ + afterThreshold: 0, + beforeThreshold: Infinity, + }), + ).toThrow(); + }); - it('allows after threshold greater than before threshold when before is 0', () => { - const timestampAfterThreshold = 1672531200; // 2023-01-01 00:00:00 UTC - const timestampBeforeThreshold = 0; + it('allows after threshold greater than before threshold when before is 0', () => { + const afterThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const beforeThreshold = 0; + + // Should not throw + const result = createTimestampTerms({ + afterThreshold, + beforeThreshold, + }); + expect(result).toStrictEqual( + '0x00000000000000000000000063b0cd0000000000000000000000000000000000', + ); + }); - // Should not throw - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + // Tests for bytes return type + describe('bytes return type', () => { + it('returns Uint8Array when bytes encoding is specified', () => { + const afterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC + const beforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const result = createTimestampTerms( + { + afterThreshold, + beforeThreshold, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for zero thresholds with bytes encoding', () => { + const afterThreshold = 0; + const beforeThreshold = 0; + const result = createTimestampTerms( + { + afterThreshold, + beforeThreshold, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + expect(Array.from(result)).toEqual( + new Array(EXPECTED_BYTE_LENGTH).fill(0), + ); + }); + + it('returns Uint8Array for single timestamp with bytes encoding', () => { + const afterThreshold = 1640995200; + const beforeThreshold = 1672531200; + const result = createTimestampTerms( + { + afterThreshold, + beforeThreshold, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32); + + // 1640995200 == 0x61cf9980 + const afterThresholdBytes = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x61, 0xcf, 0x99, 0x80, + ]; + // 1672531200 == 0x63b0cd00 + const beforeThresholdBytes = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x63, 0xb0, 0xcd, 0x00, + ]; + const expectedButes = new Uint8Array([ + ...afterThresholdBytes, + ...beforeThresholdBytes, + ]); + expect(result).toEqual(expectedButes); + }); + + it('returns Uint8Array for maximum allowed timestamp with bytes encoding', () => { + const maxTimestamp = 253402300799; // January 1, 10000 CE + const result = createTimestampTerms( + { + afterThreshold: maxTimestamp, + beforeThreshold: 0, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32); + }); }); - expect(result).toStrictEqual( - '0x00000000000000000000000063b0cd0000000000000000000000000000000000', - ); }); - // Tests for bytes return type - describe('bytes return type', () => { - it('returns Uint8Array when bytes encoding is specified', () => { - const timestampAfterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC - const timestampBeforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC - const result = createTimestampTerms( - { - timestampAfterThreshold, - timestampBeforeThreshold, - }, - { out: 'bytes' }, - ); + describe('decodeTimestampTerms', () => { + it('decodes a valid range', () => { + const original = { + afterThreshold: 1640995200, + beforeThreshold: 1672531200, + }; + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + it('decodes both thresholds zero', () => { + const original = { + afterThreshold: 0, + beforeThreshold: 0, + }; + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); - it('returns Uint8Array for zero thresholds with bytes encoding', () => { - const timestampAfterThreshold = 0; - const timestampBeforeThreshold = 0; - const result = createTimestampTerms( - { - timestampAfterThreshold, - timestampBeforeThreshold, - }, - { out: 'bytes' }, - ); + it('decodes only after threshold set', () => { + const original = { + afterThreshold: 1640995200, + beforeThreshold: 0, + }; + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - expect(Array.from(result)).toEqual( - new Array(EXPECTED_BYTE_LENGTH).fill(0), - ); + it('decodes only before threshold set', () => { + const original = { + afterThreshold: 0, + beforeThreshold: 1672531200, + }; + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); - it('returns Uint8Array for single timestamp with bytes encoding', () => { - const timestampAfterThreshold = 1640995200; - const timestampBeforeThreshold = 1672531200; - const result = createTimestampTerms( - { - timestampAfterThreshold, - timestampBeforeThreshold, - }, - { out: 'bytes' }, - ); + it('decodes small positive timestamps', () => { + const original = { + afterThreshold: 1, + beforeThreshold: 2, + }; + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); - - // 1640995200 == 0x61cf9980 - const afterThresholdBytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x61, 0xcf, 0x99, 0x80, - ]; - // 1672531200 == 0x63b0cd00 - const beforeThresholdBytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x63, 0xb0, 0xcd, 0x00, - ]; - const expectedButes = new Uint8Array([ - ...afterThresholdBytes, - ...beforeThresholdBytes, - ]); - expect(result).toEqual(expectedButes); + it('decodes maximum allowed before threshold', () => { + const maxTimestamp = 253402300799; + const original = { + afterThreshold: 0, + beforeThreshold: maxTimestamp, + }; + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); - it('returns Uint8Array for maximum allowed timestamp with bytes encoding', () => { - const maxTimestamp = 253402300799; // January 1, 10000 CE - const result = createTimestampTerms( - { - timestampAfterThreshold: maxTimestamp, - timestampBeforeThreshold: 0, - }, - { out: 'bytes' }, - ); + it('accepts Uint8Array terms from the encoder', () => { + const original = { + afterThreshold: 1640995200, + beforeThreshold: 1672531200, + }; + const bytes = createTimestampTerms(original, { out: 'bytes' }); + expect(decodeTimestampTerms(bytes)).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('throws when encoded terms are not exactly 32 bytes', () => { + expect(() => decodeTimestampTerms(`0x${'00'.repeat(31)}`)).toThrow( + 'Invalid Timestamp terms: must be exactly 32 bytes', + ); }); }); }); diff --git a/packages/delegation-core/test/caveats/valueLte.test.ts b/packages/delegation-core/test/caveats/valueLte.test.ts index 8d41d01b..7981e313 100644 --- a/packages/delegation-core/test/caveats/valueLte.test.ts +++ b/packages/delegation-core/test/caveats/valueLte.test.ts @@ -1,134 +1,197 @@ import { describe, it, expect } from 'vitest'; -import { createValueLteTerms } from '../../src/caveats/valueLte'; +import { + createValueLteTerms, + decodeValueLteTerms, +} from '../../src/caveats/valueLte'; + +describe('ValueLte', () => { + describe('createValueLteTerms', () => { + const EXPECTED_BYTE_LENGTH = 32; // 32 bytes for maxValue + it('creates valid terms for positive values', () => { + const maxValue = 1000000000000000000n; // 1 ETH in wei + const result = createValueLteTerms({ maxValue }); -describe('createValueLteTerms', () => { - const EXPECTED_BYTE_LENGTH = 32; // 32 bytes for maxValue - it('creates valid terms for positive values', () => { - const maxValue = 1000000000000000000n; // 1 ETH in wei - const result = createValueLteTerms({ maxValue }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000', + ); + }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000', - ); - }); + it('creates valid terms for zero value', () => { + const maxValue = 0n; + const result = createValueLteTerms({ maxValue }); - it('creates valid terms for zero value', () => { - const maxValue = 0n; - const result = createValueLteTerms({ maxValue }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); + }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000000', - ); - }); + it('creates valid terms for a small value', () => { + const maxValue = 1n; + const result = createValueLteTerms({ maxValue }); - it('creates valid terms for a small value', () => { - const maxValue = 1n; - const result = createValueLteTerms({ maxValue }); + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000001', + ); + }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000001', - ); - }); + it('creates valid terms for a large value', () => { + const maxValue = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; // max uint256 + const result = createValueLteTerms({ maxValue }); - it('creates valid terms for a large value', () => { - const maxValue = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; // max uint256 - const result = createValueLteTerms({ maxValue }); + expect(result).toStrictEqual( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + ); + }); - expect(result).toStrictEqual( - '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', - ); - }); + it('pads smaller hex values with leading zeros', () => { + const maxValue = 255n; // 0xff + const result = createValueLteTerms({ maxValue }); - it('pads smaller hex values with leading zeros', () => { - const maxValue = 255n; // 0xff - const result = createValueLteTerms({ maxValue }); + expect(result).toStrictEqual( + '0x00000000000000000000000000000000000000000000000000000000000000ff', + ); + }); - expect(result).toStrictEqual( - '0x00000000000000000000000000000000000000000000000000000000000000ff', - ); - }); + it('throws an error for negative values', () => { + const maxValue = -1n; - it('throws an error for negative values', () => { - const maxValue = -1n; + expect(() => createValueLteTerms({ maxValue })).toThrow( + 'Invalid maxValue: must be greater than or equal to zero', + ); + }); - expect(() => createValueLteTerms({ maxValue })).toThrow( - 'Invalid maxValue: must be greater than or equal to zero', - ); - }); + it('throws an error for large negative values', () => { + const maxValue = -1000000000000000000n; - it('throws an error for large negative values', () => { - const maxValue = -1000000000000000000n; + expect(() => createValueLteTerms({ maxValue })).toThrow( + 'Invalid maxValue: must be greater than or equal to zero', + ); + }); - expect(() => createValueLteTerms({ maxValue })).toThrow( - 'Invalid maxValue: must be greater than or equal to zero', - ); - }); + it('throws an error for undefined maxValue', () => { + expect(() => + createValueLteTerms({ maxValue: undefined as any }), + ).toThrow(); + }); - it('throws an error for undefined maxValue', () => { - expect(() => createValueLteTerms({ maxValue: undefined as any })).toThrow(); - }); + it('throws an error for null maxValue', () => { + expect(() => createValueLteTerms({ maxValue: null as any })).toThrow(); + }); - it('throws an error for null maxValue', () => { - expect(() => createValueLteTerms({ maxValue: null as any })).toThrow(); + // Tests for bytes return type + describe('bytes return type', () => { + it('returns Uint8Array when bytes encoding is specified', () => { + const maxValue = 1000000000000000000n; // 1 ETH in wei + const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for zero value with bytes encoding', () => { + const maxValue = 0n; + const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + expect(Array.from(result)).toEqual( + new Array(EXPECTED_BYTE_LENGTH).fill(0), + ); + }); + + it('returns Uint8Array for small value with bytes encoding', () => { + const maxValue = 1n; + const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Should be 31 zeros followed by 1 + const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); + expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 1; + expect(Array.from(result)).toEqual(expectedBytes); + }); + + it('returns Uint8Array for large value with bytes encoding', () => { + const maxValue = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; // max uint256 + const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + expect(Array.from(result)).toEqual( + new Array(EXPECTED_BYTE_LENGTH).fill(0xff), + ); + }); + + it('returns Uint8Array for padded hex values with bytes encoding', () => { + const maxValue = 255n; // 0xff + const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // Should be 31 zeros followed by 0xff + const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); + expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 0xff; + expect(Array.from(result)).toEqual(expectedBytes); + }); + }); }); - // Tests for bytes return type - describe('bytes return type', () => { - it('returns Uint8Array when bytes encoding is specified', () => { - const maxValue = 1000000000000000000n; // 1 ETH in wei - const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + describe('decodeValueLteTerms', () => { + it('decodes zero maxValue', () => { + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue: 0n })), + ).toStrictEqual({ + maxValue: 0n, + }); }); - it('returns Uint8Array for zero value with bytes encoding', () => { - const maxValue = 0n; - const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - expect(Array.from(result)).toEqual( - new Array(EXPECTED_BYTE_LENGTH).fill(0), - ); + it('decodes one wei', () => { + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue: 1n })), + ).toStrictEqual({ + maxValue: 1n, + }); }); - it('returns Uint8Array for small value with bytes encoding', () => { - const maxValue = 1n; - const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); + it('decodes 1 ETH in wei', () => { + const maxValue = 1000000000000000000n; + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue })), + ).toStrictEqual({ + maxValue, + }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Should be 31 zeros followed by 1 - const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); - expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 1; - expect(Array.from(result)).toEqual(expectedBytes); + it('decodes 255 padded as uint256', () => { + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue: 255n })), + ).toStrictEqual({ + maxValue: 255n, + }); }); - it('returns Uint8Array for large value with bytes encoding', () => { + it('decodes maximum uint256', () => { const maxValue = - 115792089237316195423570985008687907853269984665640564039457584007913129639935n; // max uint256 - const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue })), + ).toStrictEqual({ + maxValue, + }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - expect(Array.from(result)).toEqual( - new Array(EXPECTED_BYTE_LENGTH).fill(0xff), - ); + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createValueLteTerms({ maxValue: 1000n }, { out: 'bytes' }); + expect(decodeValueLteTerms(bytes)).toStrictEqual({ maxValue: 1000n }); }); - it('returns Uint8Array for padded hex values with bytes encoding', () => { - const maxValue = 255n; // 0xff - const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - // Should be 31 zeros followed by 0xff - const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); - expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 0xff; - expect(Array.from(result)).toEqual(expectedBytes); + it('throws when encoded terms are not exactly 32 bytes', () => { + expect(() => decodeValueLteTerms(`0x${'00'.repeat(31)}`)).toThrow( + 'Invalid ValueLte terms: must be exactly 32 bytes', + ); }); }); }); diff --git a/packages/delegation-core/test/internalUtils.test.ts b/packages/delegation-core/test/internalUtils.test.ts index c46989c1..3d576d68 100644 --- a/packages/delegation-core/test/internalUtils.test.ts +++ b/packages/delegation-core/test/internalUtils.test.ts @@ -1,13 +1,92 @@ import { describe, it, expect } from 'vitest'; import { + assertHexBytesMinLength, + assertHexByteExactLength, + assertHexByteLengthAtLeastOneMultipleOf, concatHex, + extractAddress, + extractBigInt, + extractHex, + extractNumber, + extractRemainingHex, + getByteLength, normalizeAddress, normalizeAddressLowercase, normalizeHex, + toHexString, } from '../src/internalUtils'; +import type { Hex } from '../src/types'; describe('internal utils', () => { + describe('getByteLength', () => { + it('returns byte count excluding 0x prefix', () => { + expect(getByteLength('0x')).toBe(0); + expect(getByteLength('0x00')).toBe(1); + expect(getByteLength('0xabcd')).toBe(2); + expect(getByteLength(`0x${'00'.repeat(32)}`)).toBe(32); + }); + }); + + describe('assertHexByteExactLength', () => { + it('does not throw when length matches', () => { + expect(() => + assertHexByteExactLength(`0x${'00'.repeat(32)}`, 32, 'err'), + ).not.toThrow(); + }); + + it('throws with the given message when length differs', () => { + expect(() => + assertHexByteExactLength(`0x${'00'.repeat(31)}`, 32, 'bad len'), + ).toThrow('bad len'); + }); + }); + + describe('assertHexByteLengthAtLeastOneMultipleOf', () => { + it('does not throw when length is a multiple', () => { + expect(() => + assertHexByteLengthAtLeastOneMultipleOf( + `0x${'00'.repeat(40)}`, + 20, + 'err', + ), + ).not.toThrow(); + }); + + it('throws when length is not a multiple', () => { + expect(() => + assertHexByteLengthAtLeastOneMultipleOf( + `0x${'00'.repeat(19)}`, + 20, + 'bad', + ), + ).toThrow('bad'); + }); + + it('throws when length is 0', () => { + expect(() => + assertHexByteLengthAtLeastOneMultipleOf(`0x`, 20, 'bad'), + ).toThrow('bad'); + }); + }); + + describe('assertHexBytesMinLength', () => { + it('does not throw when at or above minimum', () => { + expect(() => + assertHexBytesMinLength(`0x${'00'.repeat(32)}`, 32, 'err'), + ).not.toThrow(); + expect(() => + assertHexBytesMinLength(`0x${'00'.repeat(40)}`, 32, 'err'), + ).not.toThrow(); + }); + + it('throws when shorter than minimum', () => { + expect(() => + assertHexBytesMinLength(`0x${'00'.repeat(31)}`, 32, 'short'), + ).toThrow('short'); + }); + }); + describe('normalizeHex', () => { it('returns a valid hex string as-is', () => { const value = '0x1234'; @@ -84,4 +163,113 @@ describe('internal utils', () => { expect(concatHex([])).toStrictEqual('0x'); }); }); + + describe('toHexString', () => { + it('pads a small number to the requested byte width', () => { + expect(toHexString({ value: 255, size: 2 })).toBe('00ff'); + expect(toHexString({ value: 1, size: 32 })).toBe(`${'0'.repeat(62)}01`); + }); + + it('works with bigint', () => { + expect(toHexString({ value: 16n, size: 1 })).toBe('10'); + }); + + it('pads zero', () => { + expect(toHexString({ value: 0, size: 1 })).toBe('00'); + expect(toHexString({ value: 0n, size: 32 })).toBe('0'.repeat(64)); + }); + + it('fits max uint256 in 32 bytes', () => { + const max = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + expect(toHexString({ value: max, size: 32 })).toBe(`${'f'.repeat(64)}`); + }); + }); + + describe('extractBigInt', () => { + const value = + '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' as const; + + it('reads uint256 at offset 0', () => { + expect(extractBigInt(value, 0, 32)).toBe(1000000000000000000n); + }); + + it('reads a slice at a non-zero offset', () => { + const data = + '0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002'; + expect(extractBigInt(data, 0, 32)).toBe(1n); + expect(extractBigInt(data, 32, 32)).toBe(2n); + }); + + it('treats missing digits past the string end as zero', () => { + expect(extractBigInt('0x01', 0, 32)).toBe(1n); + }); + }); + + describe('extractNumber', () => { + it('reads a 32-byte uint as a number', () => { + const data = + '0x0000000000000000000000000000000000000000000000000000000061cf9980'; + expect(extractNumber(data, 0, 32)).toBe(1640995200); + }); + + it('reads a 16-byte uint (timestamp-style)', () => { + const data = + '0x00000000000000000000000061cf998000000000000000000000000063b0cd00'; + expect(extractNumber(data, 0, 16)).toBe(1640995200); + expect(extractNumber(data, 16, 16)).toBe(1672531200); + }); + + it('reads small packed values', () => { + expect(extractNumber('0x000a', 0, 2)).toBe(10); + }); + + it('throws when extracted value exceeds MAX_SAFE_INTEGER', () => { + const aboveMaxSafe = '0x0020000000000000' as Hex; + expect(() => extractNumber(aboveMaxSafe, 0, 8)).toThrow( + 'Number is too large', + ); + }); + }); + + describe('extractAddress', () => { + it('reads an address at offset 0', () => { + const addr = '1234567890123456789012345678901234567890'; + const data = `0x${addr}0000000000000000000000000000000000000000000000000000000000000001`; + expect(extractAddress(data, 0)).toBe(`0x${addr}`); + }); + + it('reads an address after a leading word', () => { + const prefix = '0'.repeat(64); + const addr = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + const data = `0x${prefix}${addr}` as Hex; + expect(extractAddress(data, 32)).toBe(`0x${addr}`); + }); + }); + + describe('extractHex', () => { + it('reads a 4-byte selector', () => { + const data = + '0xa9059cbb00000000000000000000000000000000000000000000000000000000'; + expect(extractHex(data, 0, 4)).toBe('0xa9059cbb'); + }); + + it('reads a slice at a non-zero offset', () => { + const data = '0x00112233445566778899aabbccddeeff'; + expect(extractHex(data, 2, 4)).toBe('0x22334455'); + expect(extractHex(data, 8, 4)).toBe('0x8899aabb'); + }); + }); + + describe('extractRemainingHex', () => { + it('returns everything after the byte offset', () => { + const data = + '0x0000000000000000000000000000000000000000000000000000000000000001123456'; + expect(extractRemainingHex(data, 32)).toBe('0x123456'); + }); + + it('returns 0x when offset points at the end', () => { + expect(extractRemainingHex('0xabcd', 2)).toBe('0x'); + }); + }); }); diff --git a/packages/smart-accounts-kit/src/caveatBuilder/timestampBuilder.ts b/packages/smart-accounts-kit/src/caveatBuilder/timestampBuilder.ts index 5e44ea46..e1ea0f90 100644 --- a/packages/smart-accounts-kit/src/caveatBuilder/timestampBuilder.ts +++ b/packages/smart-accounts-kit/src/caveatBuilder/timestampBuilder.ts @@ -32,8 +32,8 @@ export const timestampBuilder = ( const { afterThreshold, beforeThreshold } = config; const terms = createTimestampTerms({ - timestampAfterThreshold: afterThreshold, - timestampBeforeThreshold: beforeThreshold, + afterThreshold, + beforeThreshold, }); const { diff --git a/packages/smart-accounts-kit/src/caveats.ts b/packages/smart-accounts-kit/src/caveats.ts index 0c2c9f57..2d64ba4d 100644 --- a/packages/smart-accounts-kit/src/caveats.ts +++ b/packages/smart-accounts-kit/src/caveats.ts @@ -1,3 +1,36 @@ +import { + decodeAllowedCalldataTerms, + decodeERC20StreamingTerms, + decodeERC20TransferAmountTerms, + decodeERC20BalanceChangeTerms, + decodeAllowedMethodsTerms, + decodeAllowedTargetsTerms, + decodeArgsEqualityCheckTerms, + decodeBlockNumberTerms, + decodeDeployedTerms, + decodeERC721BalanceChangeTerms, + decodeERC721TransferTerms, + decodeERC1155BalanceChangeTerms, + decodeTimestampTerms, + decodeNonceTerms, + decodeValueLteTerms, + decodeLimitedCallsTerms, + decodeIdTerms, + decodeNativeTokenTransferAmountTerms, + decodeNativeBalanceChangeTerms, + decodeNativeTokenStreamingTerms, + decodeNativeTokenPaymentTerms, + decodeRedeemerTerms, + decodeSpecificActionERC20TransferBatchTerms, + decodeNativeTokenPeriodTransferTerms, + decodeERC20TokenPeriodTransferTerms, + decodeExactExecutionTerms, + decodeExactCalldataTerms, + decodeExactCalldataBatchTerms, + decodeExactExecutionBatchTerms, + decodeMultiTokenPeriodTerms, + decodeOwnershipTransferTerms, +} from '@metamask/delegation-core'; import { type Hex, encodeAbiParameters, @@ -6,7 +39,8 @@ import { toHex, } from 'viem'; -import type { Caveat } from './types'; +import type { CoreCaveatConfiguration } from './caveatBuilder/coreCaveatBuilder'; +import type { Caveat, SmartAccountsEnvironment } from './types'; export const CAVEAT_ABI_TYPE_COMPONENTS = [ { type: 'address', name: 'enforcer' }, @@ -49,3 +83,136 @@ export const createCaveat = ( terms, args, }); + +/** + * Decodes a caveat's encoded `terms` bytes by matching `enforcer` to the known enforcer addresses + * in the environment, then delegating to the corresponding `delegation-core` decoder. + * + * @param params - The caveat to decode and the environment that supplies enforcer contract addresses. + * @param params.caveat - The on-chain caveat (`enforcer` + ABI-encoded `terms`). + * @param params.environment - Smart accounts environment, including `caveatEnforcers` address map. + * @returns A {@link CoreCaveatConfiguration} discriminated by `type`, ready for caveat builders. + * @throws If `enforcer` is not a known enforcer in `environment.caveatEnforcers`. + */ +export const decodeCaveat = ({ + caveat: { enforcer, terms }, + environment: { caveatEnforcers }, +}: { + caveat: Caveat; + environment: SmartAccountsEnvironment; +}): CoreCaveatConfiguration => { + switch (enforcer.toLowerCase()) { + case caveatEnforcers.AllowedCalldataEnforcer?.toLowerCase(): + return { type: 'allowedCalldata', ...decodeAllowedCalldataTerms(terms) }; + case caveatEnforcers.AllowedMethodsEnforcer?.toLowerCase(): + return { type: 'allowedMethods', ...decodeAllowedMethodsTerms(terms) }; + case caveatEnforcers.AllowedTargetsEnforcer?.toLowerCase(): + return { type: 'allowedTargets', ...decodeAllowedTargetsTerms(terms) }; + case caveatEnforcers.ArgsEqualityCheckEnforcer?.toLowerCase(): + return { + type: 'argsEqualityCheck', + ...decodeArgsEqualityCheckTerms(terms), + }; + case caveatEnforcers.BlockNumberEnforcer?.toLowerCase(): + return { type: 'blockNumber', ...decodeBlockNumberTerms(terms) }; + case caveatEnforcers.DeployedEnforcer?.toLowerCase(): + return { type: 'deployed', ...decodeDeployedTerms(terms) }; + case caveatEnforcers.ERC20BalanceChangeEnforcer?.toLowerCase(): + return { + type: 'erc20BalanceChange', + ...decodeERC20BalanceChangeTerms(terms), + }; + case caveatEnforcers.ERC20TransferAmountEnforcer?.toLowerCase(): + return { + type: 'erc20TransferAmount', + ...decodeERC20TransferAmountTerms(terms), + }; + case caveatEnforcers.ERC20StreamingEnforcer?.toLowerCase(): + return { type: 'erc20Streaming', ...decodeERC20StreamingTerms(terms) }; + case caveatEnforcers.ERC721BalanceChangeEnforcer?.toLowerCase(): + return { + type: 'erc721BalanceChange', + ...decodeERC721BalanceChangeTerms(terms), + }; + case caveatEnforcers.ERC721TransferEnforcer?.toLowerCase(): + return { type: 'erc721Transfer', ...decodeERC721TransferTerms(terms) }; + case caveatEnforcers.ERC1155BalanceChangeEnforcer?.toLowerCase(): + return { + type: 'erc1155BalanceChange', + ...decodeERC1155BalanceChangeTerms(terms), + }; + case caveatEnforcers.IdEnforcer?.toLowerCase(): + return { type: 'id', ...decodeIdTerms(terms) }; + case caveatEnforcers.LimitedCallsEnforcer?.toLowerCase(): + return { type: 'limitedCalls', ...decodeLimitedCallsTerms(terms) }; + case caveatEnforcers.NonceEnforcer?.toLowerCase(): + return { type: 'nonce', ...decodeNonceTerms(terms) }; + case caveatEnforcers.TimestampEnforcer?.toLowerCase(): + return { type: 'timestamp', ...decodeTimestampTerms(terms) }; + case caveatEnforcers.ValueLteEnforcer?.toLowerCase(): + return { type: 'valueLte', ...decodeValueLteTerms(terms) }; + case caveatEnforcers.NativeTokenTransferAmountEnforcer?.toLowerCase(): + return { + type: 'nativeTokenTransferAmount', + ...decodeNativeTokenTransferAmountTerms(terms), + }; + case caveatEnforcers.NativeBalanceChangeEnforcer?.toLowerCase(): + return { + type: 'nativeBalanceChange', + ...decodeNativeBalanceChangeTerms(terms), + }; + case caveatEnforcers.NativeTokenStreamingEnforcer?.toLowerCase(): + return { + type: 'nativeTokenStreaming', + ...decodeNativeTokenStreamingTerms(terms), + }; + case caveatEnforcers.NativeTokenPaymentEnforcer?.toLowerCase(): + return { + type: 'nativeTokenPayment', + ...decodeNativeTokenPaymentTerms(terms), + }; + case caveatEnforcers.RedeemerEnforcer?.toLowerCase(): + return { type: 'redeemer', ...decodeRedeemerTerms(terms) }; + case caveatEnforcers.SpecificActionERC20TransferBatchEnforcer?.toLowerCase(): + return { + type: 'specificActionERC20TransferBatch', + ...decodeSpecificActionERC20TransferBatchTerms(terms), + }; + case caveatEnforcers.ERC20PeriodTransferEnforcer?.toLowerCase(): + return { + type: 'erc20PeriodTransfer', + ...decodeERC20TokenPeriodTransferTerms(terms), + }; + case caveatEnforcers.NativeTokenPeriodTransferEnforcer?.toLowerCase(): + return { + type: 'nativeTokenPeriodTransfer', + ...decodeNativeTokenPeriodTransferTerms(terms), + }; + case caveatEnforcers.ExactCalldataBatchEnforcer?.toLowerCase(): + return { + type: 'exactCalldataBatch', + ...decodeExactCalldataBatchTerms(terms), + }; + case caveatEnforcers.ExactCalldataEnforcer?.toLowerCase(): + return { type: 'exactCalldata', ...decodeExactCalldataTerms(terms) }; + case caveatEnforcers.ExactExecutionEnforcer?.toLowerCase(): + return { type: 'exactExecution', ...decodeExactExecutionTerms(terms) }; + case caveatEnforcers.ExactExecutionBatchEnforcer?.toLowerCase(): + return { + type: 'exactExecutionBatch', + ...decodeExactExecutionBatchTerms(terms), + }; + case caveatEnforcers.MultiTokenPeriodEnforcer?.toLowerCase(): + return { + type: 'multiTokenPeriod', + ...decodeMultiTokenPeriodTerms(terms), + }; + case caveatEnforcers.OwnershipTransferEnforcer?.toLowerCase(): + return { + type: 'ownershipTransfer', + ...decodeOwnershipTransferTerms(terms), + }; + default: + throw new Error(`Unknown enforcer address: ${enforcer}`); + } +}; diff --git a/packages/smart-accounts-kit/src/utils/index.ts b/packages/smart-accounts-kit/src/utils/index.ts index 6ed7c103..18074e30 100644 --- a/packages/smart-accounts-kit/src/utils/index.ts +++ b/packages/smart-accounts-kit/src/utils/index.ts @@ -1,8 +1,11 @@ +export { decodeCaveat } from '../caveats'; + export { encodeDelegations, decodeDelegations, encodeDelegation, decodeDelegation, + hashDelegation, toDelegationStruct, toDelegation, DELEGATION_ARRAY_ABI_TYPE, @@ -12,8 +15,6 @@ export { SIGNABLE_DELEGATION_TYPED_DATA, } from '../delegation'; -export { hashDelegation } from '../delegation'; - export type { DelegationStruct } from '../delegation'; export { diff --git a/packages/smart-accounts-kit/test/caveatBuilder/timestampBuilder.test.ts b/packages/smart-accounts-kit/test/caveatBuilder/timestampBuilder.test.ts index e800c6ff..c6acaddf 100644 --- a/packages/smart-accounts-kit/test/caveatBuilder/timestampBuilder.test.ts +++ b/packages/smart-accounts-kit/test/caveatBuilder/timestampBuilder.test.ts @@ -23,19 +23,19 @@ describe('timestampBuilder()', () => { describe('validation', () => { it('should fail with negative timestamps', () => { expect(() => buildWithTimestamps(-1, 100)).to.throw( - 'Invalid timestampAfterThreshold: must be zero or positive', + 'Invalid afterThreshold: must be zero or positive', ); expect(() => buildWithTimestamps(100, -100)).to.throw( - 'Invalid timestampBeforeThreshold: must be zero or positive', + 'Invalid beforeThreshold: must be zero or positive', ); }); - it('should fail when timestampBeforeThreshold is not greater than timestampAfterThreshold', () => { + it('should fail when beforeThreshold is not greater than afterThreshold', () => { expect(() => buildWithTimestamps(100, 100)).to.throw( - 'Invalid thresholds: timestampBeforeThreshold must be greater than timestampAfterThreshold', + 'Invalid thresholds: beforeThreshold must be greater than afterThreshold when both are specified', ); expect(() => buildWithTimestamps(101, 100)).to.throw( - 'Invalid thresholds: timestampBeforeThreshold must be greater than timestampAfterThreshold', + 'Invalid thresholds: beforeThreshold must be greater than afterThreshold when both are specified', ); }); }); @@ -82,19 +82,19 @@ describe('timestampBuilder()', () => { expect(size(caveat.terms)).to.equal(EXPECTED_TERMS_LENGTH); }); - it('should fail when timestampBeforeThreshold is greater than the upper bound', () => { + it('should fail when beforeThreshold is greater than the upper bound', () => { expect(() => buildWithTimestamps(0, TIMESTAMP_UPPER_BOUND_SECONDS + 1), ).to.throw( - 'Invalid timestampBeforeThreshold: must be less than or equal to 253402300799', + 'Invalid beforeThreshold: must be less than or equal to 253402300799', ); }); - it('should fail when timestampAfterThreshold is greater than the upper bound', () => { + it('should fail when afterThreshold is greater than the upper bound', () => { expect(() => buildWithTimestamps(TIMESTAMP_UPPER_BOUND_SECONDS + 1, 0), ).to.throw( - 'Invalid timestampAfterThreshold: must be less than or equal to 253402300799', + 'Invalid afterThreshold: must be less than or equal to 253402300799', ); }); }); diff --git a/packages/smart-accounts-kit/test/caveats.test.ts b/packages/smart-accounts-kit/test/caveats.test.ts new file mode 100644 index 00000000..15dad6dd --- /dev/null +++ b/packages/smart-accounts-kit/test/caveats.test.ts @@ -0,0 +1,281 @@ +import type { Hex } from 'viem'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { randomAddress } from './utils'; +import type { SmartAccountsEnvironment } from '../src/types'; + +const delegationCoreMocks = vi.hoisted(() => ({ + decodeAllowedCalldataTerms: vi.fn(() => ({})), + decodeAllowedMethodsTerms: vi.fn(() => ({})), + decodeAllowedTargetsTerms: vi.fn(() => ({})), + decodeArgsEqualityCheckTerms: vi.fn(() => ({})), + decodeBlockNumberTerms: vi.fn(() => ({})), + decodeDeployedTerms: vi.fn(() => ({})), + decodeERC20BalanceChangeTerms: vi.fn(() => ({})), + decodeERC20TransferAmountTerms: vi.fn(() => ({})), + decodeERC20StreamingTerms: vi.fn(() => ({})), + decodeERC721BalanceChangeTerms: vi.fn(() => ({})), + decodeERC721TransferTerms: vi.fn(() => ({})), + decodeERC1155BalanceChangeTerms: vi.fn(() => ({})), + decodeIdTerms: vi.fn(() => ({})), + decodeLimitedCallsTerms: vi.fn(() => ({})), + decodeNonceTerms: vi.fn(() => ({})), + decodeTimestampTerms: vi.fn(() => ({})), + decodeValueLteTerms: vi.fn(() => ({})), + decodeNativeTokenTransferAmountTerms: vi.fn(() => ({})), + decodeNativeBalanceChangeTerms: vi.fn(() => ({})), + decodeNativeTokenStreamingTerms: vi.fn(() => ({})), + decodeNativeTokenPaymentTerms: vi.fn(() => ({})), + decodeRedeemerTerms: vi.fn(() => ({})), + decodeSpecificActionERC20TransferBatchTerms: vi.fn(() => ({})), + decodeERC20TokenPeriodTransferTerms: vi.fn(() => ({})), + decodeNativeTokenPeriodTransferTerms: vi.fn(() => ({})), + decodeExactCalldataBatchTerms: vi.fn(() => ({})), + decodeExactCalldataTerms: vi.fn(() => ({})), + decodeExactExecutionTerms: vi.fn(() => ({})), + decodeExactExecutionBatchTerms: vi.fn(() => ({})), + decodeMultiTokenPeriodTerms: vi.fn(() => ({})), + decodeOwnershipTransferTerms: vi.fn(() => ({})), +})); + +vi.mock('@metamask/delegation-core', () => delegationCoreMocks); + +const { decodeCaveat } = await import('../src/caveats'); + +type DecoderName = keyof typeof delegationCoreMocks; + +const DECODE_CASES: { + enforcerKey: keyof SmartAccountsEnvironment['caveatEnforcers']; + decoder: DecoderName; + type: string; +}[] = [ + { + enforcerKey: 'AllowedCalldataEnforcer', + decoder: 'decodeAllowedCalldataTerms', + type: 'allowedCalldata', + }, + { + enforcerKey: 'AllowedMethodsEnforcer', + decoder: 'decodeAllowedMethodsTerms', + type: 'allowedMethods', + }, + { + enforcerKey: 'AllowedTargetsEnforcer', + decoder: 'decodeAllowedTargetsTerms', + type: 'allowedTargets', + }, + { + enforcerKey: 'ArgsEqualityCheckEnforcer', + decoder: 'decodeArgsEqualityCheckTerms', + type: 'argsEqualityCheck', + }, + { + enforcerKey: 'BlockNumberEnforcer', + decoder: 'decodeBlockNumberTerms', + type: 'blockNumber', + }, + { + enforcerKey: 'DeployedEnforcer', + decoder: 'decodeDeployedTerms', + type: 'deployed', + }, + { + enforcerKey: 'ERC20BalanceChangeEnforcer', + decoder: 'decodeERC20BalanceChangeTerms', + type: 'erc20BalanceChange', + }, + { + enforcerKey: 'ERC20TransferAmountEnforcer', + decoder: 'decodeERC20TransferAmountTerms', + type: 'erc20TransferAmount', + }, + { + enforcerKey: 'ERC20StreamingEnforcer', + decoder: 'decodeERC20StreamingTerms', + type: 'erc20Streaming', + }, + { + enforcerKey: 'ERC721BalanceChangeEnforcer', + decoder: 'decodeERC721BalanceChangeTerms', + type: 'erc721BalanceChange', + }, + { + enforcerKey: 'ERC721TransferEnforcer', + decoder: 'decodeERC721TransferTerms', + type: 'erc721Transfer', + }, + { + enforcerKey: 'ERC1155BalanceChangeEnforcer', + decoder: 'decodeERC1155BalanceChangeTerms', + type: 'erc1155BalanceChange', + }, + { + enforcerKey: 'IdEnforcer', + decoder: 'decodeIdTerms', + type: 'id', + }, + { + enforcerKey: 'LimitedCallsEnforcer', + decoder: 'decodeLimitedCallsTerms', + type: 'limitedCalls', + }, + { + enforcerKey: 'NonceEnforcer', + decoder: 'decodeNonceTerms', + type: 'nonce', + }, + { + enforcerKey: 'TimestampEnforcer', + decoder: 'decodeTimestampTerms', + type: 'timestamp', + }, + { + enforcerKey: 'ValueLteEnforcer', + decoder: 'decodeValueLteTerms', + type: 'valueLte', + }, + { + enforcerKey: 'NativeTokenTransferAmountEnforcer', + decoder: 'decodeNativeTokenTransferAmountTerms', + type: 'nativeTokenTransferAmount', + }, + { + enforcerKey: 'NativeBalanceChangeEnforcer', + decoder: 'decodeNativeBalanceChangeTerms', + type: 'nativeBalanceChange', + }, + { + enforcerKey: 'NativeTokenStreamingEnforcer', + decoder: 'decodeNativeTokenStreamingTerms', + type: 'nativeTokenStreaming', + }, + { + enforcerKey: 'NativeTokenPaymentEnforcer', + decoder: 'decodeNativeTokenPaymentTerms', + type: 'nativeTokenPayment', + }, + { + enforcerKey: 'RedeemerEnforcer', + decoder: 'decodeRedeemerTerms', + type: 'redeemer', + }, + { + enforcerKey: 'SpecificActionERC20TransferBatchEnforcer', + decoder: 'decodeSpecificActionERC20TransferBatchTerms', + type: 'specificActionERC20TransferBatch', + }, + { + enforcerKey: 'ERC20PeriodTransferEnforcer', + decoder: 'decodeERC20TokenPeriodTransferTerms', + type: 'erc20PeriodTransfer', + }, + { + enforcerKey: 'NativeTokenPeriodTransferEnforcer', + decoder: 'decodeNativeTokenPeriodTransferTerms', + type: 'nativeTokenPeriodTransfer', + }, + { + enforcerKey: 'ExactCalldataBatchEnforcer', + decoder: 'decodeExactCalldataBatchTerms', + type: 'exactCalldataBatch', + }, + { + enforcerKey: 'ExactCalldataEnforcer', + decoder: 'decodeExactCalldataTerms', + type: 'exactCalldata', + }, + { + enforcerKey: 'ExactExecutionEnforcer', + decoder: 'decodeExactExecutionTerms', + type: 'exactExecution', + }, + { + enforcerKey: 'ExactExecutionBatchEnforcer', + decoder: 'decodeExactExecutionBatchTerms', + type: 'exactExecutionBatch', + }, + { + enforcerKey: 'MultiTokenPeriodEnforcer', + decoder: 'decodeMultiTokenPeriodTerms', + type: 'multiTokenPeriod', + }, + { + enforcerKey: 'OwnershipTransferEnforcer', + decoder: 'decodeOwnershipTransferTerms', + type: 'ownershipTransfer', + }, +]; + +/** + * Minimal {@link SmartAccountsEnvironment} for `decodeCaveat` tests (only + * `caveatEnforcers` is read by the implementation). + * + * @param caveatEnforcers - Enforcer name to contract address map. + * @returns A stub environment suitable for `decodeCaveat`. + */ +function buildEnvironment( + caveatEnforcers: SmartAccountsEnvironment['caveatEnforcers'], +): SmartAccountsEnvironment { + return { + DelegationManager: randomAddress(), + EntryPoint: randomAddress(), + SimpleFactory: randomAddress(), + implementations: {}, + caveatEnforcers, + }; +} + +describe('decodeCaveat', () => { + const terms = '0xabcd' as Hex; + + beforeEach(() => { + for (const mock of Object.values(delegationCoreMocks)) { + mock.mockReset(); + mock.mockReturnValue({}); + } + }); + + it.each(DECODE_CASES)( + 'routes $enforcerKey to $decoder and sets type $type', + ({ enforcerKey, decoder, type }) => { + const enforcerAddress = randomAddress(); + const decodedPayload = { fromMock: enforcerKey }; + delegationCoreMocks[decoder].mockReturnValue(decodedPayload); + + const environment = buildEnvironment({ + [enforcerKey]: enforcerAddress, + }); + + const result = decodeCaveat({ + caveat: { + enforcer: enforcerAddress, + terms, + args: '0x', + }, + environment, + }); + + expect(delegationCoreMocks[decoder]).toHaveBeenCalledTimes(1); + expect(delegationCoreMocks[decoder]).toHaveBeenCalledWith(terms); + expect(result).toEqual({ type, ...decodedPayload }); + }, + ); + + it('throws when the enforcer address is unknown', () => { + const unknownEnforcer = randomAddress(); + const environment = buildEnvironment({ + AllowedCalldataEnforcer: randomAddress(), + }); + + expect(() => + decodeCaveat({ + caveat: { + enforcer: unknownEnforcer, + terms, + args: '0x', + }, + environment, + }), + ).toThrow(`Unknown enforcer address: ${unknownEnforcer}`); + }); +});