From 0d621e0398a60cd85706bfe852f15a2988b29344 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 23 Mar 2026 21:59:36 +0000 Subject: [PATCH 01/15] feat: add terms decoders for all caveat enforcers - Add decoder utility functions (extractBigInt, extractNumber, extractAddress, extractHex, extractRemainingHex) to internalUtils - Implement decoder functions for all 32 caveat types to match existing encoders - Export all decoder functions from caveats/index.ts and main index.ts - Add comprehensive test suite covering all decoder functions - All 435 existing tests continue to pass Co-authored-by: jeffsmale90 --- .../src/caveats/allowedCalldata.ts | 21 +- .../src/caveats/allowedMethods.ts | 28 +- .../src/caveats/allowedTargets.ts | 28 +- .../src/caveats/argsEqualityCheck.ts | 14 + .../src/caveats/blockNumber.ts | 17 +- .../delegation-core/src/caveats/deployed.ts | 27 +- .../src/caveats/erc1155BalanceChange.ts | 25 + .../src/caveats/erc20BalanceChange.ts | 24 + .../src/caveats/erc20Streaming.ts | 29 +- .../src/caveats/erc20TokenPeriodTransfer.ts | 28 +- .../src/caveats/erc20TransferAmount.ts | 27 +- .../src/caveats/erc721BalanceChange.ts | 24 + .../src/caveats/erc721Transfer.ts | 27 +- .../src/caveats/exactCalldata.ts | 14 + .../src/caveats/exactCalldataBatch.ts | 28 +- .../src/caveats/exactExecution.ts | 35 +- .../src/caveats/exactExecutionBatch.ts | 28 +- packages/delegation-core/src/caveats/id.ts | 16 +- packages/delegation-core/src/caveats/index.ts | 122 +++-- .../src/caveats/limitedCalls.ts | 16 +- .../src/caveats/multiTokenPeriod.ts | 40 +- .../src/caveats/nativeBalanceChange.ts | 23 + .../src/caveats/nativeTokenPayment.ts | 21 + .../src/caveats/nativeTokenPeriodTransfer.ts | 23 +- .../src/caveats/nativeTokenStreaming.ts | 24 +- .../src/caveats/nativeTokenTransferAmount.ts | 18 +- packages/delegation-core/src/caveats/nonce.ts | 18 +- .../src/caveats/ownershipTransfer.ts | 17 +- .../delegation-core/src/caveats/redeemer.ts | 26 +- .../specificActionERC20TransferBatch.ts | 31 +- .../delegation-core/src/caveats/timestamp.ts | 17 +- .../delegation-core/src/caveats/valueLte.ts | 16 +- packages/delegation-core/src/index.ts | 31 ++ packages/delegation-core/src/internalUtils.ts | 77 +++ .../test/caveats/decoders.test.ts | 510 ++++++++++++++++++ 35 files changed, 1415 insertions(+), 55 deletions(-) create mode 100644 packages/delegation-core/test/caveats/decoders.test.ts diff --git a/packages/delegation-core/src/caveats/allowedCalldata.ts b/packages/delegation-core/src/caveats/allowedCalldata.ts index c2a5a180..5208aade 100644 --- a/packages/delegation-core/src/caveats/allowedCalldata.ts +++ b/packages/delegation-core/src/caveats/allowedCalldata.ts @@ -1,7 +1,8 @@ import { bytesToHex, remove0x, type BytesLike } from '@metamask/utils'; -import { toHexString } from '../internalUtils'; +import { extractNumber, extractRemainingHex, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -73,3 +74,21 @@ export function createAllowedCalldataTerms( // 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. + * @returns The decoded AllowedCalldataTerms object. + */ +export function decodeAllowedCalldataTerms( + terms: BytesLike, +): AllowedCalldataTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: startIndex (32 bytes) + value (remaining) + const startIndex = extractNumber(hexTerms, 0, 32); + const value = extractRemainingHex(hexTerms, 32); + + return { startIndex, value }; +} diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index 65af5189..785c3df1 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -1,7 +1,8 @@ import { bytesToHex, isHexString, type BytesLike } from '@metamask/utils'; -import { concatHex } from '../internalUtils'; +import { concatHex, extractHex } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -76,3 +77,28 @@ 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. + * @returns The decoded AllowedMethodsTerms object. + */ +export function decodeAllowedMethodsTerms( + terms: BytesLike, +): AllowedMethodsTerms { + const hexTerms = bytesLikeToHex(terms); + + // Each selector is 4 bytes + const selectorSize = 4; + const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 + const selectorCount = totalBytes / selectorSize; + + const selectors: string[] = []; + for (let i = 0; i < selectorCount; i++) { + const selector = extractHex(hexTerms, i * selectorSize, selectorSize); + selectors.push(selector); + } + + return { selectors }; +} diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index eae6056b..a06d1555 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -1,7 +1,8 @@ import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress } from '../internalUtils'; +import { concatHex, extractAddress, normalizeAddress } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -60,3 +61,28 @@ 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. + * @returns The decoded AllowedTargetsTerms object. + */ +export function decodeAllowedTargetsTerms( + terms: BytesLike, +): AllowedTargetsTerms { + const hexTerms = bytesLikeToHex(terms); + + // Each address is 20 bytes + const addressSize = 20; + const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 + const addressCount = totalBytes / addressSize; + + const targets: string[] = []; + for (let i = 0; i < addressCount; i++) { + const target = extractAddress(hexTerms, i * addressSize); + targets.push(target); + } + + return { targets }; +} diff --git a/packages/delegation-core/src/caveats/argsEqualityCheck.ts b/packages/delegation-core/src/caveats/argsEqualityCheck.ts index 4be828cb..214ed249 100644 --- a/packages/delegation-core/src/caveats/argsEqualityCheck.ts +++ b/packages/delegation-core/src/caveats/argsEqualityCheck.ts @@ -2,6 +2,7 @@ import type { BytesLike } from '@metamask/utils'; import { normalizeHex } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -58,3 +59,16 @@ 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. + * @returns The decoded ArgsEqualityCheckTerms object. + */ +export function decodeArgsEqualityCheckTerms( + terms: BytesLike, +): ArgsEqualityCheckTerms { + const args = bytesLikeToHex(terms); + return { args }; +} diff --git a/packages/delegation-core/src/caveats/blockNumber.ts b/packages/delegation-core/src/caveats/blockNumber.ts index 89c6f6db..03a3409f 100644 --- a/packages/delegation-core/src/caveats/blockNumber.ts +++ b/packages/delegation-core/src/caveats/blockNumber.ts @@ -1,11 +1,13 @@ -import { toHexString } from '../internalUtils'; +import { extractBigInt, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, type ResultValue, } from '../returns'; import type { Hex } from '../types'; +import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a BlockNumber caveat. @@ -69,3 +71,16 @@ 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); + 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..d95e4f46 100644 --- a/packages/delegation-core/src/caveats/deployed.ts +++ b/packages/delegation-core/src/caveats/deployed.ts @@ -1,8 +1,16 @@ import type { BytesLike } from '@metamask/utils'; import { remove0x } from '@metamask/utils'; -import { concatHex, normalizeAddress, normalizeHex } from '../internalUtils'; import { + concatHex, + extractAddress, + extractHex, + extractRemainingHex, + normalizeAddress, + normalizeHex, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -74,3 +82,20 @@ 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. + * @returns The decoded DeployedTerms object. + */ +export function decodeDeployedTerms(terms: BytesLike): DeployedTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: contractAddress (20 bytes) + salt (32 bytes) + bytecode (remaining) + const contractAddress = extractAddress(hexTerms, 0); + const salt = extractHex(hexTerms, 20, 32); + const bytecode = extractRemainingHex(hexTerms, 52); + + return { contractAddress, salt, bytecode }; +} diff --git a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts index b54561da..2cc7eae9 100644 --- a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts @@ -2,10 +2,14 @@ import type { BytesLike } from '@metamask/utils'; import { concatHex, + extractAddress, + extractBigInt, + extractNumber, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -105,3 +109,24 @@ 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. + * @returns The decoded ERC1155BalanceChangeTerms object. + */ +export function decodeERC1155BalanceChangeTerms( + terms: BytesLike, +): ERC1155BalanceChangeTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + tokenId (32 bytes) + balance (32 bytes) + const changeType = extractNumber(hexTerms, 0, 1); + const tokenAddress = extractAddress(hexTerms, 1); + const recipient = extractAddress(hexTerms, 21); + const tokenId = extractBigInt(hexTerms, 41, 32); + const balance = extractBigInt(hexTerms, 73, 32); + + return { changeType, tokenAddress, recipient, tokenId, balance }; +} diff --git a/packages/delegation-core/src/caveats/erc20BalanceChange.ts b/packages/delegation-core/src/caveats/erc20BalanceChange.ts index 5f9bffa1..5136f6b4 100644 --- a/packages/delegation-core/src/caveats/erc20BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc20BalanceChange.ts @@ -2,10 +2,14 @@ import type { BytesLike } from '@metamask/utils'; import { concatHex, + extractAddress, + extractBigInt, + extractNumber, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -96,3 +100,23 @@ 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. + * @returns The decoded ERC20BalanceChangeTerms object. + */ +export function decodeERC20BalanceChangeTerms( + terms: BytesLike, +): ERC20BalanceChangeTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + balance (32 bytes) + const changeType = extractNumber(hexTerms, 0, 1); + const tokenAddress = extractAddress(hexTerms, 1); + const recipient = extractAddress(hexTerms, 21); + const balance = extractBigInt(hexTerms, 41, 32); + + return { changeType, tokenAddress, recipient, balance }; +} diff --git a/packages/delegation-core/src/caveats/erc20Streaming.ts b/packages/delegation-core/src/caveats/erc20Streaming.ts index bf424674..2c03fb56 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -1,7 +1,13 @@ import { type BytesLike, bytesToHex, isHexString } from '@metamask/utils'; -import { toHexString } from '../internalUtils'; import { + extractAddress, + extractBigInt, + extractNumber, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -120,3 +126,24 @@ 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. + * @returns The decoded ERC20StreamingTerms object. + */ +export function decodeERC20StreamingTerms( + terms: BytesLike, +): ERC20StreamingTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: tokenAddress (20 bytes) + initialAmount (32 bytes) + maxAmount (32 bytes) + amountPerSecond (32 bytes) + startTime (32 bytes) + const tokenAddress = 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, initialAmount, maxAmount, amountPerSecond, startTime }; +} diff --git a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts index 954d8f07..93726f60 100644 --- a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -1,7 +1,13 @@ import { type BytesLike, isHexString, bytesToHex } from '@metamask/utils'; -import { toHexString } from '../internalUtils'; import { + extractAddress, + extractBigInt, + extractNumber, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -94,3 +100,23 @@ 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. + * @returns The decoded ERC20TokenPeriodTransferTerms object. + */ +export function decodeERC20TokenPeriodTransferTerms( + terms: BytesLike, +): ERC20TokenPeriodTransferTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: tokenAddress (20 bytes) + periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 bytes) + const tokenAddress = extractAddress(hexTerms, 0); + const periodAmount = extractBigInt(hexTerms, 20, 32); + const periodDuration = extractNumber(hexTerms, 52, 32); + const startDate = extractNumber(hexTerms, 84, 32); + + return { tokenAddress, periodAmount, periodDuration, startDate }; +} diff --git a/packages/delegation-core/src/caveats/erc20TransferAmount.ts b/packages/delegation-core/src/caveats/erc20TransferAmount.ts index 50f189ee..aa87c916 100644 --- a/packages/delegation-core/src/caveats/erc20TransferAmount.ts +++ b/packages/delegation-core/src/caveats/erc20TransferAmount.ts @@ -1,7 +1,14 @@ import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + concatHex, + extractAddress, + extractBigInt, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -63,3 +70,21 @@ 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. + * @returns The decoded ERC20TransferAmountTerms object. + */ +export function decodeERC20TransferAmountTerms( + terms: BytesLike, +): ERC20TransferAmountTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: tokenAddress (20 bytes) + maxAmount (32 bytes) + const tokenAddress = extractAddress(hexTerms, 0); + const maxAmount = extractBigInt(hexTerms, 20, 32); + + return { tokenAddress, maxAmount }; +} diff --git a/packages/delegation-core/src/caveats/erc721BalanceChange.ts b/packages/delegation-core/src/caveats/erc721BalanceChange.ts index 912f0454..66c368e3 100644 --- a/packages/delegation-core/src/caveats/erc721BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc721BalanceChange.ts @@ -2,10 +2,14 @@ import type { BytesLike } from '@metamask/utils'; import { concatHex, + extractAddress, + extractBigInt, + extractNumber, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -96,3 +100,23 @@ 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. + * @returns The decoded ERC721BalanceChangeTerms object. + */ +export function decodeERC721BalanceChangeTerms( + terms: BytesLike, +): ERC721BalanceChangeTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + amount (32 bytes) + const changeType = extractNumber(hexTerms, 0, 1); + const tokenAddress = extractAddress(hexTerms, 1); + const recipient = extractAddress(hexTerms, 21); + const amount = extractBigInt(hexTerms, 41, 32); + + return { changeType, tokenAddress, recipient, amount }; +} diff --git a/packages/delegation-core/src/caveats/erc721Transfer.ts b/packages/delegation-core/src/caveats/erc721Transfer.ts index 8270dbad..11358356 100644 --- a/packages/delegation-core/src/caveats/erc721Transfer.ts +++ b/packages/delegation-core/src/caveats/erc721Transfer.ts @@ -1,7 +1,14 @@ import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + concatHex, + extractAddress, + extractBigInt, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -63,3 +70,21 @@ 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. + * @returns The decoded ERC721TransferTerms object. + */ +export function decodeERC721TransferTerms( + terms: BytesLike, +): ERC721TransferTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: tokenAddress (20 bytes) + tokenId (32 bytes) + const tokenAddress = extractAddress(hexTerms, 0); + const tokenId = extractBigInt(hexTerms, 20, 32); + + return { tokenAddress, tokenId }; +} diff --git a/packages/delegation-core/src/caveats/exactCalldata.ts b/packages/delegation-core/src/caveats/exactCalldata.ts index 43b25fd9..e362e3dc 100644 --- a/packages/delegation-core/src/caveats/exactCalldata.ts +++ b/packages/delegation-core/src/caveats/exactCalldata.ts @@ -1,6 +1,7 @@ import type { BytesLike } from '@metamask/utils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -59,3 +60,16 @@ export function createExactCalldataTerms( // 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. + * @returns The decoded ExactCalldataTerms object. + */ +export function decodeExactCalldataTerms( + terms: BytesLike, +): ExactCalldataTerms { + const calldata = bytesLikeToHex(terms); + return { calldata }; +} diff --git a/packages/delegation-core/src/caveats/exactCalldataBatch.ts b/packages/delegation-core/src/caveats/exactCalldataBatch.ts index cfe4ec33..d311c310 100644 --- a/packages/delegation-core/src/caveats/exactCalldataBatch.ts +++ b/packages/delegation-core/src/caveats/exactCalldataBatch.ts @@ -1,8 +1,9 @@ -import { encodeSingle } from '@metamask/abi-utils'; +import { decodeSingle, encodeSingle } from '@metamask/abi-utils'; import { bytesToHex, type BytesLike } from '@metamask/utils'; import { normalizeAddress } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -86,3 +87,28 @@ 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. + * @returns The decoded ExactCalldataBatchTerms object. + */ +export function decodeExactCalldataBatchTerms( + terms: BytesLike, +): ExactCalldataBatchTerms { + const hexTerms = bytesLikeToHex(terms); + + // Decode using ABI: (address,uint256,bytes)[] + const decoded = decodeSingle(EXECUTION_ARRAY_ABI, hexTerms); + + const executions = (decoded as [string, bigint, Uint8Array][]).map( + ([target, value, callData]) => ({ + target, + value, + callData: bytesToHex(callData), + }), + ); + + return { executions }; +} diff --git a/packages/delegation-core/src/caveats/exactExecution.ts b/packages/delegation-core/src/caveats/exactExecution.ts index f8d3a56c..6afdcf99 100644 --- a/packages/delegation-core/src/caveats/exactExecution.ts +++ b/packages/delegation-core/src/caveats/exactExecution.ts @@ -1,7 +1,15 @@ import { bytesToHex, type BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + concatHex, + extractAddress, + extractBigInt, + extractRemainingHex, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -77,3 +85,28 @@ 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. + * @returns The decoded ExactExecutionTerms object. + */ +export function decodeExactExecutionTerms( + terms: BytesLike, +): ExactExecutionTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: target (20 bytes) + value (32 bytes) + callData (remaining) + const target = extractAddress(hexTerms, 0); + const value = extractBigInt(hexTerms, 20, 32); + const callData = extractRemainingHex(hexTerms, 52); + + return { + execution: { + target, + value, + callData, + }, + }; +} diff --git a/packages/delegation-core/src/caveats/exactExecutionBatch.ts b/packages/delegation-core/src/caveats/exactExecutionBatch.ts index a8a72e25..ea1d64e4 100644 --- a/packages/delegation-core/src/caveats/exactExecutionBatch.ts +++ b/packages/delegation-core/src/caveats/exactExecutionBatch.ts @@ -1,8 +1,9 @@ -import { encodeSingle } from '@metamask/abi-utils'; +import { decodeSingle, encodeSingle } from '@metamask/abi-utils'; import { bytesToHex, type BytesLike } from '@metamask/utils'; import { normalizeAddress } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -86,3 +87,28 @@ 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. + * @returns The decoded ExactExecutionBatchTerms object. + */ +export function decodeExactExecutionBatchTerms( + terms: BytesLike, +): ExactExecutionBatchTerms { + const hexTerms = bytesLikeToHex(terms); + + // Decode using ABI: (address,uint256,bytes)[] + const decoded = decodeSingle(EXECUTION_ARRAY_ABI, hexTerms); + + const executions = (decoded as [string, bigint, Uint8Array][]).map( + ([target, value, callData]) => ({ + target, + value, + callData: bytesToHex(callData), + }), + ); + + return { executions }; +} diff --git a/packages/delegation-core/src/caveats/id.ts b/packages/delegation-core/src/caveats/id.ts index de6f1ea9..875f2e60 100644 --- a/packages/delegation-core/src/caveats/id.ts +++ b/packages/delegation-core/src/caveats/id.ts @@ -1,11 +1,13 @@ -import { toHexString } from '../internalUtils'; +import { extractBigInt, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, type ResultValue, } from '../returns'; import type { Hex } from '../types'; +import type { BytesLike } from '@metamask/utils'; const MAX_UINT256 = BigInt(`0x${'f'.repeat(64)}`); @@ -71,3 +73,15 @@ 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); + 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..e2027eb3 100644 --- a/packages/delegation-core/src/caveats/index.ts +++ b/packages/delegation-core/src/caveats/index.ts @@ -1,31 +1,91 @@ -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..76107167 100644 --- a/packages/delegation-core/src/caveats/limitedCalls.ts +++ b/packages/delegation-core/src/caveats/limitedCalls.ts @@ -1,11 +1,13 @@ -import { toHexString } from '../internalUtils'; +import { extractNumber, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, type ResultValue, } from '../returns'; import type { Hex } from '../types'; +import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a LimitedCalls caveat. @@ -56,3 +58,15 @@ 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); + 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..d5f4915f 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -1,7 +1,15 @@ import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + concatHex, + extractAddress, + extractBigInt, + extractNumber, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -93,3 +101,33 @@ 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. + * @returns The decoded MultiTokenPeriodTerms object. + */ +export function decodeMultiTokenPeriodTerms( + terms: BytesLike, +): MultiTokenPeriodTerms { + const hexTerms = bytesLikeToHex(terms); + + // Each token config is: token (20 bytes) + periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 bytes) = 116 bytes + const configSize = 116; + const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 + const configCount = totalBytes / configSize; + + const tokenConfigs: TokenPeriodConfig[] = []; + for (let i = 0; i < configCount; i++) { + const offset = i * configSize; + const token = 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, periodAmount, periodDuration, startDate }); + } + + return { tokenConfigs }; +} diff --git a/packages/delegation-core/src/caveats/nativeBalanceChange.ts b/packages/delegation-core/src/caveats/nativeBalanceChange.ts index a52e2ecb..66e09941 100644 --- a/packages/delegation-core/src/caveats/nativeBalanceChange.ts +++ b/packages/delegation-core/src/caveats/nativeBalanceChange.ts @@ -2,10 +2,14 @@ import type { BytesLike } from '@metamask/utils'; import { concatHex, + extractAddress, + extractBigInt, + extractNumber, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -80,3 +84,22 @@ 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. + * @returns The decoded NativeBalanceChangeTerms object. + */ +export function decodeNativeBalanceChangeTerms( + terms: BytesLike, +): NativeBalanceChangeTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: changeType (1 byte) + recipient (20 bytes) + balance (32 bytes) + const changeType = extractNumber(hexTerms, 0, 1); + const recipient = extractAddress(hexTerms, 1); + const balance = extractBigInt(hexTerms, 21, 32); + + return { changeType, recipient, balance }; +} diff --git a/packages/delegation-core/src/caveats/nativeTokenPayment.ts b/packages/delegation-core/src/caveats/nativeTokenPayment.ts index 3074320e..f6fdc7c6 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPayment.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPayment.ts @@ -2,10 +2,13 @@ import type { BytesLike } from '@metamask/utils'; import { concatHex, + extractAddress, + extractBigInt, normalizeAddressLowercase, toHexString, } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -67,3 +70,21 @@ 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. + * @returns The decoded NativeTokenPaymentTerms object. + */ +export function decodeNativeTokenPaymentTerms( + terms: BytesLike, +): NativeTokenPaymentTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: recipient (20 bytes) + amount (32 bytes) + const recipient = extractAddress(hexTerms, 0); + const amount = extractBigInt(hexTerms, 20, 32); + + return { recipient, amount }; +} diff --git a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts index 80015d46..fb635aa0 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts @@ -1,11 +1,13 @@ -import { toHexString } from '../internalUtils'; +import { extractBigInt, extractNumber, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, type ResultValue, } from '../returns'; import type { Hex } from '../types'; +import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a periodic transfer allowance of native tokens. @@ -72,3 +74,22 @@ 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); + + // Structure: periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 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..f36a89b5 100644 --- a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts +++ b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts @@ -1,11 +1,13 @@ -import { toHexString } from '../internalUtils'; +import { extractBigInt, extractNumber, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, type ResultValue, } from '../returns'; import type { Hex } from '../types'; +import type { BytesLike } from '@metamask/utils'; // Upper bound for timestamps (January 1, 10000 CE) const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; @@ -96,3 +98,23 @@ 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); + + // Structure: initialAmount (32 bytes) + maxAmount (32 bytes) + amountPerSecond (32 bytes) + startTime (32 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..55f58d09 100644 --- a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts +++ b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts @@ -1,11 +1,13 @@ -import { toHexString } from '../internalUtils'; +import { extractBigInt, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, type ResultValue, } from '../returns'; import type { Hex } from '../types'; +import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a NativeTokenTransferAmount caveat. @@ -52,3 +54,17 @@ 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); + 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..97b529b9 100644 --- a/packages/delegation-core/src/caveats/nonce.ts +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -1,4 +1,4 @@ -import { isHexString } from '@metamask/utils'; +import { hexToBytes, isHexString } from '@metamask/utils'; import type { BytesLike } from '@metamask/utils'; import { @@ -84,3 +84,19 @@ 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. + * @returns The decoded NonceTerms object. + */ +export function decodeNonceTerms(terms: BytesLike): NonceTerms { + const hexTerms = bytesLikeToHex(terms); + + // The nonce is stored as a 32-byte padded value + // We return it as-is (padded) to maintain consistency + const nonce = hexTerms; + + return { nonce }; +} diff --git a/packages/delegation-core/src/caveats/ownershipTransfer.ts b/packages/delegation-core/src/caveats/ownershipTransfer.ts index 5b0675eb..a3e90fee 100644 --- a/packages/delegation-core/src/caveats/ownershipTransfer.ts +++ b/packages/delegation-core/src/caveats/ownershipTransfer.ts @@ -1,7 +1,8 @@ import type { BytesLike } from '@metamask/utils'; -import { normalizeAddress } from '../internalUtils'; +import { extractAddress, normalizeAddress } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -54,3 +55,17 @@ 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. + * @returns The decoded OwnershipTransferTerms object. + */ +export function decodeOwnershipTransferTerms( + terms: BytesLike, +): OwnershipTransferTerms { + const hexTerms = bytesLikeToHex(terms); + const contractAddress = extractAddress(hexTerms, 0); + return { contractAddress }; +} diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index d88d29f2..12daebc1 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -1,7 +1,8 @@ import type { BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress } from '../internalUtils'; +import { concatHex, extractAddress, normalizeAddress } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -60,3 +61,26 @@ 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. + * @returns The decoded RedeemerTerms object. + */ +export function decodeRedeemerTerms(terms: BytesLike): RedeemerTerms { + const hexTerms = bytesLikeToHex(terms); + + // Each address is 20 bytes + const addressSize = 20; + const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 + const addressCount = totalBytes / addressSize; + + const redeemers: string[] = []; + for (let i = 0; i < addressCount; i++) { + const redeemer = extractAddress(hexTerms, i * addressSize); + redeemers.push(redeemer); + } + + return { redeemers }; +} diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts index bfc33e13..4c36f813 100644 --- a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -1,7 +1,15 @@ import { bytesToHex, type BytesLike } from '@metamask/utils'; -import { concatHex, normalizeAddress, toHexString } from '../internalUtils'; import { + concatHex, + extractAddress, + extractBigInt, + extractRemainingHex, + normalizeAddress, + toHexString, +} from '../internalUtils'; +import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -98,3 +106,24 @@ 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. + * @returns The decoded SpecificActionERC20TransferBatchTerms object. + */ +export function decodeSpecificActionERC20TransferBatchTerms( + terms: BytesLike, +): SpecificActionERC20TransferBatchTerms { + const hexTerms = bytesLikeToHex(terms); + + // Structure: tokenAddress (20 bytes) + recipient (20 bytes) + amount (32 bytes) + target (20 bytes) + calldata (remaining) + const tokenAddress = extractAddress(hexTerms, 0); + const recipient = extractAddress(hexTerms, 20); + const amount = extractBigInt(hexTerms, 40, 32); + const target = extractAddress(hexTerms, 72); + const calldata = extractRemainingHex(hexTerms, 92); + + return { tokenAddress, recipient, amount, target, calldata }; +} diff --git a/packages/delegation-core/src/caveats/timestamp.ts b/packages/delegation-core/src/caveats/timestamp.ts index e6f45998..c8a40106 100644 --- a/packages/delegation-core/src/caveats/timestamp.ts +++ b/packages/delegation-core/src/caveats/timestamp.ts @@ -1,11 +1,13 @@ -import { toHexString } from '../internalUtils'; +import { extractNumber, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, type ResultValue, } from '../returns'; import type { Hex } from '../types'; +import type { BytesLike } from '@metamask/utils'; // Upper bound for timestamps (equivalent to January 1, 10000 CE) const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; @@ -96,3 +98,16 @@ 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); + const timestampAfterThreshold = extractNumber(hexTerms, 0, 16); + const timestampBeforeThreshold = extractNumber(hexTerms, 16, 16); + return { timestampAfterThreshold, timestampBeforeThreshold }; +} diff --git a/packages/delegation-core/src/caveats/valueLte.ts b/packages/delegation-core/src/caveats/valueLte.ts index af5d65b3..933665c8 100644 --- a/packages/delegation-core/src/caveats/valueLte.ts +++ b/packages/delegation-core/src/caveats/valueLte.ts @@ -1,11 +1,13 @@ -import { toHexString } from '../internalUtils'; +import { extractBigInt, toHexString } from '../internalUtils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, type ResultValue, } from '../returns'; import type { Hex } from '../types'; +import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a ValueLte caveat. @@ -52,3 +54,15 @@ 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); + 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..1c0e67a7 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -112,3 +112,80 @@ 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 hex - 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 = ( + hex: string, + offset: number, + size: number, +): bigint => { + const start = 2 + offset * 2; // Skip '0x' prefix + const end = start + size * 2; + const slice = hex.slice(start, end); + return BigInt(`0x${slice}`); +}; + +/** + * Extracts a number value from a hex string at a specific byte offset. + * + * @param hex - 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 = ( + hex: string, + offset: number, + size: number, +): number => { + const start = 2 + offset * 2; // Skip '0x' prefix + const end = start + size * 2; + const slice = hex.slice(start, end); + return parseInt(slice, 16); +}; + +/** + * Extracts an address from a hex string at a specific byte offset. + * + * @param hex - 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 = (hex: string, offset: number): string => { + const start = 2 + offset * 2; // Skip '0x' prefix + const end = start + 40; // 20 bytes = 40 hex chars + return `0x${hex.slice(start, end)}`; +}; + +/** + * Extracts a hex slice from a hex string at a specific byte offset. + * + * @param hex - 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 = (hex: string, offset: number, size: number): string => { + const start = 2 + offset * 2; // Skip '0x' prefix + const end = start + size * 2; + return `0x${hex.slice(start, end)}`; +}; + +/** + * Extracts the remaining hex data from a hex string starting at a specific byte offset. + * + * @param hex - The hex string to extract from. + * @param offset - The byte offset to start extraction. + * @returns The extracted hex string (0x-prefixed). + */ +export const extractRemainingHex = (hex: string, offset: number): string => { + const start = 2 + offset * 2; // Skip '0x' prefix + return `0x${hex.slice(start)}`; +}; 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..a1e06b32 --- /dev/null +++ b/packages/delegation-core/test/caveats/decoders.test.ts @@ -0,0 +1,510 @@ +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 = { + timestampAfterThreshold: 1640995200, + timestampBeforeThreshold: 1672531200, + }; + const encoded = createTimestampTerms(original); + const decoded = decodeTimestampTerms(encoded); + expect(decoded).toEqual(original); + }); + + it('decodes zero thresholds', () => { + const original = { + timestampAfterThreshold: 0, + timestampBeforeThreshold: 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' }; + 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'] }; + const encoded = createAllowedMethodsTerms(original); + const decoded = decodeAllowedMethodsTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeAllowedTargetsTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + targets: [ + '0x1234567890123456789012345678901234567890', + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + ], + }; + const encoded = createAllowedTargetsTerms(original); + const decoded = decodeAllowedTargetsTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeRedeemerTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + redeemers: ['0x1234567890123456789012345678901234567890'], + }; + 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' }; + const encoded = createAllowedCalldataTerms(original); + const decoded = decodeAllowedCalldataTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeArgsEqualityCheckTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { args: '0x1234567890abcdef' }; + const encoded = createArgsEqualityCheckTerms(original); + const decoded = decodeArgsEqualityCheckTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeExactCalldataTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { calldata: '0x70a08231000000000000000000000000' }; + const encoded = createExactCalldataTerms(original); + const decoded = decodeExactCalldataTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeExactExecutionTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + execution: { + target: '0x1234567890123456789012345678901234567890', + value: 1000n, + callData: '0x70a08231', + }, + }; + const encoded = createExactExecutionTerms(original); + const decoded = decodeExactExecutionTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeExactCalldataBatchTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + executions: [ + { + target: '0x1234567890123456789012345678901234567890', + value: 1000n, + callData: '0x70a08231', + }, + { + target: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + value: 2000n, + callData: '0xa9059cbb', + }, + ], + }; + const encoded = createExactCalldataBatchTerms(original); + const decoded = decodeExactCalldataBatchTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeExactExecutionBatchTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + executions: [ + { + target: '0x1234567890123456789012345678901234567890', + value: 1000n, + callData: '0x70a08231', + }, + ], + }; + 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', + amount: 1000000000000000000n, + }; + const encoded = createNativeTokenPaymentTerms(original); + const decoded = decodeNativeTokenPaymentTerms(encoded); + expect(decoded.recipient.toLowerCase()).toEqual( + original.recipient.toLowerCase(), + ); + expect(decoded.amount).toEqual(original.amount); + }); + }); + + describe('decodeNativeBalanceChangeTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + recipient: '0x1234567890123456789012345678901234567890', + balance: 1000000000000000000n, + changeType: 0, + }; + const encoded = createNativeBalanceChangeTerms(original); + const decoded = decodeNativeBalanceChangeTerms(encoded); + expect(decoded.recipient.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', + 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', + recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + balance: 1000000000000000000n, + changeType: 0, + }; + const encoded = createERC20BalanceChangeTerms(original); + const decoded = decodeERC20BalanceChangeTerms(encoded); + expect(decoded.tokenAddress.toLowerCase()).toEqual( + original.tokenAddress.toLowerCase(), + ); + expect(decoded.recipient.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', + periodAmount: 1000000000000000000n, + periodDuration: 86400, + startDate: 1640995200, + }; + const encoded = createERC20TokenPeriodTransferTerms(original); + const decoded = decodeERC20TokenPeriodTransferTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeERC20StreamingTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenAddress: '0x1234567890123456789012345678901234567890', + 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', + 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', + recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + amount: 5n, + changeType: 0, + }; + const encoded = createERC721BalanceChangeTerms(original); + const decoded = decodeERC721BalanceChangeTerms(encoded); + expect(decoded.tokenAddress.toLowerCase()).toEqual( + original.tokenAddress.toLowerCase(), + ); + expect(decoded.recipient.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', + recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + tokenId: 123n, + balance: 1000n, + changeType: 0, + }; + const encoded = createERC1155BalanceChangeTerms(original); + const decoded = decodeERC1155BalanceChangeTerms(encoded); + expect(decoded.tokenAddress.toLowerCase()).toEqual( + original.tokenAddress.toLowerCase(), + ); + expect(decoded.recipient.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', + salt: '0x1234', + bytecode: '0x608060405234801561001057600080fd5b50', + }; + const encoded = createDeployedTerms(original); + const decoded = decodeDeployedTerms(encoded); + expect(decoded.contractAddress).toEqual(original.contractAddress); + expect(decoded.salt.toLowerCase()).toEqual( + '0x0000000000000000000000000000000000000000000000000000000000001234', + ); + expect(decoded.bytecode).toEqual(original.bytecode); + }); + }); + + describe('decodeOwnershipTransferTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + contractAddress: '0x1234567890123456789012345678901234567890', + }; + const encoded = createOwnershipTransferTerms(original); + const decoded = decodeOwnershipTransferTerms(encoded); + expect(decoded).toEqual(original); + }); + }); + + describe('decodeMultiTokenPeriodTerms', () => { + it('correctly decodes encoded terms', () => { + const original = { + tokenConfigs: [ + { + token: '0x1234567890123456789012345678901234567890', + periodAmount: 1000000000000000000n, + periodDuration: 86400, + startDate: 1640995200, + }, + { + token: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + 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', + recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + amount: 1000000000000000000n, + target: '0x9876543210987654321098765432109876543210', + calldata: '0x70a08231', + }; + const encoded = createSpecificActionERC20TransferBatchTerms(original); + const decoded = decodeSpecificActionERC20TransferBatchTerms(encoded); + expect(decoded).toEqual(original); + }); + }); +}); From 07c46593368a864df3862665723d86ba939966f8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 23 Mar 2026 22:06:43 +0000 Subject: [PATCH 02/15] fix: add proper type annotations to decoder functions - Cast extractAddress, extractHex, and extractRemainingHex return types to `0x${string}` - Update array declarations in decoders to use proper `0x${string}[]` types - Cast target addresses in ABI decoded results - Add type assertions in tests for BytesLike comparisons - All TypeScript build errors resolved - All 435 tests still passing Co-authored-by: jeffsmale90 --- .../src/caveats/allowedMethods.ts | 2 +- .../src/caveats/allowedTargets.ts | 2 +- .../src/caveats/exactCalldataBatch.ts | 2 +- .../src/caveats/exactExecutionBatch.ts | 2 +- .../delegation-core/src/caveats/redeemer.ts | 2 +- packages/delegation-core/src/internalUtils.ts | 12 +-- .../test/caveats/decoders.test.ts | 92 +++++++++---------- 7 files changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index 785c3df1..4eee903d 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -94,7 +94,7 @@ export function decodeAllowedMethodsTerms( const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const selectorCount = totalBytes / selectorSize; - const selectors: string[] = []; + const selectors: `0x${string}`[] = []; for (let i = 0; i < selectorCount; i++) { const selector = extractHex(hexTerms, i * selectorSize, selectorSize); selectors.push(selector); diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index a06d1555..7805389b 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -78,7 +78,7 @@ export function decodeAllowedTargetsTerms( const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const addressCount = totalBytes / addressSize; - const targets: string[] = []; + const targets: `0x${string}`[] = []; for (let i = 0; i < addressCount; i++) { const target = extractAddress(hexTerms, i * addressSize); targets.push(target); diff --git a/packages/delegation-core/src/caveats/exactCalldataBatch.ts b/packages/delegation-core/src/caveats/exactCalldataBatch.ts index d311c310..6b940ec2 100644 --- a/packages/delegation-core/src/caveats/exactCalldataBatch.ts +++ b/packages/delegation-core/src/caveats/exactCalldataBatch.ts @@ -104,7 +104,7 @@ export function decodeExactCalldataBatchTerms( const executions = (decoded as [string, bigint, Uint8Array][]).map( ([target, value, callData]) => ({ - target, + target: target as `0x${string}`, value, callData: bytesToHex(callData), }), diff --git a/packages/delegation-core/src/caveats/exactExecutionBatch.ts b/packages/delegation-core/src/caveats/exactExecutionBatch.ts index ea1d64e4..6e0aeec8 100644 --- a/packages/delegation-core/src/caveats/exactExecutionBatch.ts +++ b/packages/delegation-core/src/caveats/exactExecutionBatch.ts @@ -104,7 +104,7 @@ export function decodeExactExecutionBatchTerms( const executions = (decoded as [string, bigint, Uint8Array][]).map( ([target, value, callData]) => ({ - target, + target: target as `0x${string}`, value, callData: bytesToHex(callData), }), diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index 12daebc1..973618e6 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -76,7 +76,7 @@ export function decodeRedeemerTerms(terms: BytesLike): RedeemerTerms { const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const addressCount = totalBytes / addressSize; - const redeemers: string[] = []; + const redeemers: `0x${string}`[] = []; for (let i = 0; i < addressCount; i++) { const redeemer = extractAddress(hexTerms, i * addressSize); redeemers.push(redeemer); diff --git a/packages/delegation-core/src/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts index 1c0e67a7..68666a8c 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -158,10 +158,10 @@ export const extractNumber = ( * @param offset - The byte offset to start extraction. * @returns The extracted address as a 0x-prefixed hex string. */ -export const extractAddress = (hex: string, offset: number): string => { +export const extractAddress = (hex: string, offset: number): `0x${string}` => { const start = 2 + offset * 2; // Skip '0x' prefix const end = start + 40; // 20 bytes = 40 hex chars - return `0x${hex.slice(start, end)}`; + return `0x${hex.slice(start, end)}` as `0x${string}`; }; /** @@ -172,10 +172,10 @@ export const extractAddress = (hex: string, offset: number): string => { * @param size - The number of bytes to extract. * @returns The extracted hex string (0x-prefixed). */ -export const extractHex = (hex: string, offset: number, size: number): string => { +export const extractHex = (hex: string, offset: number, size: number): `0x${string}` => { const start = 2 + offset * 2; // Skip '0x' prefix const end = start + size * 2; - return `0x${hex.slice(start, end)}`; + return `0x${hex.slice(start, end)}` as `0x${string}`; }; /** @@ -185,7 +185,7 @@ export const extractHex = (hex: string, offset: number, size: number): string => * @param offset - The byte offset to start extraction. * @returns The extracted hex string (0x-prefixed). */ -export const extractRemainingHex = (hex: string, offset: number): string => { +export const extractRemainingHex = (hex: string, offset: number): `0x${string}` => { const start = 2 + offset * 2; // Skip '0x' prefix - return `0x${hex.slice(start)}`; + return `0x${hex.slice(start)}` as `0x${string}`; }; diff --git a/packages/delegation-core/test/caveats/decoders.test.ts b/packages/delegation-core/test/caveats/decoders.test.ts index a1e06b32..b7182c0c 100644 --- a/packages/delegation-core/test/caveats/decoders.test.ts +++ b/packages/delegation-core/test/caveats/decoders.test.ts @@ -133,7 +133,7 @@ describe('Terms Decoders', () => { describe('decodeNonceTerms', () => { it('correctly decodes encoded terms', () => { - const original = { nonce: '0x1234' }; + const original = { nonce: '0x1234' as `0x${string}` }; const encoded = createNonceTerms(original); const decoded = decodeNonceTerms(encoded); expect(decoded.nonce).toEqual(encoded); @@ -142,7 +142,7 @@ describe('Terms Decoders', () => { describe('decodeAllowedMethodsTerms', () => { it('correctly decodes encoded terms', () => { - const original = { selectors: ['0x70a08231', '0xa9059cbb'] }; + const original = { selectors: ['0x70a08231', '0xa9059cbb'] as `0x${string}`[] }; const encoded = createAllowedMethodsTerms(original); const decoded = decodeAllowedMethodsTerms(encoded); expect(decoded).toEqual(original); @@ -155,7 +155,7 @@ describe('Terms Decoders', () => { targets: [ '0x1234567890123456789012345678901234567890', '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', - ], + ] as `0x${string}`[], }; const encoded = createAllowedTargetsTerms(original); const decoded = decodeAllowedTargetsTerms(encoded); @@ -166,7 +166,7 @@ describe('Terms Decoders', () => { describe('decodeRedeemerTerms', () => { it('correctly decodes encoded terms', () => { const original = { - redeemers: ['0x1234567890123456789012345678901234567890'], + redeemers: ['0x1234567890123456789012345678901234567890'] as `0x${string}`[], }; const encoded = createRedeemerTerms(original); const decoded = decodeRedeemerTerms(encoded); @@ -176,7 +176,7 @@ describe('Terms Decoders', () => { describe('decodeAllowedCalldataTerms', () => { it('correctly decodes encoded terms', () => { - const original = { startIndex: 4, value: '0x1234' }; + const original = { startIndex: 4, value: '0x1234' as `0x${string}` }; const encoded = createAllowedCalldataTerms(original); const decoded = decodeAllowedCalldataTerms(encoded); expect(decoded).toEqual(original); @@ -185,7 +185,7 @@ describe('Terms Decoders', () => { describe('decodeArgsEqualityCheckTerms', () => { it('correctly decodes encoded terms', () => { - const original = { args: '0x1234567890abcdef' }; + const original = { args: '0x1234567890abcdef' as `0x${string}` }; const encoded = createArgsEqualityCheckTerms(original); const decoded = decodeArgsEqualityCheckTerms(encoded); expect(decoded).toEqual(original); @@ -194,7 +194,7 @@ describe('Terms Decoders', () => { describe('decodeExactCalldataTerms', () => { it('correctly decodes encoded terms', () => { - const original = { calldata: '0x70a08231000000000000000000000000' }; + const original = { calldata: '0x70a08231000000000000000000000000' as `0x${string}` }; const encoded = createExactCalldataTerms(original); const decoded = decodeExactCalldataTerms(encoded); expect(decoded).toEqual(original); @@ -205,9 +205,9 @@ describe('Terms Decoders', () => { it('correctly decodes encoded terms', () => { const original = { execution: { - target: '0x1234567890123456789012345678901234567890', + target: '0x1234567890123456789012345678901234567890' as `0x${string}`, value: 1000n, - callData: '0x70a08231', + callData: '0x70a08231' as `0x${string}`, }, }; const encoded = createExactExecutionTerms(original); @@ -221,14 +221,14 @@ describe('Terms Decoders', () => { const original = { executions: [ { - target: '0x1234567890123456789012345678901234567890', + target: '0x1234567890123456789012345678901234567890' as `0x${string}`, value: 1000n, - callData: '0x70a08231', + callData: '0x70a08231' as `0x${string}`, }, { - target: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + target: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, value: 2000n, - callData: '0xa9059cbb', + callData: '0xa9059cbb' as `0x${string}`, }, ], }; @@ -243,9 +243,9 @@ describe('Terms Decoders', () => { const original = { executions: [ { - target: '0x1234567890123456789012345678901234567890', + target: '0x1234567890123456789012345678901234567890' as `0x${string}`, value: 1000n, - callData: '0x70a08231', + callData: '0x70a08231' as `0x${string}`, }, ], }; @@ -267,12 +267,12 @@ describe('Terms Decoders', () => { describe('decodeNativeTokenPaymentTerms', () => { it('correctly decodes encoded terms', () => { const original = { - recipient: '0x1234567890123456789012345678901234567890', + recipient: '0x1234567890123456789012345678901234567890' as `0x${string}`, amount: 1000000000000000000n, }; const encoded = createNativeTokenPaymentTerms(original); const decoded = decodeNativeTokenPaymentTerms(encoded); - expect(decoded.recipient.toLowerCase()).toEqual( + expect((decoded.recipient as string).toLowerCase()).toEqual( original.recipient.toLowerCase(), ); expect(decoded.amount).toEqual(original.amount); @@ -282,13 +282,13 @@ describe('Terms Decoders', () => { describe('decodeNativeBalanceChangeTerms', () => { it('correctly decodes encoded terms', () => { const original = { - recipient: '0x1234567890123456789012345678901234567890', + recipient: '0x1234567890123456789012345678901234567890' as `0x${string}`, balance: 1000000000000000000n, changeType: 0, }; const encoded = createNativeBalanceChangeTerms(original); const decoded = decodeNativeBalanceChangeTerms(encoded); - expect(decoded.recipient.toLowerCase()).toEqual( + expect((decoded.recipient as string).toLowerCase()).toEqual( original.recipient.toLowerCase(), ); expect(decoded.balance).toEqual(original.balance); @@ -326,7 +326,7 @@ describe('Terms Decoders', () => { describe('decodeERC20TransferAmountTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890', + tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, maxAmount: 1000000000000000000n, }; const encoded = createERC20TransferAmountTerms(original); @@ -338,17 +338,17 @@ describe('Terms Decoders', () => { describe('decodeERC20BalanceChangeTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890', - recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + 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.toLowerCase()).toEqual( + expect((decoded.tokenAddress as string).toLowerCase()).toEqual( original.tokenAddress.toLowerCase(), ); - expect(decoded.recipient.toLowerCase()).toEqual( + expect((decoded.recipient as string).toLowerCase()).toEqual( original.recipient.toLowerCase(), ); expect(decoded.balance).toEqual(original.balance); @@ -359,7 +359,7 @@ describe('Terms Decoders', () => { describe('decodeERC20TokenPeriodTransferTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890', + tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, periodAmount: 1000000000000000000n, periodDuration: 86400, startDate: 1640995200, @@ -373,7 +373,7 @@ describe('Terms Decoders', () => { describe('decodeERC20StreamingTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890', + tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, initialAmount: 1000000000000000000n, maxAmount: 10000000000000000000n, amountPerSecond: 1000000000000000n, @@ -388,7 +388,7 @@ describe('Terms Decoders', () => { describe('decodeERC721TransferTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890', + tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, tokenId: 123n, }; const encoded = createERC721TransferTerms(original); @@ -400,17 +400,17 @@ describe('Terms Decoders', () => { describe('decodeERC721BalanceChangeTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890', - recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + 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.toLowerCase()).toEqual( + expect((decoded.tokenAddress as string).toLowerCase()).toEqual( original.tokenAddress.toLowerCase(), ); - expect(decoded.recipient.toLowerCase()).toEqual( + expect((decoded.recipient as string).toLowerCase()).toEqual( original.recipient.toLowerCase(), ); expect(decoded.amount).toEqual(original.amount); @@ -421,18 +421,18 @@ describe('Terms Decoders', () => { describe('decodeERC1155BalanceChangeTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890', - recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + 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.toLowerCase()).toEqual( + expect((decoded.tokenAddress as string).toLowerCase()).toEqual( original.tokenAddress.toLowerCase(), ); - expect(decoded.recipient.toLowerCase()).toEqual( + expect((decoded.recipient as string).toLowerCase()).toEqual( original.recipient.toLowerCase(), ); expect(decoded.tokenId).toEqual(original.tokenId); @@ -444,14 +444,14 @@ describe('Terms Decoders', () => { describe('decodeDeployedTerms', () => { it('correctly decodes encoded terms', () => { const original = { - contractAddress: '0x1234567890123456789012345678901234567890', - salt: '0x1234', - bytecode: '0x608060405234801561001057600080fd5b50', + 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.toLowerCase()).toEqual( + expect((decoded.salt as string).toLowerCase()).toEqual( '0x0000000000000000000000000000000000000000000000000000000000001234', ); expect(decoded.bytecode).toEqual(original.bytecode); @@ -461,7 +461,7 @@ describe('Terms Decoders', () => { describe('decodeOwnershipTransferTerms', () => { it('correctly decodes encoded terms', () => { const original = { - contractAddress: '0x1234567890123456789012345678901234567890', + contractAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, }; const encoded = createOwnershipTransferTerms(original); const decoded = decodeOwnershipTransferTerms(encoded); @@ -474,13 +474,13 @@ describe('Terms Decoders', () => { const original = { tokenConfigs: [ { - token: '0x1234567890123456789012345678901234567890', + token: '0x1234567890123456789012345678901234567890' as `0x${string}`, periodAmount: 1000000000000000000n, periodDuration: 86400, startDate: 1640995200, }, { - token: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + token: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, periodAmount: 2000000000000000000n, periodDuration: 172800, startDate: 1640995200, @@ -496,11 +496,11 @@ describe('Terms Decoders', () => { describe('decodeSpecificActionERC20TransferBatchTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890', - recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, amount: 1000000000000000000n, - target: '0x9876543210987654321098765432109876543210', - calldata: '0x70a08231', + target: '0x9876543210987654321098765432109876543210' as `0x${string}`, + calldata: '0x70a08231' as `0x${string}`, }; const encoded = createSpecificActionERC20TransferBatchTerms(original); const decoded = decodeSpecificActionERC20TransferBatchTerms(encoded); From a61d6a99cf1b2c76a96ceb6ecf6669d198aa43b5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 23 Mar 2026 22:11:18 +0000 Subject: [PATCH 03/15] fix: resolve linting issues - Remove unused hexToBytes import from nonce.ts - Rename 'hex' parameter to 'hexString' in decoder utility functions to comply with id-denylist eslint rule - All linting checks now pass Co-authored-by: jeffsmale90 --- .../src/caveats/allowedCalldata.ts | 10 ++- .../src/caveats/allowedMethods.ts | 6 +- .../src/caveats/allowedTargets.ts | 6 +- .../src/caveats/blockNumber.ts | 3 +- .../delegation-core/src/caveats/deployed.ts | 4 +- .../src/caveats/erc1155BalanceChange.ts | 4 +- .../src/caveats/erc20BalanceChange.ts | 4 +- .../src/caveats/erc20Streaming.ts | 4 +- .../src/caveats/erc20TokenPeriodTransfer.ts | 4 +- .../src/caveats/erc20TransferAmount.ts | 4 +- .../src/caveats/erc721BalanceChange.ts | 4 +- .../src/caveats/erc721Transfer.ts | 4 +- .../src/caveats/exactCalldata.ts | 4 +- .../src/caveats/exactCalldataBatch.ts | 6 +- .../src/caveats/exactExecution.ts | 4 +- .../src/caveats/exactExecutionBatch.ts | 6 +- packages/delegation-core/src/caveats/id.ts | 3 +- packages/delegation-core/src/caveats/index.ts | 20 ++++- .../src/caveats/limitedCalls.ts | 3 +- .../src/caveats/multiTokenPeriod.ts | 8 +- .../src/caveats/nativeBalanceChange.ts | 4 +- .../src/caveats/nativeTokenPayment.ts | 4 +- .../src/caveats/nativeTokenPeriodTransfer.ts | 7 +- .../src/caveats/nativeTokenStreaming.ts | 7 +- .../src/caveats/nativeTokenTransferAmount.ts | 3 +- packages/delegation-core/src/caveats/nonce.ts | 6 +- .../delegation-core/src/caveats/redeemer.ts | 6 +- .../specificActionERC20TransferBatch.ts | 4 +- .../delegation-core/src/caveats/timestamp.ts | 3 +- .../delegation-core/src/caveats/valueLte.ts | 3 +- packages/delegation-core/src/internalUtils.ts | 40 ++++++---- .../test/caveats/decoders.test.ts | 75 +++++++++++++------ 32 files changed, 166 insertions(+), 107 deletions(-) diff --git a/packages/delegation-core/src/caveats/allowedCalldata.ts b/packages/delegation-core/src/caveats/allowedCalldata.ts index 5208aade..edc45ddb 100644 --- a/packages/delegation-core/src/caveats/allowedCalldata.ts +++ b/packages/delegation-core/src/caveats/allowedCalldata.ts @@ -1,6 +1,10 @@ import { bytesToHex, remove0x, type BytesLike } from '@metamask/utils'; -import { extractNumber, extractRemainingHex, toHexString } from '../internalUtils'; +import { + extractNumber, + extractRemainingHex, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -85,10 +89,10 @@ export function decodeAllowedCalldataTerms( terms: BytesLike, ): AllowedCalldataTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: startIndex (32 bytes) + value (remaining) const startIndex = extractNumber(hexTerms, 0, 32); const value = extractRemainingHex(hexTerms, 32); - + return { startIndex, value }; } diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index 4eee903d..ccd9ad53 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -88,17 +88,17 @@ export function decodeAllowedMethodsTerms( terms: BytesLike, ): AllowedMethodsTerms { const hexTerms = bytesLikeToHex(terms); - + // Each selector is 4 bytes const selectorSize = 4; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const selectorCount = totalBytes / selectorSize; - + const selectors: `0x${string}`[] = []; for (let i = 0; i < selectorCount; i++) { const selector = extractHex(hexTerms, i * selectorSize, selectorSize); selectors.push(selector); } - + return { selectors }; } diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index 7805389b..089a0d19 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -72,17 +72,17 @@ export function decodeAllowedTargetsTerms( terms: BytesLike, ): AllowedTargetsTerms { const hexTerms = bytesLikeToHex(terms); - + // Each address is 20 bytes const addressSize = 20; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const addressCount = totalBytes / addressSize; - + const targets: `0x${string}`[] = []; for (let i = 0; i < addressCount; i++) { const target = extractAddress(hexTerms, i * addressSize); targets.push(target); } - + return { targets }; } diff --git a/packages/delegation-core/src/caveats/blockNumber.ts b/packages/delegation-core/src/caveats/blockNumber.ts index 03a3409f..8d307503 100644 --- a/packages/delegation-core/src/caveats/blockNumber.ts +++ b/packages/delegation-core/src/caveats/blockNumber.ts @@ -1,3 +1,5 @@ +import type { BytesLike } from '@metamask/utils'; + import { extractBigInt, toHexString } from '../internalUtils'; import { bytesLikeToHex, @@ -7,7 +9,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a BlockNumber caveat. diff --git a/packages/delegation-core/src/caveats/deployed.ts b/packages/delegation-core/src/caveats/deployed.ts index d95e4f46..799e0308 100644 --- a/packages/delegation-core/src/caveats/deployed.ts +++ b/packages/delegation-core/src/caveats/deployed.ts @@ -91,11 +91,11 @@ export function createDeployedTerms( */ export function decodeDeployedTerms(terms: BytesLike): DeployedTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: contractAddress (20 bytes) + salt (32 bytes) + bytecode (remaining) const contractAddress = extractAddress(hexTerms, 0); const salt = extractHex(hexTerms, 20, 32); const bytecode = extractRemainingHex(hexTerms, 52); - + return { contractAddress, salt, bytecode }; } diff --git a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts index 2cc7eae9..8c78009f 100644 --- a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts @@ -120,13 +120,13 @@ export function decodeERC1155BalanceChangeTerms( terms: BytesLike, ): ERC1155BalanceChangeTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + tokenId (32 bytes) + balance (32 bytes) const changeType = extractNumber(hexTerms, 0, 1); const tokenAddress = extractAddress(hexTerms, 1); const recipient = extractAddress(hexTerms, 21); const tokenId = extractBigInt(hexTerms, 41, 32); const balance = extractBigInt(hexTerms, 73, 32); - + return { changeType, tokenAddress, recipient, tokenId, balance }; } diff --git a/packages/delegation-core/src/caveats/erc20BalanceChange.ts b/packages/delegation-core/src/caveats/erc20BalanceChange.ts index 5136f6b4..3594c7f1 100644 --- a/packages/delegation-core/src/caveats/erc20BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc20BalanceChange.ts @@ -111,12 +111,12 @@ export function decodeERC20BalanceChangeTerms( terms: BytesLike, ): ERC20BalanceChangeTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + balance (32 bytes) const changeType = extractNumber(hexTerms, 0, 1); const tokenAddress = extractAddress(hexTerms, 1); const recipient = extractAddress(hexTerms, 21); const balance = extractBigInt(hexTerms, 41, 32); - + return { changeType, tokenAddress, recipient, balance }; } diff --git a/packages/delegation-core/src/caveats/erc20Streaming.ts b/packages/delegation-core/src/caveats/erc20Streaming.ts index 2c03fb56..c7ba0832 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -137,13 +137,13 @@ export function decodeERC20StreamingTerms( terms: BytesLike, ): ERC20StreamingTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: tokenAddress (20 bytes) + initialAmount (32 bytes) + maxAmount (32 bytes) + amountPerSecond (32 bytes) + startTime (32 bytes) const tokenAddress = 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, initialAmount, maxAmount, amountPerSecond, startTime }; } diff --git a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts index 93726f60..f4b8d1d2 100644 --- a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -111,12 +111,12 @@ export function decodeERC20TokenPeriodTransferTerms( terms: BytesLike, ): ERC20TokenPeriodTransferTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: tokenAddress (20 bytes) + periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 bytes) const tokenAddress = extractAddress(hexTerms, 0); const periodAmount = extractBigInt(hexTerms, 20, 32); const periodDuration = extractNumber(hexTerms, 52, 32); const startDate = extractNumber(hexTerms, 84, 32); - + return { tokenAddress, periodAmount, periodDuration, startDate }; } diff --git a/packages/delegation-core/src/caveats/erc20TransferAmount.ts b/packages/delegation-core/src/caveats/erc20TransferAmount.ts index aa87c916..0e4d83ec 100644 --- a/packages/delegation-core/src/caveats/erc20TransferAmount.ts +++ b/packages/delegation-core/src/caveats/erc20TransferAmount.ts @@ -81,10 +81,10 @@ export function decodeERC20TransferAmountTerms( terms: BytesLike, ): ERC20TransferAmountTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: tokenAddress (20 bytes) + maxAmount (32 bytes) const tokenAddress = extractAddress(hexTerms, 0); const maxAmount = extractBigInt(hexTerms, 20, 32); - + return { tokenAddress, maxAmount }; } diff --git a/packages/delegation-core/src/caveats/erc721BalanceChange.ts b/packages/delegation-core/src/caveats/erc721BalanceChange.ts index 66c368e3..a9486e70 100644 --- a/packages/delegation-core/src/caveats/erc721BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc721BalanceChange.ts @@ -111,12 +111,12 @@ export function decodeERC721BalanceChangeTerms( terms: BytesLike, ): ERC721BalanceChangeTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + amount (32 bytes) const changeType = extractNumber(hexTerms, 0, 1); const tokenAddress = extractAddress(hexTerms, 1); const recipient = extractAddress(hexTerms, 21); const amount = extractBigInt(hexTerms, 41, 32); - + return { changeType, tokenAddress, recipient, amount }; } diff --git a/packages/delegation-core/src/caveats/erc721Transfer.ts b/packages/delegation-core/src/caveats/erc721Transfer.ts index 11358356..0bd62f3f 100644 --- a/packages/delegation-core/src/caveats/erc721Transfer.ts +++ b/packages/delegation-core/src/caveats/erc721Transfer.ts @@ -81,10 +81,10 @@ export function decodeERC721TransferTerms( terms: BytesLike, ): ERC721TransferTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: tokenAddress (20 bytes) + tokenId (32 bytes) const tokenAddress = extractAddress(hexTerms, 0); const tokenId = extractBigInt(hexTerms, 20, 32); - + return { tokenAddress, tokenId }; } diff --git a/packages/delegation-core/src/caveats/exactCalldata.ts b/packages/delegation-core/src/caveats/exactCalldata.ts index e362e3dc..0eda7d79 100644 --- a/packages/delegation-core/src/caveats/exactCalldata.ts +++ b/packages/delegation-core/src/caveats/exactCalldata.ts @@ -67,9 +67,7 @@ export function createExactCalldataTerms( * @param terms - The encoded terms as a hex string or Uint8Array. * @returns The decoded ExactCalldataTerms object. */ -export function decodeExactCalldataTerms( - terms: BytesLike, -): ExactCalldataTerms { +export function decodeExactCalldataTerms(terms: BytesLike): ExactCalldataTerms { const calldata = bytesLikeToHex(terms); return { calldata }; } diff --git a/packages/delegation-core/src/caveats/exactCalldataBatch.ts b/packages/delegation-core/src/caveats/exactCalldataBatch.ts index 6b940ec2..65251149 100644 --- a/packages/delegation-core/src/caveats/exactCalldataBatch.ts +++ b/packages/delegation-core/src/caveats/exactCalldataBatch.ts @@ -98,10 +98,10 @@ export function decodeExactCalldataBatchTerms( terms: BytesLike, ): ExactCalldataBatchTerms { const hexTerms = bytesLikeToHex(terms); - + // Decode using ABI: (address,uint256,bytes)[] const decoded = decodeSingle(EXECUTION_ARRAY_ABI, hexTerms); - + const executions = (decoded as [string, bigint, Uint8Array][]).map( ([target, value, callData]) => ({ target: target as `0x${string}`, @@ -109,6 +109,6 @@ export function decodeExactCalldataBatchTerms( callData: bytesToHex(callData), }), ); - + return { executions }; } diff --git a/packages/delegation-core/src/caveats/exactExecution.ts b/packages/delegation-core/src/caveats/exactExecution.ts index 6afdcf99..439dac5c 100644 --- a/packages/delegation-core/src/caveats/exactExecution.ts +++ b/packages/delegation-core/src/caveats/exactExecution.ts @@ -96,12 +96,12 @@ export function decodeExactExecutionTerms( terms: BytesLike, ): ExactExecutionTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: target (20 bytes) + value (32 bytes) + callData (remaining) const target = extractAddress(hexTerms, 0); const value = extractBigInt(hexTerms, 20, 32); const callData = extractRemainingHex(hexTerms, 52); - + return { execution: { target, diff --git a/packages/delegation-core/src/caveats/exactExecutionBatch.ts b/packages/delegation-core/src/caveats/exactExecutionBatch.ts index 6e0aeec8..fd254938 100644 --- a/packages/delegation-core/src/caveats/exactExecutionBatch.ts +++ b/packages/delegation-core/src/caveats/exactExecutionBatch.ts @@ -98,10 +98,10 @@ export function decodeExactExecutionBatchTerms( terms: BytesLike, ): ExactExecutionBatchTerms { const hexTerms = bytesLikeToHex(terms); - + // Decode using ABI: (address,uint256,bytes)[] const decoded = decodeSingle(EXECUTION_ARRAY_ABI, hexTerms); - + const executions = (decoded as [string, bigint, Uint8Array][]).map( ([target, value, callData]) => ({ target: target as `0x${string}`, @@ -109,6 +109,6 @@ export function decodeExactExecutionBatchTerms( callData: bytesToHex(callData), }), ); - + return { executions }; } diff --git a/packages/delegation-core/src/caveats/id.ts b/packages/delegation-core/src/caveats/id.ts index 875f2e60..91983292 100644 --- a/packages/delegation-core/src/caveats/id.ts +++ b/packages/delegation-core/src/caveats/id.ts @@ -1,3 +1,5 @@ +import type { BytesLike } from '@metamask/utils'; + import { extractBigInt, toHexString } from '../internalUtils'; import { bytesLikeToHex, @@ -7,7 +9,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import type { BytesLike } from '@metamask/utils'; const MAX_UINT256 = BigInt(`0x${'f'.repeat(64)}`); diff --git a/packages/delegation-core/src/caveats/index.ts b/packages/delegation-core/src/caveats/index.ts index e2027eb3..68d69cf8 100644 --- a/packages/delegation-core/src/caveats/index.ts +++ b/packages/delegation-core/src/caveats/index.ts @@ -4,7 +4,10 @@ export { createNativeTokenPeriodTransferTerms, decodeNativeTokenPeriodTransferTerms, } from './nativeTokenPeriodTransfer'; -export { createExactCalldataTerms, decodeExactCalldataTerms } from './exactCalldata'; +export { + createExactCalldataTerms, + decodeExactCalldataTerms, +} from './exactCalldata'; export { createExactCalldataBatchTerms, decodeExactCalldataBatchTerms, @@ -33,7 +36,10 @@ export { createNativeBalanceChangeTerms, decodeNativeBalanceChangeTerms, } from './nativeBalanceChange'; -export { createERC20StreamingTerms, decodeERC20StreamingTerms } from './erc20Streaming'; +export { + createERC20StreamingTerms, + decodeERC20StreamingTerms, +} from './erc20Streaming'; export { createERC20TokenPeriodTransferTerms, decodeERC20TokenPeriodTransferTerms, @@ -50,7 +56,10 @@ export { createERC721BalanceChangeTerms, decodeERC721BalanceChangeTerms, } from './erc721BalanceChange'; -export { createERC721TransferTerms, decodeERC721TransferTerms } from './erc721Transfer'; +export { + createERC721TransferTerms, + decodeERC721TransferTerms, +} from './erc721Transfer'; export { createERC1155BalanceChangeTerms, decodeERC1155BalanceChangeTerms, @@ -75,7 +84,10 @@ export { export { createBlockNumberTerms, decodeBlockNumberTerms } from './blockNumber'; export { createDeployedTerms, decodeDeployedTerms } from './deployed'; export { createIdTerms, decodeIdTerms } from './id'; -export { createLimitedCallsTerms, decodeLimitedCallsTerms } from './limitedCalls'; +export { + createLimitedCallsTerms, + decodeLimitedCallsTerms, +} from './limitedCalls'; export { createMultiTokenPeriodTerms, decodeMultiTokenPeriodTerms, diff --git a/packages/delegation-core/src/caveats/limitedCalls.ts b/packages/delegation-core/src/caveats/limitedCalls.ts index 76107167..57b24645 100644 --- a/packages/delegation-core/src/caveats/limitedCalls.ts +++ b/packages/delegation-core/src/caveats/limitedCalls.ts @@ -1,3 +1,5 @@ +import type { BytesLike } from '@metamask/utils'; + import { extractNumber, toHexString } from '../internalUtils'; import { bytesLikeToHex, @@ -7,7 +9,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a LimitedCalls caveat. diff --git a/packages/delegation-core/src/caveats/multiTokenPeriod.ts b/packages/delegation-core/src/caveats/multiTokenPeriod.ts index d5f4915f..7c096495 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -112,12 +112,12 @@ export function decodeMultiTokenPeriodTerms( terms: BytesLike, ): MultiTokenPeriodTerms { const hexTerms = bytesLikeToHex(terms); - + // Each token config is: token (20 bytes) + periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 bytes) = 116 bytes const configSize = 116; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const configCount = totalBytes / configSize; - + const tokenConfigs: TokenPeriodConfig[] = []; for (let i = 0; i < configCount; i++) { const offset = i * configSize; @@ -125,9 +125,9 @@ export function decodeMultiTokenPeriodTerms( const periodAmount = extractBigInt(hexTerms, offset + 20, 32); const periodDuration = extractNumber(hexTerms, offset + 52, 32); const startDate = extractNumber(hexTerms, offset + 84, 32); - + tokenConfigs.push({ token, periodAmount, periodDuration, startDate }); } - + return { tokenConfigs }; } diff --git a/packages/delegation-core/src/caveats/nativeBalanceChange.ts b/packages/delegation-core/src/caveats/nativeBalanceChange.ts index 66e09941..12fc5e18 100644 --- a/packages/delegation-core/src/caveats/nativeBalanceChange.ts +++ b/packages/delegation-core/src/caveats/nativeBalanceChange.ts @@ -95,11 +95,11 @@ export function decodeNativeBalanceChangeTerms( terms: BytesLike, ): NativeBalanceChangeTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: changeType (1 byte) + recipient (20 bytes) + balance (32 bytes) const changeType = extractNumber(hexTerms, 0, 1); const recipient = extractAddress(hexTerms, 1); const balance = extractBigInt(hexTerms, 21, 32); - + return { changeType, recipient, balance }; } diff --git a/packages/delegation-core/src/caveats/nativeTokenPayment.ts b/packages/delegation-core/src/caveats/nativeTokenPayment.ts index f6fdc7c6..a166dd79 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPayment.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPayment.ts @@ -81,10 +81,10 @@ export function decodeNativeTokenPaymentTerms( terms: BytesLike, ): NativeTokenPaymentTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: recipient (20 bytes) + amount (32 bytes) const recipient = extractAddress(hexTerms, 0); const amount = extractBigInt(hexTerms, 20, 32); - + return { recipient, amount }; } diff --git a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts index fb635aa0..3e4e501a 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts @@ -1,3 +1,5 @@ +import type { BytesLike } from '@metamask/utils'; + import { extractBigInt, extractNumber, toHexString } from '../internalUtils'; import { bytesLikeToHex, @@ -7,7 +9,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a periodic transfer allowance of native tokens. @@ -85,11 +86,11 @@ export function decodeNativeTokenPeriodTransferTerms( terms: BytesLike, ): NativeTokenPeriodTransferTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 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 f36a89b5..25123def 100644 --- a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts +++ b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts @@ -1,3 +1,5 @@ +import type { BytesLike } from '@metamask/utils'; + import { extractBigInt, extractNumber, toHexString } from '../internalUtils'; import { bytesLikeToHex, @@ -7,7 +9,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import type { BytesLike } from '@metamask/utils'; // Upper bound for timestamps (January 1, 10000 CE) const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; @@ -109,12 +110,12 @@ export function decodeNativeTokenStreamingTerms( terms: BytesLike, ): NativeTokenStreamingTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: initialAmount (32 bytes) + maxAmount (32 bytes) + amountPerSecond (32 bytes) + startTime (32 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 55f58d09..516034a7 100644 --- a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts +++ b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts @@ -1,3 +1,5 @@ +import type { BytesLike } from '@metamask/utils'; + import { extractBigInt, toHexString } from '../internalUtils'; import { bytesLikeToHex, @@ -7,7 +9,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a NativeTokenTransferAmount caveat. diff --git a/packages/delegation-core/src/caveats/nonce.ts b/packages/delegation-core/src/caveats/nonce.ts index 97b529b9..5bb208b7 100644 --- a/packages/delegation-core/src/caveats/nonce.ts +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -1,4 +1,4 @@ -import { hexToBytes, isHexString } from '@metamask/utils'; +import { isHexString } from '@metamask/utils'; import type { BytesLike } from '@metamask/utils'; import { @@ -93,10 +93,10 @@ export function createNonceTerms( */ export function decodeNonceTerms(terms: BytesLike): NonceTerms { const hexTerms = bytesLikeToHex(terms); - + // The nonce is stored as a 32-byte padded value // We return it as-is (padded) to maintain consistency const nonce = hexTerms; - + return { nonce }; } diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index 973618e6..cf5f42fd 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -70,17 +70,17 @@ export function createRedeemerTerms( */ export function decodeRedeemerTerms(terms: BytesLike): RedeemerTerms { const hexTerms = bytesLikeToHex(terms); - + // Each address is 20 bytes const addressSize = 20; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const addressCount = totalBytes / addressSize; - + const redeemers: `0x${string}`[] = []; for (let i = 0; i < addressCount; i++) { const redeemer = extractAddress(hexTerms, i * addressSize); redeemers.push(redeemer); } - + return { redeemers }; } diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts index 4c36f813..57ad668b 100644 --- a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -117,13 +117,13 @@ export function decodeSpecificActionERC20TransferBatchTerms( terms: BytesLike, ): SpecificActionERC20TransferBatchTerms { const hexTerms = bytesLikeToHex(terms); - + // Structure: tokenAddress (20 bytes) + recipient (20 bytes) + amount (32 bytes) + target (20 bytes) + calldata (remaining) const tokenAddress = extractAddress(hexTerms, 0); const recipient = extractAddress(hexTerms, 20); const amount = extractBigInt(hexTerms, 40, 32); const target = extractAddress(hexTerms, 72); const calldata = extractRemainingHex(hexTerms, 92); - + return { tokenAddress, recipient, amount, target, calldata }; } diff --git a/packages/delegation-core/src/caveats/timestamp.ts b/packages/delegation-core/src/caveats/timestamp.ts index c8a40106..9356e8f5 100644 --- a/packages/delegation-core/src/caveats/timestamp.ts +++ b/packages/delegation-core/src/caveats/timestamp.ts @@ -1,3 +1,5 @@ +import type { BytesLike } from '@metamask/utils'; + import { extractNumber, toHexString } from '../internalUtils'; import { bytesLikeToHex, @@ -7,7 +9,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import type { BytesLike } from '@metamask/utils'; // Upper bound for timestamps (equivalent to January 1, 10000 CE) const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799; diff --git a/packages/delegation-core/src/caveats/valueLte.ts b/packages/delegation-core/src/caveats/valueLte.ts index 933665c8..138728d0 100644 --- a/packages/delegation-core/src/caveats/valueLte.ts +++ b/packages/delegation-core/src/caveats/valueLte.ts @@ -1,3 +1,5 @@ +import type { BytesLike } from '@metamask/utils'; + import { extractBigInt, toHexString } from '../internalUtils'; import { bytesLikeToHex, @@ -7,7 +9,6 @@ import { type ResultValue, } from '../returns'; import type { Hex } from '../types'; -import type { BytesLike } from '@metamask/utils'; /** * Terms for configuring a ValueLte caveat. diff --git a/packages/delegation-core/src/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts index 68666a8c..1aa3ed94 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -116,76 +116,86 @@ export const concatHex = (parts: string[]): string => { /** * Extracts a bigint value from a hex string at a specific byte offset. * - * @param hex - The hex string to extract from. + * @param hexString - 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 = ( - hex: string, + hexString: string, offset: number, size: number, ): bigint => { const start = 2 + offset * 2; // Skip '0x' prefix const end = start + size * 2; - const slice = hex.slice(start, end); + const slice = hexString.slice(start, end); return BigInt(`0x${slice}`); }; /** * Extracts a number value from a hex string at a specific byte offset. * - * @param hex - The hex string to extract from. + * @param hexString - 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 = ( - hex: string, + hexString: string, offset: number, size: number, ): number => { const start = 2 + offset * 2; // Skip '0x' prefix const end = start + size * 2; - const slice = hex.slice(start, end); + const slice = hexString.slice(start, end); return parseInt(slice, 16); }; /** * Extracts an address from a hex string at a specific byte offset. * - * @param hex - The hex string to extract from. + * @param hexString - 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 = (hex: string, offset: number): `0x${string}` => { +export const extractAddress = ( + hexString: string, + offset: number, +): `0x${string}` => { const start = 2 + offset * 2; // Skip '0x' prefix const end = start + 40; // 20 bytes = 40 hex chars - return `0x${hex.slice(start, end)}` as `0x${string}`; + return `0x${hexString.slice(start, end)}`; }; /** * Extracts a hex slice from a hex string at a specific byte offset. * - * @param hex - The hex string to extract from. + * @param hexString - 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 = (hex: string, offset: number, size: number): `0x${string}` => { +export const extractHex = ( + hexString: string, + offset: number, + size: number, +): `0x${string}` => { const start = 2 + offset * 2; // Skip '0x' prefix const end = start + size * 2; - return `0x${hex.slice(start, end)}` as `0x${string}`; + return `0x${hexString.slice(start, end)}`; }; /** * Extracts the remaining hex data from a hex string starting at a specific byte offset. * - * @param hex - The hex string to extract from. + * @param hexString - The hex string to extract from. * @param offset - The byte offset to start extraction. * @returns The extracted hex string (0x-prefixed). */ -export const extractRemainingHex = (hex: string, offset: number): `0x${string}` => { +export const extractRemainingHex = ( + hexString: string, + offset: number, +): `0x${string}` => { const start = 2 + offset * 2; // Skip '0x' prefix - return `0x${hex.slice(start)}` as `0x${string}`; + return `0x${hexString.slice(start)}`; }; diff --git a/packages/delegation-core/test/caveats/decoders.test.ts b/packages/delegation-core/test/caveats/decoders.test.ts index b7182c0c..9f15237b 100644 --- a/packages/delegation-core/test/caveats/decoders.test.ts +++ b/packages/delegation-core/test/caveats/decoders.test.ts @@ -142,7 +142,9 @@ describe('Terms Decoders', () => { describe('decodeAllowedMethodsTerms', () => { it('correctly decodes encoded terms', () => { - const original = { selectors: ['0x70a08231', '0xa9059cbb'] as `0x${string}`[] }; + const original = { + selectors: ['0x70a08231', '0xa9059cbb'] as `0x${string}`[], + }; const encoded = createAllowedMethodsTerms(original); const decoded = decodeAllowedMethodsTerms(encoded); expect(decoded).toEqual(original); @@ -166,7 +168,9 @@ describe('Terms Decoders', () => { describe('decodeRedeemerTerms', () => { it('correctly decodes encoded terms', () => { const original = { - redeemers: ['0x1234567890123456789012345678901234567890'] as `0x${string}`[], + redeemers: [ + '0x1234567890123456789012345678901234567890', + ] as `0x${string}`[], }; const encoded = createRedeemerTerms(original); const decoded = decodeRedeemerTerms(encoded); @@ -194,7 +198,9 @@ describe('Terms Decoders', () => { describe('decodeExactCalldataTerms', () => { it('correctly decodes encoded terms', () => { - const original = { calldata: '0x70a08231000000000000000000000000' as `0x${string}` }; + const original = { + calldata: '0x70a08231000000000000000000000000' as `0x${string}`, + }; const encoded = createExactCalldataTerms(original); const decoded = decodeExactCalldataTerms(encoded); expect(decoded).toEqual(original); @@ -221,12 +227,14 @@ describe('Terms Decoders', () => { const original = { executions: [ { - target: '0x1234567890123456789012345678901234567890' as `0x${string}`, + target: + '0x1234567890123456789012345678901234567890' as `0x${string}`, value: 1000n, callData: '0x70a08231' as `0x${string}`, }, { - target: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + target: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, value: 2000n, callData: '0xa9059cbb' as `0x${string}`, }, @@ -243,7 +251,8 @@ describe('Terms Decoders', () => { const original = { executions: [ { - target: '0x1234567890123456789012345678901234567890' as `0x${string}`, + target: + '0x1234567890123456789012345678901234567890' as `0x${string}`, value: 1000n, callData: '0x70a08231' as `0x${string}`, }, @@ -267,7 +276,8 @@ describe('Terms Decoders', () => { describe('decodeNativeTokenPaymentTerms', () => { it('correctly decodes encoded terms', () => { const original = { - recipient: '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0x1234567890123456789012345678901234567890' as `0x${string}`, amount: 1000000000000000000n, }; const encoded = createNativeTokenPaymentTerms(original); @@ -282,7 +292,8 @@ describe('Terms Decoders', () => { describe('decodeNativeBalanceChangeTerms', () => { it('correctly decodes encoded terms', () => { const original = { - recipient: '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0x1234567890123456789012345678901234567890' as `0x${string}`, balance: 1000000000000000000n, changeType: 0, }; @@ -326,7 +337,8 @@ describe('Terms Decoders', () => { describe('decodeERC20TransferAmountTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, maxAmount: 1000000000000000000n, }; const encoded = createERC20TransferAmountTerms(original); @@ -338,8 +350,10 @@ describe('Terms Decoders', () => { describe('decodeERC20BalanceChangeTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, - recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, balance: 1000000000000000000n, changeType: 0, }; @@ -359,7 +373,8 @@ describe('Terms Decoders', () => { describe('decodeERC20TokenPeriodTransferTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, periodAmount: 1000000000000000000n, periodDuration: 86400, startDate: 1640995200, @@ -373,7 +388,8 @@ describe('Terms Decoders', () => { describe('decodeERC20StreamingTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, initialAmount: 1000000000000000000n, maxAmount: 10000000000000000000n, amountPerSecond: 1000000000000000n, @@ -388,7 +404,8 @@ describe('Terms Decoders', () => { describe('decodeERC721TransferTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, tokenId: 123n, }; const encoded = createERC721TransferTerms(original); @@ -400,8 +417,10 @@ describe('Terms Decoders', () => { describe('decodeERC721BalanceChangeTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, - recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, amount: 5n, changeType: 0, }; @@ -421,8 +440,10 @@ describe('Terms Decoders', () => { describe('decodeERC1155BalanceChangeTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, - recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, tokenId: 123n, balance: 1000n, changeType: 0, @@ -444,7 +465,8 @@ describe('Terms Decoders', () => { describe('decodeDeployedTerms', () => { it('correctly decodes encoded terms', () => { const original = { - contractAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, + contractAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, salt: '0x1234' as `0x${string}`, bytecode: '0x608060405234801561001057600080fd5b50' as `0x${string}`, }; @@ -461,7 +483,8 @@ describe('Terms Decoders', () => { describe('decodeOwnershipTransferTerms', () => { it('correctly decodes encoded terms', () => { const original = { - contractAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, + contractAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, }; const encoded = createOwnershipTransferTerms(original); const decoded = decodeOwnershipTransferTerms(encoded); @@ -474,13 +497,15 @@ describe('Terms Decoders', () => { const original = { tokenConfigs: [ { - token: '0x1234567890123456789012345678901234567890' as `0x${string}`, + token: + '0x1234567890123456789012345678901234567890' as `0x${string}`, periodAmount: 1000000000000000000n, periodDuration: 86400, startDate: 1640995200, }, { - token: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + token: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, periodAmount: 2000000000000000000n, periodDuration: 172800, startDate: 1640995200, @@ -496,8 +521,10 @@ describe('Terms Decoders', () => { describe('decodeSpecificActionERC20TransferBatchTerms', () => { it('correctly decodes encoded terms', () => { const original = { - tokenAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`, - recipient: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, + tokenAddress: + '0x1234567890123456789012345678901234567890' as `0x${string}`, + recipient: + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as `0x${string}`, amount: 1000000000000000000n, target: '0x9876543210987654321098765432109876543210' as `0x${string}`, calldata: '0x70a08231' as `0x${string}`, From e906d5abc09e6f9795aa8f7f965032149ff34a29 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:09:02 +1300 Subject: [PATCH 04/15] Update internal utils: - change type string to Hex - remove unnecessary comments - change parameter name hexString to value - add test coverage for new utilities --- packages/delegation-core/src/internalUtils.ts | 56 +++++---- .../test/internalUtils.test.ts | 111 ++++++++++++++++++ 2 files changed, 142 insertions(+), 25 deletions(-) diff --git a/packages/delegation-core/src/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts index 1aa3ed94..f0e325b2 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -1,5 +1,6 @@ import { bytesToHex, + Hex, hexToBytes, isHexString, remove0x, @@ -116,86 +117,91 @@ export const concatHex = (parts: string[]): string => { /** * Extracts a bigint value from a hex string at a specific byte offset. * - * @param hexString - The hex string to extract from. + * @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 = ( - hexString: string, + value: string, offset: number, size: number, ): bigint => { - const start = 2 + offset * 2; // Skip '0x' prefix + const start = 2 + offset * 2; const end = start + size * 2; - const slice = hexString.slice(start, end); + const slice = value.slice(start, end); + return BigInt(`0x${slice}`); }; /** * Extracts a number value from a hex string at a specific byte offset. * - * @param hexString - The hex string to extract from. + * @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 = ( - hexString: string, + value: Hex, offset: number, size: number, ): number => { - const start = 2 + offset * 2; // Skip '0x' prefix + const start = 2 + offset * 2; const end = start + size * 2; - const slice = hexString.slice(start, end); - return parseInt(slice, 16); + const slice = value.slice(start, end); + + return Number.parseInt(slice, 16); }; /** * Extracts an address from a hex string at a specific byte offset. * - * @param hexString - The hex string to extract from. + * @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 = ( - hexString: string, + value: Hex, offset: number, -): `0x${string}` => { - const start = 2 + offset * 2; // Skip '0x' prefix - const end = start + 40; // 20 bytes = 40 hex chars - return `0x${hexString.slice(start, end)}`; +): 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 hexString - The hex string to extract from. + * @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 = ( - hexString: string, + value: Hex, offset: number, size: number, -): `0x${string}` => { - const start = 2 + offset * 2; // Skip '0x' prefix +): Hex => { + const start = 2 + offset * 2; const end = start + size * 2; - return `0x${hexString.slice(start, end)}`; + + return `0x${value.slice(start, end)}`; }; /** * Extracts the remaining hex data from a hex string starting at a specific byte offset. * - * @param hexString - The hex string to extract from. + * @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 = ( - hexString: string, + value: Hex, offset: number, -): `0x${string}` => { - const start = 2 + offset * 2; // Skip '0x' prefix - return `0x${hexString.slice(start)}`; +): Hex => { + const start = 2 + offset * 2; + + return `0x${value.slice(start)}`; }; diff --git a/packages/delegation-core/test/internalUtils.test.ts b/packages/delegation-core/test/internalUtils.test.ts index c46989c1..eeea35a7 100644 --- a/packages/delegation-core/test/internalUtils.test.ts +++ b/packages/delegation-core/test/internalUtils.test.ts @@ -2,10 +2,17 @@ import { describe, it, expect } from 'vitest'; import { concatHex, + extractAddress, + extractBigInt, + extractHex, + extractNumber, + extractRemainingHex, normalizeAddress, normalizeAddressLowercase, normalizeHex, + toHexString, } from '../src/internalUtils'; +import type { Hex } from '../src/types'; describe('internal utils', () => { describe('normalizeHex', () => { @@ -84,4 +91,108 @@ 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 hex = + '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' as const; + + it('reads uint256 at offset 0', () => { + expect(extractBigInt(hex, 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); + }); + }); + + 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'); + }); + }); }); From 8e91178e14b0cd2762ea0793b00cec721bd47606 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:10:31 +1300 Subject: [PATCH 05/15] Add test coverage for terms decoders --- .../test/caveats/allowedCalldata.test.ts | 716 ++++---- .../test/caveats/allowedMethods.test.ts | 121 +- .../test/caveats/allowedTargets.test.ts | 97 +- .../test/caveats/argsEqualityCheck.test.ts | 65 +- .../test/caveats/blockNumber.test.ts | 109 +- .../test/caveats/deployed.test.ts | 118 +- .../test/caveats/erc1155BalanceChange.test.ts | 136 +- .../test/caveats/erc20BalanceChange.test.ts | 137 +- .../test/caveats/erc20Streaming.test.ts | 1519 +++++++++-------- .../test/caveats/erc20TransferAmount.test.ts | 88 +- .../test/caveats/erc721BalanceChange.test.ts | 132 +- .../test/caveats/erc721Transfer.test.ts | 95 +- .../test/caveats/exactCalldata.test.ts | 466 ++--- .../test/caveats/exactCalldataBatch.test.ts | 95 +- .../test/caveats/exactExecution.test.ts | 144 +- .../test/caveats/exactExecutionBatch.test.ts | 101 +- .../delegation-core/test/caveats/id.test.ts | 98 +- .../test/caveats/limitedCalls.test.ts | 65 +- .../test/caveats/multiTokenPeriod.test.ts | 216 ++- .../test/caveats/nativeBalanceChange.test.ts | 137 +- .../test/caveats/nativeTokenPayment.test.ts | 91 +- .../caveats/nativeTokenPeriodTransfer.test.ts | 574 ++++--- .../test/caveats/nativeTokenStreaming.test.ts | 1023 ++++++----- .../caveats/nativeTokenTransferAmount.test.ts | 78 +- .../test/caveats/nonce.test.ts | 732 ++++---- .../test/caveats/ownershipTransfer.test.ts | 54 +- .../test/caveats/redeemer.test.ts | 85 +- .../specificActionERC20TransferBatch.test.ts | 156 +- .../test/caveats/timestamp.test.ts | 564 +++--- .../test/caveats/valueLte.test.ts | 244 +-- 30 files changed, 4765 insertions(+), 3491 deletions(-) diff --git a/packages/delegation-core/test/caveats/allowedCalldata.test.ts b/packages/delegation-core/test/caveats/allowedCalldata.test.ts index 8709e653..c2728e57 100644 --- a/packages/delegation-core/test/caveats/allowedCalldata.test.ts +++ b/packages/delegation-core/test/caveats/allowedCalldata.test.ts @@ -1,380 +1,454 @@ 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)); - }); - - 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 complex value', () => { - const value = - '0x23b872dd000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5f0000000000000000000000000000000000000000000000000de0b6b3a7640000'; - const startIndex = 4; - 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 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 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 very long value', () => { - const longValue: Hex = `0x${'a'.repeat(1000)}`; - const startIndex = 31; - const result = createAllowedCalldataTerms({ - startIndex, - value: longValue, + // 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)); }); - 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('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 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 undefined value', () => { - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: undefined as unknown as Hex, - }), - ).toThrow(); - }); - it('throws an error for null value', () => { - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: null as unknown as Hex, - }), - ).toThrow(); - }); - - it('throws an error for non-string non-Uint8Array value', () => { - expect(() => - createAllowedCalldataTerms({ - startIndex: 0, - value: 1234 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('creates valid terms for empty value', () => { + const value = '0x'; + const startIndex = 5; + const result = createAllowedCalldataTerms({ startIndex, value }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); }); - 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('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)); }); - 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' }, - ); - // 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('creates valid terms for complex value', () => { + const value = + '0x23b872dd000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5f0000000000000000000000000000000000000000000000000de0b6b3a7640000'; + const startIndex = 4; + const result = createAllowedCalldataTerms({ startIndex, value }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); }); - it('returns Uint8Array for empty value with bytes encoding', () => { - const value = '0x'; + it('creates valid terms for uppercase hex value', () => { + const value = '0x1234567890ABCDEF'; 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 }); + expect(result).toStrictEqual(prefixWithIndex(startIndex, value)); }); - 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); + 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)); }); - }); - 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('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 for negative fractional startIndex', () => { - const value = '0x1234'; + it('throws an error for value without 0x prefix', () => { + const invalidValue = '1234567890abcdef' as Hex; expect(() => - createAllowedCalldataTerms({ startIndex: -0.1, value }), - ).toThrow('Invalid startIndex: must be zero or positive'); + createAllowedCalldataTerms({ + startIndex: 0, + value: invalidValue, + }), + ).toThrow('Invalid value: must be a hex string starting with 0x'); }); - it('throws for non-integer positive startIndex', () => { - const value = '0x1234'; + it('throws an error for empty string', () => { + const invalidValue = '' as Hex; expect(() => - createAllowedCalldataTerms({ startIndex: 1.5, value }), - ).toThrow('Invalid startIndex: must be a whole number'); + createAllowedCalldataTerms({ + startIndex: 0, + value: invalidValue, + }), + ).toThrow('Invalid value: must be a hex string starting with 0x'); }); - it('throws for NaN startIndex', () => { - const value = '0x1234'; + it('throws an error for malformed hex prefix', () => { + const invalidValue = '0X1234' as Hex; // uppercase X expect(() => - createAllowedCalldataTerms({ startIndex: Number.NaN, value }), - ).toThrow('Invalid startIndex: must be a whole number'); + createAllowedCalldataTerms({ + startIndex: 0, + value: invalidValue, + }), + ).toThrow('Invalid value: must be a hex string starting with 0x'); }); - it('throws for Infinity startIndex', () => { - const value = '0x1234'; + it('throws an error for undefined value', () => { expect(() => - createAllowedCalldataTerms({ startIndex: Infinity, value }), - ).toThrow('Invalid startIndex: must be a whole number'); + createAllowedCalldataTerms({ + startIndex: 0, + value: undefined as unknown as Hex, + }), + ).toThrow(); }); - it('accepts zero startIndex', () => { - const value = '0xdeadbeef'; - const result = createAllowedCalldataTerms({ startIndex: 0, value }); - expect(result).toStrictEqual(prefixWithIndex(0, value)); + it('throws an error for null value', () => { + expect(() => + createAllowedCalldataTerms({ + startIndex: 0, + value: null as unknown as Hex, + }), + ).toThrow(); }); - 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('throws an error for non-string non-Uint8Array value', () => { + expect(() => + createAllowedCalldataTerms({ + startIndex: 0, + value: 1234 as unknown as Hex, + }), + ).toThrow(); }); - }); - // 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; + it('handles single function selector', () => { + const functionSelector = '0xa9059cbb'; // transfer(address,uint256) selector + const startIndex = 7; const result = createAllowedCalldataTerms({ startIndex, - value: valueBytes, + value: functionSelector, }); - expect(result).toStrictEqual( - prefixWithIndex(startIndex, '0x1234567890abcdef'), - ); + expect(result).toStrictEqual(prefixWithIndex(startIndex, functionSelector)); }); - it('accepts empty Uint8Array as value parameter', () => { - const callDataBytes = new Uint8Array([]); + it('handles calldata with odd length', () => { + const oddLengthValue = '0x123'; const startIndex = 0; const result = createAllowedCalldataTerms({ startIndex, - value: callDataBytes, + value: oddLengthValue, }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, '0x')); + expect(result).toStrictEqual(prefixWithIndex(startIndex, oddLengthValue)); }); - 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, + // 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, + ]); }); - expect(result).toStrictEqual( - prefixWithIndex( + + 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, - '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000', - ), + 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('decodeAllowedCalldataTerms', () => { + it('decodes simple value and start index', () => { + const original = { startIndex: 0, value: '0x1234567890abcdef' as Hex }; + expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).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('decodes empty value with non-zero start index', () => { + const original = { startIndex: 5, value: '0x' as Hex }; + expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( + original, + ); + }); + + it('decodes function call shaped calldata', () => { + const value = + '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000' as Hex; + const original = { startIndex: 12, value }; + expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( + original, ); - expect(result).toBeInstanceOf(Uint8Array); - expect(Array.from(result)).toEqual([ - // 32-byte index (all zeros) - ...new Array(32).fill(0x00), - // calldata - 0x12, - 0x34, - 0x56, - 0x78, + }); + + it('preserves hex casing in the value slice', () => { + const original = { startIndex: 0, value: '0x1234567890ABCDEF' as Hex }; + expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( + original, + ); + }); + + it('decodes odd-length hex value', () => { + const original = { startIndex: 0, value: '0x123' as Hex }; + expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( + original, + ); + }); + + it('decodes very long value', () => { + const value = `0x${'a'.repeat(1000)}` as Hex; + const original = { startIndex: 31, value }; + expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( + original, + ); + }); + + it('decodes large start index', () => { + const original = { startIndex: Number.MAX_SAFE_INTEGER, value: '0x00' as Hex }; + expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( + original, + ); + }); + + it('decodes terms created from Uint8Array value', () => { + const valueBytes = new Uint8Array([ + 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, ]); + const startIndex = 3; + const encoded = createAllowedCalldataTerms({ startIndex, value: valueBytes }); + expect(decodeAllowedCalldataTerms(encoded)).toStrictEqual({ + startIndex, + value: '0x1234567890abcdef', + }); + }); + + 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); }); }); }); diff --git a/packages/delegation-core/test/caveats/allowedMethods.test.ts b/packages/delegation-core/test/caveats/allowedMethods.test.ts index e6707c50..408ebc49 100644 --- a/packages/delegation-core/test/caveats/allowedMethods.test.ts +++ b/packages/delegation-core/test/caveats/allowedMethods.test.ts @@ -1,60 +1,91 @@ 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('returns Uint8Array when bytes encoding is specified', () => { + const result = createAllowedMethodsTerms( + { selectors: [selectorA, selectorB] }, + { out: 'bytes' }, + ); - 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', - ); + 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}`; + + 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, + ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(8); + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createAllowedMethodsTerms( + { selectors: [selectorA, selectorB] }, + { out: 'bytes' }, + ); + expect(decodeAllowedMethodsTerms(bytes)).toStrictEqual({ + selectors: [selectorA, selectorB], + }); + }); }); }); diff --git a/packages/delegation-core/test/caveats/allowedTargets.test.ts b/packages/delegation-core/test/caveats/allowedTargets.test.ts index e50c6c77..459a136b 100644 --- a/packages/delegation-core/test/caveats/allowedTargets.test.ts +++ b/packages/delegation-core/test/caveats/allowedTargets.test.ts @@ -1,48 +1,75 @@ 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, + ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(40); + it('accepts Uint8Array terms from the encoder', () => { + const original = { targets: [addressA, addressB] }; + const bytes = createAllowedTargetsTerms(original, { out: 'bytes' }); + expect(decodeAllowedTargetsTerms(bytes)).toStrictEqual(original); + }); }); }); diff --git a/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts b/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts index e67903d2..733ee424 100644 --- a/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts +++ b/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts @@ -1,32 +1,55 @@ import { describe, it, expect } from 'vitest'; -import { createArgsEqualityCheckTerms } from '../../src/caveats/argsEqualityCheck'; +import { createArgsEqualityCheckTerms, decodeArgsEqualityCheckTerms } from '../../src/caveats/argsEqualityCheck'; -describe('createArgsEqualityCheckTerms', () => { - it('creates valid terms for args', () => { - const args = '0x1234abcd'; - const result = createArgsEqualityCheckTerms({ args }); +describe('ArgsEqualityCheck', () => { + describe('createArgsEqualityCheckTerms', () => { + it('creates valid terms for args', () => { + const args = '0x1234abcd'; + const result = createArgsEqualityCheckTerms({ args }); - expect(result).toStrictEqual(args); - }); + expect(result).toStrictEqual(args); + }); - it('creates valid terms for empty args', () => { - const result = createArgsEqualityCheckTerms({ args: '0x' }); + it('creates valid terms for empty args', () => { + const result = createArgsEqualityCheckTerms({ args: '0x' }); - expect(result).toStrictEqual('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('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' }); + 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); + }); + }); - 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..8ef4515f 100644 --- a/packages/delegation-core/test/caveats/blockNumber.test.ts +++ b/packages/delegation-core/test/caveats/blockNumber.test.ts @@ -1,54 +1,83 @@ 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('returns Uint8Array when bytes encoding is specified', () => { + const result = createBlockNumberTerms( + { afterThreshold: 1n, beforeThreshold: 2n }, + { out: 'bytes' }, + ); - 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', - ); + 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); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + 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, + }); + }); }); }); diff --git a/packages/delegation-core/test/caveats/deployed.test.ts b/packages/delegation-core/test/caveats/deployed.test.ts index fd59fde8..71324729 100644 --- a/packages/delegation-core/test/caveats/deployed.test.ts +++ b/packages/delegation-core/test/caveats/deployed.test.ts @@ -1,59 +1,85 @@ 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); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(20 + 32 + 2); + 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); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts index f1868b86..1f23fc0f 100644 --- a/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts @@ -1,67 +1,109 @@ 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' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(105); + }; + const bytes = createERC1155BalanceChangeTerms(original, { out: 'bytes' }); + const decoded = decodeERC1155BalanceChangeTerms(bytes); + expect(decoded.tokenId).toBe(1n); + expect(decoded.balance).toBe(1n); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts index 88115deb..af38757d 100644 --- a/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts @@ -1,62 +1,117 @@ 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' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(73); + }; + const bytes = createERC20BalanceChangeTerms(original, { out: 'bytes' }); + const decoded = decodeERC20BalanceChangeTerms(bytes); + expect(decoded.balance).toBe(1n); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc20Streaming.test.ts b/packages/delegation-core/test/caveats/erc20Streaming.test.ts index 3c674638..4fd26709 100644 --- a/packages/delegation-core/test/caveats/erc20Streaming.test.ts +++ b/packages/delegation-core/test/caveats/erc20Streaming.test.ts @@ -1,842 +1,552 @@ 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', + ); + }); + + 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 - expect(() => - createERC20StreamingTerms({ + 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,16 +554,417 @@ 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); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts index 10d4bccf..1ee73678 100644 --- a/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts +++ b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts @@ -1,47 +1,67 @@ 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}`; - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(52); + 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); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts index 52b4b305..bab03dbf 100644 --- a/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts @@ -1,62 +1,116 @@ 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' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(73); + }; + const bytes = createERC721BalanceChangeTerms(original, { out: 'bytes' }); + expect(decodeERC721BalanceChangeTerms(bytes).amount).toBe(2n); + }); }); }); diff --git a/packages/delegation-core/test/caveats/erc721Transfer.test.ts b/packages/delegation-core/test/caveats/erc721Transfer.test.ts index 30b61766..9d1c4e76 100644 --- a/packages/delegation-core/test/caveats/erc721Transfer.test.ts +++ b/packages/delegation-core/test/caveats/erc721Transfer.test.ts @@ -1,47 +1,74 @@ 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, + ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(52); + 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); + }); }); }); diff --git a/packages/delegation-core/test/caveats/exactCalldata.test.ts b/packages/delegation-core/test/caveats/exactCalldata.test.ts index 0a3c8677..f693653b 100644 --- a/packages/delegation-core/test/caveats/exactCalldata.test.ts +++ b/packages/delegation-core/test/caveats/exactCalldata.test.ts @@ -1,252 +1,320 @@ 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 = `0x${'a'.repeat(1000)}` as Hex; + 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..71d40b4c 100644 --- a/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts +++ b/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts @@ -2,46 +2,71 @@ 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..75ee963a 100644 --- a/packages/delegation-core/test/caveats/exactExecution.test.ts +++ b/packages/delegation-core/test/caveats/exactExecution.test.ts @@ -1,75 +1,119 @@ 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' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(20 + 32 + 2); + }; + const bytes = createExactExecutionTerms(original, { out: 'bytes' }); + expect(decodeExactExecutionTerms(bytes)).toStrictEqual(original); + }); }); }); diff --git a/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts b/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts index a68e4b45..ada79589 100644 --- a/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts +++ b/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts @@ -2,50 +2,75 @@ 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..fb919503 100644 --- a/packages/delegation-core/test/caveats/id.test.ts +++ b/packages/delegation-core/test/caveats/id.test.ts @@ -1,46 +1,80 @@ 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, + }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createIdTerms({ id: 42n }, { out: 'bytes' }); + expect(decodeIdTerms(bytes)).toStrictEqual({ id: 42n }); + }); }); }); diff --git a/packages/delegation-core/test/caveats/limitedCalls.test.ts b/packages/delegation-core/test/caveats/limitedCalls.test.ts index 4cad5e60..6dbe153b 100644 --- a/packages/delegation-core/test/caveats/limitedCalls.test.ts +++ b/packages/delegation-core/test/caveats/limitedCalls.test.ts @@ -1,32 +1,55 @@ 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, + ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createLimitedCallsTerms({ limit: 3 }, { out: 'bytes' }); + expect(decodeLimitedCallsTerms(bytes)).toStrictEqual({ limit: 3 }); + }); }); }); diff --git a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts index bb3d9131..d6637530 100644 --- a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts +++ b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts @@ -1,99 +1,161 @@ 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 +164,9 @@ describe('createMultiTokenPeriodTerms', () => { startDate: 1, }, ], - }, - { out: 'bytes' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(116); + }; + const bytes = createMultiTokenPeriodTerms(original, { out: 'bytes' }); + expect(decodeMultiTokenPeriodTerms(bytes)).toStrictEqual(original); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts index 877eb06b..7d85c82f 100644 --- a/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts @@ -1,66 +1,111 @@ 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 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 changeType', () => { - expect(() => - createNativeBalanceChangeTerms({ + describe('decodeNativeBalanceChangeTerms', () => { + const recipient = '0x00000000000000000000000000000000000000cc' as `0x${string}`; + + it('decodes increase balance change', () => { + const original = { recipient, balance: 1n, - changeType: 2 as any, - }), - ).toThrow('Invalid changeType: must be either Increase or Decrease'); - }); + changeType: BalanceChangeType.Increase, + }; + 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('returns Uint8Array when bytes encoding is specified', () => { - const result = createNativeBalanceChangeTerms( - { + it('decodes decrease balance change', () => { + const original = { recipient, balance: 2n, changeType: BalanceChangeType.Decrease, - }, - { out: 'bytes' }, - ); + }; + const decoded = decodeNativeBalanceChangeTerms( + createNativeBalanceChangeTerms(original), + ); + expect(decoded.changeType).toBe(original.changeType); + expect(decoded.balance).toBe(original.balance); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(53); + it('accepts Uint8Array terms from the encoder', () => { + const original = { + recipient, + balance: 2n, + changeType: BalanceChangeType.Decrease, + }; + const bytes = createNativeBalanceChangeTerms(original, { out: 'bytes' }); + const decoded = decodeNativeBalanceChangeTerms(bytes); + expect(decoded.balance).toBe(2n); + expect(decoded.changeType).toBe(BalanceChangeType.Decrease); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts index c52d6e7d..86d48fa1 100644 --- a/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts @@ -1,47 +1,70 @@ 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}`; - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(52); + 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); + }); + + 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); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts b/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts index d65bc1f7..96d4cfd6 100644 --- a/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts @@ -1,313 +1,367 @@ 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, - }); +import { createNativeTokenPeriodTransferTerms, decodeNativeTokenPeriodTransferTerms } from '../../src/caveats/nativeTokenPeriodTransfer'; - 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({ +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); + }); + }); + }); - 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); }); - 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 small values', () => { + const original = { + periodAmount: 1n, + periodDuration: 1, + startDate: 1, + }; + expect( + decodeNativeTokenPeriodTransferTerms( + createNativeTokenPeriodTransferTerms(original), + ), + ).toStrictEqual(original); + }); + + 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); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(96); + 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); }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts b/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts index 49ebc22c..e5803905 100644 --- a/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts @@ -1,570 +1,659 @@ 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 +import { createNativeTokenStreamingTerms, decodeNativeTokenStreamingTerms } from '../../src/caveats/nativeTokenStreaming'; - 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; +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'); - }); + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000001bc16d674ec80000000000000000000000000000000000000000000000000000016345785d8a00000000000000000000000000000000000000000000000000000000000061cf9980', + ); + }); - it('throws an error for negative amount per second', () => { - const initialAmount = 0n; - const maxAmount = 1000000000000000000n; - const amountPerSecond = -1n; - const startTime = 1640995200; + 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' }, - ); - 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), + const result = createNativeTokenStreamingTerms({ + initialAmount, + maxAmount, + amountPerSecond, + startTime, + }); + + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000de0b6b3a763ffff0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000061cf9980', ); - expect(Array.from(result.slice(64, 96))).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', ); }); + + // 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); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts index ff8b9039..63d4bff1 100644 --- a/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts @@ -1,37 +1,65 @@ 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 }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createNativeTokenTransferAmountTerms( + { maxAmount: 1n }, + { out: 'bytes' }, + ); + expect(decodeNativeTokenTransferAmountTerms(bytes)).toStrictEqual({ maxAmount: 1n }); + }); }); }); diff --git a/packages/delegation-core/test/caveats/nonce.test.ts b/packages/delegation-core/test/caveats/nonce.test.ts index 18815336..f64e091b 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,471 @@ 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( - '0x0000000000000000000000000000000000000000000000000000000000000123', + 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('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 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', ); + }); - // 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('decodes odd-length hex after encoder padding', () => { + const encoded = createNonceTerms({ nonce: '0x123' }); + expect(decodeNonceTerms(encoded).nonce).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000000000123', + ); + }); + + 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', ); }); }); diff --git a/packages/delegation-core/test/caveats/ownershipTransfer.test.ts b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts index 61305b75..6e54d902 100644 --- a/packages/delegation-core/test/caveats/ownershipTransfer.test.ts +++ b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts @@ -1,29 +1,47 @@ 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('throws for invalid contract address', () => { - expect(() => - createOwnershipTransferTerms({ contractAddress: '0x1234' }), - ).toThrow('Invalid contractAddress: must be a valid address'); + it('returns Uint8Array when bytes encoding is specified', () => { + const result = createOwnershipTransferTerms( + { contractAddress }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(20); + }); }); - 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 }); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(20); + it('accepts Uint8Array terms from the encoder', () => { + const bytes = createOwnershipTransferTerms({ contractAddress }, { out: 'bytes' }); + expect(decodeOwnershipTransferTerms(bytes)).toStrictEqual({ contractAddress }); + }); }); }); diff --git a/packages/delegation-core/test/caveats/redeemer.test.ts b/packages/delegation-core/test/caveats/redeemer.test.ts index 2d598c31..0ed72474 100644 --- a/packages/delegation-core/test/caveats/redeemer.test.ts +++ b/packages/delegation-core/test/caveats/redeemer.test.ts @@ -1,44 +1,67 @@ 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); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(40); + it('accepts Uint8Array terms from the encoder', () => { + const original = { redeemers: [redeemerA, redeemerB] }; + const bytes = createRedeemerTerms(original, { out: 'bytes' }); + expect(decodeRedeemerTerms(bytes)).toStrictEqual(original); + }); }); }); diff --git a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts index 5543432b..c6fc0295 100644 --- a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts +++ b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts @@ -1,79 +1,119 @@ 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' }, - ); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(94); + calldata: '0x1234' as `0x${string}`, + }; + const bytes = createSpecificActionERC20TransferBatchTerms(original, { + out: 'bytes', + }); + expect(decodeSpecificActionERC20TransferBatchTerms(bytes)).toStrictEqual(original); + }); }); }); diff --git a/packages/delegation-core/test/caveats/timestamp.test.ts b/packages/delegation-core/test/caveats/timestamp.test.ts index 6c286dd5..80a4f9ca 100644 --- a/packages/delegation-core/test/caveats/timestamp.test.ts +++ b/packages/delegation-core/test/caveats/timestamp.test.ts @@ -1,299 +1,361 @@ 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'; - expect(result).toStrictEqual( - '0x00000000000000000000000061cf998000000000000000000000000063b0cd00', - ); - }); +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 timestampAfterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC + const timestampBeforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const result = createTimestampTerms({ + timestampAfterThreshold, + timestampBeforeThreshold, + }); - it('creates valid terms for zero thresholds', () => { - const timestampAfterThreshold = 0; - const timestampBeforeThreshold = 0; - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + expect(result).toStrictEqual( + '0x00000000000000000000000061cf998000000000000000000000000063b0cd00', + ); }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000000000000', - ); - }); + it('creates valid terms for zero thresholds', () => { + const timestampAfterThreshold = 0; + const timestampBeforeThreshold = 0; + const result = createTimestampTerms({ + timestampAfterThreshold, + timestampBeforeThreshold, + }); - 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( + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); }); - expect(result).toStrictEqual( - '0x00000000000000000000000061cf998000000000000000000000000000000000', - ); - }); + 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, + }); - 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( + '0x00000000000000000000000061cf998000000000000000000000000000000000', + ); }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000000000000000000000000000063b0cd00', - ); - }); + 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, + }); - it('creates valid terms for small timestamp values', () => { - const timestampAfterThreshold = 1; - const timestampBeforeThreshold = 2; - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + expect(result).toStrictEqual( + '0x0000000000000000000000000000000000000000000000000000000063b0cd00', + ); }); - expect(result).toStrictEqual( - '0x0000000000000000000000000000000100000000000000000000000000000002', - ); - }); + it('creates valid terms for small timestamp values', () => { + const timestampAfterThreshold = 1; + const timestampBeforeThreshold = 2; + const result = createTimestampTerms({ + timestampAfterThreshold, + timestampBeforeThreshold, + }); - 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( + '0x0000000000000000000000000000000100000000000000000000000000000002', + ); }); - expect(result).toStrictEqual( - '0x00000000000000000000003afff4417f00000000000000000000000000000000', - ); - }); - - it('throws an error for negative after threshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: -1, + it('creates valid terms for maximum allowed timestamps', () => { + const maxTimestamp = 253402300799; // January 1, 10000 CE + const result = createTimestampTerms({ + timestampAfterThreshold: maxTimestamp, timestampBeforeThreshold: 0, - }), - ).toThrow('Invalid timestampAfterThreshold: 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'); - }); + expect(result).toStrictEqual( + '0x00000000000000000000003afff4417f00000000000000000000000000000000', + ); + }); - 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 negative after threshold', () => { + expect(() => + createTimestampTerms({ + timestampAfterThreshold: -1, + timestampBeforeThreshold: 0, + }), + ).toThrow('Invalid timestampAfterThreshold: must be zero or positive'); + }); - 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 negative before threshold', () => { + expect(() => + createTimestampTerms({ + timestampAfterThreshold: 0, + timestampBeforeThreshold: -1, + }), + ).toThrow('Invalid timestampBeforeThreshold: must be zero or positive'); + }); - 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 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 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 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 undefined timestampAfterThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: undefined as any, - timestampBeforeThreshold: 0, - }), - ).toThrow(); - }); + 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 for null timestampAfterThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: null as any, - timestampBeforeThreshold: 0, - }), - ).toThrow(); - }); + 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 for undefined timestampBeforeThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: undefined as any, - }), - ).toThrow(); - }); + it('throws an error for undefined timestampAfterThreshold', () => { + expect(() => + createTimestampTerms({ + timestampAfterThreshold: undefined as any, + timestampBeforeThreshold: 0, + }), + ).toThrow(); + }); - it('throws an error for null timestampBeforeThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: null as any, - }), - ).toThrow(); - }); + it('throws an error for null timestampAfterThreshold', () => { + expect(() => + createTimestampTerms({ + timestampAfterThreshold: null as any, + timestampBeforeThreshold: 0, + }), + ).toThrow(); + }); - it('throws an error for Infinity timestampAfterThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: Infinity, - timestampBeforeThreshold: 0, - }), - ).toThrow(); - }); + it('throws an error for undefined timestampBeforeThreshold', () => { + expect(() => + createTimestampTerms({ + timestampAfterThreshold: 0, + timestampBeforeThreshold: undefined as any, + }), + ).toThrow(); + }); - it('throws an error for Infinity timestampBeforeThreshold', () => { - expect(() => - createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: Infinity, - }), - ).toThrow(); - }); + it('throws an error for null timestampBeforeThreshold', () => { + expect(() => + createTimestampTerms({ + timestampAfterThreshold: 0, + timestampBeforeThreshold: null as any, + }), + ).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('throws an error for Infinity timestampAfterThreshold', () => { + expect(() => + createTimestampTerms({ + timestampAfterThreshold: Infinity, + timestampBeforeThreshold: 0, + }), + ).toThrow(); + }); - // Should not throw - const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + it('throws an error for Infinity timestampBeforeThreshold', () => { + expect(() => + createTimestampTerms({ + timestampAfterThreshold: 0, + timestampBeforeThreshold: Infinity, + }), + ).toThrow(); }); - 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' }, + 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; + + // Should not throw + const result = createTimestampTerms({ + timestampAfterThreshold, + timestampBeforeThreshold, + }); + expect(result).toStrictEqual( + '0x00000000000000000000000063b0cd0000000000000000000000000000000000', ); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // 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' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + }); + + it('returns Uint8Array for zero thresholds with bytes encoding', () => { + const timestampAfterThreshold = 0; + const timestampBeforeThreshold = 0; + const result = createTimestampTerms( + { + timestampAfterThreshold, + timestampBeforeThreshold, + }, + { 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 timestampAfterThreshold = 1640995200; + const timestampBeforeThreshold = 1672531200; + const result = createTimestampTerms( + { + timestampAfterThreshold, + timestampBeforeThreshold, + }, + { 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( + { + timestampAfterThreshold: maxTimestamp, + timestampBeforeThreshold: 0, + }, + { out: 'bytes' }, + ); + + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toHaveLength(32); + }); }); + }); - it('returns Uint8Array for zero thresholds with bytes encoding', () => { - const timestampAfterThreshold = 0; - const timestampBeforeThreshold = 0; - const result = createTimestampTerms( - { - timestampAfterThreshold, - timestampBeforeThreshold, - }, - { out: 'bytes' }, - ); + describe('decodeTimestampTerms', () => { + it('decodes a valid range', () => { + const original = { + timestampAfterThreshold: 1640995200, + timestampBeforeThreshold: 1672531200, + }; + 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 both thresholds zero', () => { + const original = { + timestampAfterThreshold: 0, + timestampBeforeThreshold: 0, + }; + 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 only after threshold set', () => { + const original = { + timestampAfterThreshold: 1640995200, + timestampBeforeThreshold: 0, + }; + expect(decodeTimestampTerms(createTimestampTerms(original))).toStrictEqual(original); + }); + + it('decodes only before threshold set', () => { + const original = { + timestampAfterThreshold: 0, + timestampBeforeThreshold: 1672531200, + }; + 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 small positive timestamps', () => { + const original = { + timestampAfterThreshold: 1, + timestampBeforeThreshold: 2, + }; + 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('decodes maximum allowed before threshold', () => { + const maxTimestamp = 253402300799; + const original = { + timestampAfterThreshold: 0, + timestampBeforeThreshold: maxTimestamp, + }; + expect(decodeTimestampTerms(createTimestampTerms(original))).toStrictEqual(original); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(32); + it('accepts Uint8Array terms from the encoder', () => { + const original = { + timestampAfterThreshold: 1640995200, + timestampBeforeThreshold: 1672531200, + }; + const bytes = createTimestampTerms(original, { out: 'bytes' }); + expect(decodeTimestampTerms(bytes)).toStrictEqual(original); }); }); }); diff --git a/packages/delegation-core/test/caveats/valueLte.test.ts b/packages/delegation-core/test/caveats/valueLte.test.ts index 8d41d01b..7254b650 100644 --- a/packages/delegation-core/test/caveats/valueLte.test.ts +++ b/packages/delegation-core/test/caveats/valueLte.test.ts @@ -1,134 +1,176 @@ import { describe, it, expect } from 'vitest'; -import { createValueLteTerms } from '../../src/caveats/valueLte'; +import { createValueLteTerms, decodeValueLteTerms } from '../../src/caveats/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('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 }); - 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', - ); - }); - - 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 large negative values', () => { + const maxValue = -1000000000000000000n; - it('throws an error for undefined maxValue', () => { - expect(() => createValueLteTerms({ maxValue: undefined as any })).toThrow(); - }); + expect(() => createValueLteTerms({ maxValue })).toThrow( + 'Invalid maxValue: must be greater than or equal to zero', + ); + }); - it('throws an error for null maxValue', () => { - expect(() => createValueLteTerms({ maxValue: null as any })).toThrow(); - }); + it('throws an error for undefined maxValue', () => { + expect(() => createValueLteTerms({ maxValue: undefined 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' }); + it('throws an error for null maxValue', () => { + expect(() => createValueLteTerms({ maxValue: null as any })).toThrow(); + }); - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); + // 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); + }); }); + }); - it('returns Uint8Array for zero value with bytes encoding', () => { - const maxValue = 0n; - const result = createValueLteTerms({ maxValue }, { out: 'bytes' }); + describe('decodeValueLteTerms', () => { + it('decodes zero maxValue', () => { + expect(decodeValueLteTerms(createValueLteTerms({ maxValue: 0n }))).toStrictEqual({ + maxValue: 0n, + }); + }); - 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' }); - - expect(result).toBeInstanceOf(Uint8Array); - expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); - expect(Array.from(result)).toEqual( - new Array(EXPECTED_BYTE_LENGTH).fill(0xff), - ); + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + expect(decodeValueLteTerms(createValueLteTerms({ maxValue }))).toStrictEqual({ + maxValue, + }); }); - 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('accepts Uint8Array terms from the encoder', () => { + const bytes = createValueLteTerms({ maxValue: 1000n }, { out: 'bytes' }); + expect(decodeValueLteTerms(bytes)).toStrictEqual({ maxValue: 1000n }); }); }); }); From f225313f4ab121c8806f2f7ec464ae50c662b82d Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:36:47 +1300 Subject: [PATCH 06/15] Normalise comments describing caveat encoding --- .../delegation-core/src/caveats/allowedCalldata.ts | 14 ++++++++++---- .../delegation-core/src/caveats/allowedMethods.ts | 13 ++++++++++--- .../delegation-core/src/caveats/allowedTargets.ts | 13 ++++++++++--- .../src/caveats/argsEqualityCheck.ts | 12 ++++++++++-- .../delegation-core/src/caveats/blockNumber.ts | 12 ++++++++++-- packages/delegation-core/src/caveats/deployed.ts | 13 ++++++++++--- .../src/caveats/erc1155BalanceChange.ts | 13 ++++++++++--- .../src/caveats/erc20BalanceChange.ts | 13 ++++++++++--- .../delegation-core/src/caveats/erc20Streaming.ts | 11 +++++++++-- .../src/caveats/erc20TokenPeriodTransfer.ts | 13 ++++++++++--- .../src/caveats/erc20TransferAmount.ts | 13 ++++++++++--- .../src/caveats/erc721BalanceChange.ts | 13 ++++++++++--- .../delegation-core/src/caveats/erc721Transfer.ts | 13 ++++++++++--- .../delegation-core/src/caveats/exactCalldata.ts | 13 ++++++++++--- .../src/caveats/exactCalldataBatch.ts | 13 ++++++++++--- .../delegation-core/src/caveats/exactExecution.ts | 13 ++++++++++--- .../src/caveats/exactExecutionBatch.ts | 13 ++++++++++--- packages/delegation-core/src/caveats/id.ts | 12 ++++++++++-- .../delegation-core/src/caveats/limitedCalls.ts | 12 ++++++++++-- .../src/caveats/multiTokenPeriod.ts | 13 ++++++++++--- .../src/caveats/nativeBalanceChange.ts | 13 ++++++++++--- .../src/caveats/nativeTokenPayment.ts | 13 ++++++++++--- .../src/caveats/nativeTokenPeriodTransfer.ts | 13 ++++++++++--- .../src/caveats/nativeTokenStreaming.ts | 11 +++++++++-- .../src/caveats/nativeTokenTransferAmount.ts | 12 ++++++++++-- packages/delegation-core/src/caveats/nonce.ts | 14 ++++++++++---- .../src/caveats/ownershipTransfer.ts | 12 ++++++++++-- packages/delegation-core/src/caveats/redeemer.ts | 13 ++++++++++--- .../caveats/specificActionERC20TransferBatch.ts | 13 ++++++++++--- packages/delegation-core/src/caveats/timestamp.ts | 12 ++++++++++-- packages/delegation-core/src/caveats/valueLte.ts | 12 ++++++++++-- 31 files changed, 308 insertions(+), 85 deletions(-) diff --git a/packages/delegation-core/src/caveats/allowedCalldata.ts b/packages/delegation-core/src/caveats/allowedCalldata.ts index edc45ddb..07d5ffad 100644 --- a/packages/delegation-core/src/caveats/allowedCalldata.ts +++ b/packages/delegation-core/src/caveats/allowedCalldata.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { @@ -28,7 +36,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( @@ -45,7 +53,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( @@ -75,7 +83,6 @@ 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); } @@ -90,7 +97,6 @@ export function decodeAllowedCalldataTerms( ): AllowedCalldataTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: startIndex (32 bytes) + value (remaining) const startIndex = extractNumber(hexTerms, 0, 32); const value = extractRemainingHex(hexTerms, 32); diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index ccd9ad53..96b4cad9 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -1,3 +1,11 @@ +/** + * ## 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, extractHex } from '../internalUtils'; @@ -27,7 +35,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( @@ -43,7 +51,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( @@ -89,7 +97,6 @@ export function decodeAllowedMethodsTerms( ): AllowedMethodsTerms { const hexTerms = bytesLikeToHex(terms); - // Each selector is 4 bytes const selectorSize = 4; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const selectorCount = totalBytes / selectorSize; diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index 089a0d19..1a1a59fe 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -1,3 +1,11 @@ +/** + * ## 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, extractAddress, normalizeAddress } from '../internalUtils'; @@ -23,7 +31,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( @@ -39,7 +47,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( @@ -73,7 +81,6 @@ export function decodeAllowedTargetsTerms( ): AllowedTargetsTerms { const hexTerms = bytesLikeToHex(terms); - // Each address is 20 bytes const addressSize = 20; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const addressCount = totalBytes / addressSize; diff --git a/packages/delegation-core/src/caveats/argsEqualityCheck.ts b/packages/delegation-core/src/caveats/argsEqualityCheck.ts index 214ed249..607bdaa2 100644 --- a/packages/delegation-core/src/caveats/argsEqualityCheck.ts +++ b/packages/delegation-core/src/caveats/argsEqualityCheck.ts @@ -1,3 +1,11 @@ +/** + * ## 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'; @@ -23,7 +31,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( @@ -39,7 +47,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( diff --git a/packages/delegation-core/src/caveats/blockNumber.ts b/packages/delegation-core/src/caveats/blockNumber.ts index 8d307503..00035a80 100644 --- a/packages/delegation-core/src/caveats/blockNumber.ts +++ b/packages/delegation-core/src/caveats/blockNumber.ts @@ -1,3 +1,11 @@ +/** + * ## BlockNumberEnforcer + * + * Restricts redemption to a block number range. + * + * Terms are encoded as a 16-byte after threshold followed by a 16-byte before threshold (each big-endian, zero-padded). + */ + import type { BytesLike } from '@metamask/utils'; import { extractBigInt, toHexString } from '../internalUtils'; @@ -25,7 +33,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( @@ -41,7 +49,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( diff --git a/packages/delegation-core/src/caveats/deployed.ts b/packages/delegation-core/src/caveats/deployed.ts index 799e0308..11f7544b 100644 --- a/packages/delegation-core/src/caveats/deployed.ts +++ b/packages/delegation-core/src/caveats/deployed.ts @@ -1,3 +1,11 @@ +/** + * ## 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'; @@ -35,7 +43,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( @@ -51,7 +59,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( @@ -92,7 +100,6 @@ export function createDeployedTerms( export function decodeDeployedTerms(terms: BytesLike): DeployedTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: contractAddress (20 bytes) + salt (32 bytes) + bytecode (remaining) const contractAddress = extractAddress(hexTerms, 0); const salt = extractHex(hexTerms, 20, 32); const bytecode = extractRemainingHex(hexTerms, 52); diff --git a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts index 8c78009f..27827513 100644 --- a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts @@ -1,3 +1,11 @@ +/** + * ## ERC1155BalanceChangeEnforcer + * + * Constrains ERC-1155 balance change for a token id and recipient. + * + * Terms are encoded as 1-byte change type, 20-byte token and recipient (normalized lowercase), then two 32-byte big-endian uint256 words: token id and balance. + */ + import type { BytesLike } from '@metamask/utils'; import { @@ -39,7 +47,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( @@ -55,7 +63,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( @@ -121,7 +129,6 @@ export function decodeERC1155BalanceChangeTerms( ): ERC1155BalanceChangeTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + tokenId (32 bytes) + balance (32 bytes) const changeType = extractNumber(hexTerms, 0, 1); const tokenAddress = extractAddress(hexTerms, 1); const recipient = extractAddress(hexTerms, 21); diff --git a/packages/delegation-core/src/caveats/erc20BalanceChange.ts b/packages/delegation-core/src/caveats/erc20BalanceChange.ts index 3594c7f1..850f0cc7 100644 --- a/packages/delegation-core/src/caveats/erc20BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc20BalanceChange.ts @@ -1,3 +1,11 @@ +/** + * ## ERC20BalanceChangeEnforcer + * + * Constrains ERC-20 balance change for a recipient relative to a reference balance. + * + * Terms are encoded as 1-byte change type, 20-byte token and recipient (each normalized lowercase), then 32-byte big-endian balance. + */ + import type { BytesLike } from '@metamask/utils'; import { @@ -37,7 +45,7 @@ export type ERC20BalanceChangeTerms = { * * @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( @@ -53,7 +61,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( @@ -112,7 +120,6 @@ export function decodeERC20BalanceChangeTerms( ): ERC20BalanceChangeTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + balance (32 bytes) const changeType = extractNumber(hexTerms, 0, 1); const tokenAddress = extractAddress(hexTerms, 1); const recipient = extractAddress(hexTerms, 21); diff --git a/packages/delegation-core/src/caveats/erc20Streaming.ts b/packages/delegation-core/src/caveats/erc20Streaming.ts index c7ba0832..520296bc 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { @@ -63,7 +71,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( @@ -138,7 +146,6 @@ export function decodeERC20StreamingTerms( ): ERC20StreamingTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: tokenAddress (20 bytes) + initialAmount (32 bytes) + maxAmount (32 bytes) + amountPerSecond (32 bytes) + startTime (32 bytes) const tokenAddress = extractAddress(hexTerms, 0); const initialAmount = extractBigInt(hexTerms, 20, 32); const maxAmount = extractBigInt(hexTerms, 52, 32); diff --git a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts index f4b8d1d2..04e9a7cc 100644 --- a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { @@ -36,7 +44,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( @@ -53,7 +61,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( @@ -112,7 +120,6 @@ export function decodeERC20TokenPeriodTransferTerms( ): ERC20TokenPeriodTransferTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: tokenAddress (20 bytes) + periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 bytes) const tokenAddress = extractAddress(hexTerms, 0); const periodAmount = extractBigInt(hexTerms, 20, 32); const periodDuration = extractNumber(hexTerms, 52, 32); diff --git a/packages/delegation-core/src/caveats/erc20TransferAmount.ts b/packages/delegation-core/src/caveats/erc20TransferAmount.ts index 0e4d83ec..ef1149ce 100644 --- a/packages/delegation-core/src/caveats/erc20TransferAmount.ts +++ b/packages/delegation-core/src/caveats/erc20TransferAmount.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { @@ -31,7 +39,7 @@ export type ERC20TransferAmountTerms = { * * @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( @@ -47,7 +55,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( @@ -82,7 +90,6 @@ export function decodeERC20TransferAmountTerms( ): ERC20TransferAmountTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: tokenAddress (20 bytes) + maxAmount (32 bytes) const tokenAddress = extractAddress(hexTerms, 0); const maxAmount = extractBigInt(hexTerms, 20, 32); diff --git a/packages/delegation-core/src/caveats/erc721BalanceChange.ts b/packages/delegation-core/src/caveats/erc721BalanceChange.ts index a9486e70..f2761877 100644 --- a/packages/delegation-core/src/caveats/erc721BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc721BalanceChange.ts @@ -1,3 +1,11 @@ +/** + * ## ERC721BalanceChangeEnforcer + * + * Constrains ERC-721 balance (id count) change for a recipient. + * + * Terms are encoded as 1-byte change type, 20-byte token and recipient (each normalized lowercase), then 32-byte big-endian amount. + */ + import type { BytesLike } from '@metamask/utils'; import { @@ -37,7 +45,7 @@ export type ERC721BalanceChangeTerms = { * * @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( @@ -53,7 +61,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( @@ -112,7 +120,6 @@ export function decodeERC721BalanceChangeTerms( ): ERC721BalanceChangeTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: changeType (1 byte) + tokenAddress (20 bytes) + recipient (20 bytes) + amount (32 bytes) const changeType = extractNumber(hexTerms, 0, 1); const tokenAddress = extractAddress(hexTerms, 1); const recipient = extractAddress(hexTerms, 21); diff --git a/packages/delegation-core/src/caveats/erc721Transfer.ts b/packages/delegation-core/src/caveats/erc721Transfer.ts index 0bd62f3f..918099e3 100644 --- a/packages/delegation-core/src/caveats/erc721Transfer.ts +++ b/packages/delegation-core/src/caveats/erc721Transfer.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { @@ -31,7 +39,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( @@ -47,7 +55,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( @@ -82,7 +90,6 @@ export function decodeERC721TransferTerms( ): ERC721TransferTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: tokenAddress (20 bytes) + tokenId (32 bytes) const tokenAddress = extractAddress(hexTerms, 0); const tokenId = extractBigInt(hexTerms, 20, 32); diff --git a/packages/delegation-core/src/caveats/exactCalldata.ts b/packages/delegation-core/src/caveats/exactCalldata.ts index 0eda7d79..24a0484c 100644 --- a/packages/delegation-core/src/caveats/exactCalldata.ts +++ b/packages/delegation-core/src/caveats/exactCalldata.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { @@ -23,7 +31,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( @@ -40,7 +48,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( @@ -57,7 +65,6 @@ 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); } diff --git a/packages/delegation-core/src/caveats/exactCalldataBatch.ts b/packages/delegation-core/src/caveats/exactCalldataBatch.ts index 65251149..012cf556 100644 --- a/packages/delegation-core/src/caveats/exactCalldataBatch.ts +++ b/packages/delegation-core/src/caveats/exactCalldataBatch.ts @@ -1,3 +1,11 @@ +/** + * ## ExactCalldataBatchEnforcer + * + * Requires a batch of executions to match exactly on calldata and metadata. + * + * Terms are encoded as ABI-encoded (address,uint256,bytes)[]. + */ + import { decodeSingle, encodeSingle } from '@metamask/abi-utils'; import { bytesToHex, type BytesLike } from '@metamask/utils'; @@ -30,7 +38,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( @@ -46,7 +54,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( @@ -99,7 +107,6 @@ export function decodeExactCalldataBatchTerms( ): ExactCalldataBatchTerms { const hexTerms = bytesLikeToHex(terms); - // Decode using ABI: (address,uint256,bytes)[] const decoded = decodeSingle(EXECUTION_ARRAY_ABI, hexTerms); const executions = (decoded as [string, bigint, Uint8Array][]).map( diff --git a/packages/delegation-core/src/caveats/exactExecution.ts b/packages/delegation-core/src/caveats/exactExecution.ts index 439dac5c..98404247 100644 --- a/packages/delegation-core/src/caveats/exactExecution.ts +++ b/packages/delegation-core/src/caveats/exactExecution.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { @@ -34,7 +42,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( @@ -50,7 +58,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( @@ -97,7 +105,6 @@ export function decodeExactExecutionTerms( ): ExactExecutionTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: target (20 bytes) + value (32 bytes) + callData (remaining) const target = extractAddress(hexTerms, 0); const value = extractBigInt(hexTerms, 20, 32); const callData = extractRemainingHex(hexTerms, 52); diff --git a/packages/delegation-core/src/caveats/exactExecutionBatch.ts b/packages/delegation-core/src/caveats/exactExecutionBatch.ts index fd254938..8ec629d3 100644 --- a/packages/delegation-core/src/caveats/exactExecutionBatch.ts +++ b/packages/delegation-core/src/caveats/exactExecutionBatch.ts @@ -1,3 +1,11 @@ +/** + * ## 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'; @@ -30,7 +38,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( @@ -46,7 +54,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( @@ -99,7 +107,6 @@ export function decodeExactExecutionBatchTerms( ): ExactExecutionBatchTerms { const hexTerms = bytesLikeToHex(terms); - // Decode using ABI: (address,uint256,bytes)[] const decoded = decodeSingle(EXECUTION_ARRAY_ABI, hexTerms); const executions = (decoded as [string, bigint, Uint8Array][]).map( diff --git a/packages/delegation-core/src/caveats/id.ts b/packages/delegation-core/src/caveats/id.ts index 91983292..7c4bf356 100644 --- a/packages/delegation-core/src/caveats/id.ts +++ b/packages/delegation-core/src/caveats/id.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { extractBigInt, toHexString } from '../internalUtils'; @@ -25,7 +33,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( @@ -41,7 +49,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( diff --git a/packages/delegation-core/src/caveats/limitedCalls.ts b/packages/delegation-core/src/caveats/limitedCalls.ts index 57b24645..1937ce32 100644 --- a/packages/delegation-core/src/caveats/limitedCalls.ts +++ b/packages/delegation-core/src/caveats/limitedCalls.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { extractNumber, toHexString } from '../internalUtils'; @@ -23,7 +31,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( @@ -39,7 +47,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( diff --git a/packages/delegation-core/src/caveats/multiTokenPeriod.ts b/packages/delegation-core/src/caveats/multiTokenPeriod.ts index 7c096495..047e1844 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -1,3 +1,11 @@ +/** + * ## MultiTokenPeriodEnforcer + * + * Sets independent periodic transfer limits for multiple ERC-20 tokens. + * + * Terms are encoded by repeating, per token: 20-byte token address then three 32-byte big-endian uint256 words (period amount, period duration, start date). + */ + import type { BytesLike } from '@metamask/utils'; import { @@ -39,7 +47,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( @@ -55,7 +63,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( @@ -113,7 +121,6 @@ export function decodeMultiTokenPeriodTerms( ): MultiTokenPeriodTerms { const hexTerms = bytesLikeToHex(terms); - // Each token config is: token (20 bytes) + periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 bytes) = 116 bytes const configSize = 116; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const configCount = totalBytes / configSize; diff --git a/packages/delegation-core/src/caveats/nativeBalanceChange.ts b/packages/delegation-core/src/caveats/nativeBalanceChange.ts index 12fc5e18..89f054ec 100644 --- a/packages/delegation-core/src/caveats/nativeBalanceChange.ts +++ b/packages/delegation-core/src/caveats/nativeBalanceChange.ts @@ -1,3 +1,11 @@ +/** + * ## NativeBalanceChangeEnforcer + * + * Constrains native balance change for a recipient relative to a reference balance. + * + * Terms are encoded as 1-byte change type, 20-byte recipient (normalized lowercase), then 32-byte big-endian balance in wei. + */ + import type { BytesLike } from '@metamask/utils'; import { @@ -35,7 +43,7 @@ export type NativeBalanceChangeTerms = { * * @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( @@ -51,7 +59,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( @@ -96,7 +104,6 @@ export function decodeNativeBalanceChangeTerms( ): NativeBalanceChangeTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: changeType (1 byte) + recipient (20 bytes) + balance (32 bytes) const changeType = extractNumber(hexTerms, 0, 1); const recipient = extractAddress(hexTerms, 1); const balance = extractBigInt(hexTerms, 21, 32); diff --git a/packages/delegation-core/src/caveats/nativeTokenPayment.ts b/packages/delegation-core/src/caveats/nativeTokenPayment.ts index a166dd79..f8b1fb9a 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPayment.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPayment.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { @@ -31,7 +39,7 @@ export type NativeTokenPaymentTerms = { * * @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( @@ -47,7 +55,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( @@ -82,7 +90,6 @@ export function decodeNativeTokenPaymentTerms( ): NativeTokenPaymentTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: recipient (20 bytes) + amount (32 bytes) const recipient = extractAddress(hexTerms, 0); const amount = extractBigInt(hexTerms, 20, 32); diff --git a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts index 3e4e501a..56264714 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { extractBigInt, extractNumber, toHexString } from '../internalUtils'; @@ -29,7 +37,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( @@ -46,7 +54,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( @@ -87,7 +95,6 @@ export function decodeNativeTokenPeriodTransferTerms( ): NativeTokenPeriodTransferTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: periodAmount (32 bytes) + periodDuration (32 bytes) + startDate (32 bytes) const periodAmount = extractBigInt(hexTerms, 0, 32); const periodDuration = extractNumber(hexTerms, 32, 32); const startDate = extractNumber(hexTerms, 64, 32); diff --git a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts index 25123def..53ead3c5 100644 --- a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts +++ b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { extractBigInt, extractNumber, toHexString } from '../internalUtils'; @@ -55,7 +63,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( @@ -111,7 +119,6 @@ export function decodeNativeTokenStreamingTerms( ): NativeTokenStreamingTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: initialAmount (32 bytes) + maxAmount (32 bytes) + amountPerSecond (32 bytes) + startTime (32 bytes) const initialAmount = extractBigInt(hexTerms, 0, 32); const maxAmount = extractBigInt(hexTerms, 32, 32); const amountPerSecond = extractBigInt(hexTerms, 64, 32); diff --git a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts index 516034a7..dfbd1c5d 100644 --- a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts +++ b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { extractBigInt, toHexString } from '../internalUtils'; @@ -23,7 +31,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( @@ -39,7 +47,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( diff --git a/packages/delegation-core/src/caveats/nonce.ts b/packages/delegation-core/src/caveats/nonce.ts index 5bb208b7..787b17aa 100644 --- a/packages/delegation-core/src/caveats/nonce.ts +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -1,3 +1,11 @@ +/** + * ## 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'; @@ -26,7 +34,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 +50,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( @@ -94,8 +102,6 @@ export function createNonceTerms( export function decodeNonceTerms(terms: BytesLike): NonceTerms { const hexTerms = bytesLikeToHex(terms); - // The nonce is stored as a 32-byte padded value - // We return it as-is (padded) to maintain consistency const nonce = hexTerms; return { nonce }; diff --git a/packages/delegation-core/src/caveats/ownershipTransfer.ts b/packages/delegation-core/src/caveats/ownershipTransfer.ts index a3e90fee..dcaa73b7 100644 --- a/packages/delegation-core/src/caveats/ownershipTransfer.ts +++ b/packages/delegation-core/src/caveats/ownershipTransfer.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { extractAddress, normalizeAddress } from '../internalUtils'; @@ -23,7 +31,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( @@ -39,7 +47,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( diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index cf5f42fd..2598c059 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -1,3 +1,11 @@ +/** + * ## 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, extractAddress, normalizeAddress } from '../internalUtils'; @@ -23,7 +31,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( @@ -39,7 +47,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( @@ -71,7 +79,6 @@ export function createRedeemerTerms( export function decodeRedeemerTerms(terms: BytesLike): RedeemerTerms { const hexTerms = bytesLikeToHex(terms); - // Each address is 20 bytes const addressSize = 20; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const addressCount = totalBytes / addressSize; diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts index 57ad668b..666856b0 100644 --- a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -1,3 +1,11 @@ +/** + * ## SpecificActionERC20TransferBatchEnforcer + * + * Ties an ERC-20 transfer batch to a specific preceding action (target and calldata). + * + * Terms are encoded as 20-byte token, 20-byte recipient, 32-byte amount, 20-byte target, then calldata bytes without a separate ABI length prefix. + */ + import { bytesToHex, type BytesLike } from '@metamask/utils'; import { @@ -39,7 +47,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( @@ -56,7 +64,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( @@ -118,7 +126,6 @@ export function decodeSpecificActionERC20TransferBatchTerms( ): SpecificActionERC20TransferBatchTerms { const hexTerms = bytesLikeToHex(terms); - // Structure: tokenAddress (20 bytes) + recipient (20 bytes) + amount (32 bytes) + target (20 bytes) + calldata (remaining) const tokenAddress = extractAddress(hexTerms, 0); const recipient = extractAddress(hexTerms, 20); const amount = extractBigInt(hexTerms, 40, 32); diff --git a/packages/delegation-core/src/caveats/timestamp.ts b/packages/delegation-core/src/caveats/timestamp.ts index 9356e8f5..5a32a112 100644 --- a/packages/delegation-core/src/caveats/timestamp.ts +++ b/packages/delegation-core/src/caveats/timestamp.ts @@ -1,3 +1,11 @@ +/** + * ## TimestampEnforcer + * + * Restricts redemption to a unix timestamp window (after / before thresholds). + * + * Terms are encoded as two 16-byte big-endian fields: timestamp after, then timestamp before (each zero-padded). + */ + import type { BytesLike } from '@metamask/utils'; import { extractNumber, toHexString } from '../internalUtils'; @@ -28,7 +36,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( @@ -44,7 +52,7 @@ 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( diff --git a/packages/delegation-core/src/caveats/valueLte.ts b/packages/delegation-core/src/caveats/valueLte.ts index 138728d0..741587ee 100644 --- a/packages/delegation-core/src/caveats/valueLte.ts +++ b/packages/delegation-core/src/caveats/valueLte.ts @@ -1,3 +1,11 @@ +/** + * ## 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 { extractBigInt, toHexString } from '../internalUtils'; @@ -23,7 +31,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( @@ -39,7 +47,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( From c03605c7a68979c03dcbc13c1751b9b3d1dfe568 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:14:38 +1300 Subject: [PATCH 07/15] Fix various linting errors --- packages/delegation-core/src/internalUtils.ts | 18 ++---- .../test/caveats/allowedCalldata.test.ts | 63 +++++++++++-------- .../test/caveats/allowedMethods.test.ts | 17 ++--- .../test/caveats/allowedTargets.test.ts | 27 +++++--- .../test/caveats/argsEqualityCheck.test.ts | 13 +++- .../test/caveats/blockNumber.test.ts | 5 +- .../test/caveats/deployed.test.ts | 5 +- .../test/caveats/erc1155BalanceChange.test.ts | 5 +- .../test/caveats/erc20BalanceChange.test.ts | 5 +- .../test/caveats/erc20Streaming.test.ts | 56 +++++++++-------- .../test/caveats/erc20TransferAmount.test.ts | 9 ++- .../test/caveats/erc721BalanceChange.test.ts | 5 +- .../test/caveats/erc721Transfer.test.ts | 17 ++--- .../test/caveats/exactCalldata.test.ts | 43 +++++++++---- .../test/caveats/exactCalldataBatch.test.ts | 5 +- .../test/caveats/exactExecution.test.ts | 20 +++--- .../test/caveats/exactExecutionBatch.test.ts | 9 ++- .../test/caveats/limitedCalls.test.ts | 17 ++--- .../test/caveats/multiTokenPeriod.test.ts | 8 ++- .../test/caveats/nativeBalanceChange.test.ts | 12 +++- .../test/caveats/nativeTokenPayment.test.ts | 16 +++-- .../caveats/nativeTokenPeriodTransfer.test.ts | 13 +++- .../test/caveats/nativeTokenStreaming.test.ts | 37 ++++++++--- .../caveats/nativeTokenTransferAmount.test.ts | 9 ++- .../test/caveats/nonce.test.ts | 20 +++--- .../test/caveats/ownershipTransfer.test.ts | 18 ++++-- .../test/caveats/redeemer.test.ts | 23 +++++-- .../specificActionERC20TransferBatch.test.ts | 9 ++- .../test/caveats/timestamp.test.ts | 37 +++++++---- .../test/caveats/valueLte.test.ts | 29 ++++++--- .../test/internalUtils.test.ts | 8 +-- 31 files changed, 384 insertions(+), 194 deletions(-) diff --git a/packages/delegation-core/src/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts index f0e325b2..ab57b21c 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -1,9 +1,9 @@ import { bytesToHex, - Hex, hexToBytes, isHexString, remove0x, + type Hex, type BytesLike, } from '@metamask/utils'; @@ -161,10 +161,7 @@ export const extractNumber = ( * @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 => { +export const extractAddress = (value: Hex, offset: number): Hex => { const start = 2 + offset * 2; const end = start + 40; @@ -179,11 +176,7 @@ export const extractAddress = ( * @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 => { +export const extractHex = (value: Hex, offset: number, size: number): Hex => { const start = 2 + offset * 2; const end = start + size * 2; @@ -197,10 +190,7 @@ export const extractHex = ( * @param offset - The byte offset to start extraction. * @returns The extracted hex string (0x-prefixed). */ -export const extractRemainingHex = ( - value: Hex, - offset: number, -): Hex => { +export const extractRemainingHex = (value: Hex, offset: number): Hex => { const start = 2 + offset * 2; return `0x${value.slice(start)}`; diff --git a/packages/delegation-core/test/caveats/allowedCalldata.test.ts b/packages/delegation-core/test/caveats/allowedCalldata.test.ts index c2728e57..ba68f124 100644 --- a/packages/delegation-core/test/caveats/allowedCalldata.test.ts +++ b/packages/delegation-core/test/caveats/allowedCalldata.test.ts @@ -1,7 +1,10 @@ import { hexToBytes } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { createAllowedCalldataTerms, decodeAllowedCalldataTerms } from '../../src/caveats/allowedCalldata'; +import { + createAllowedCalldataTerms, + decodeAllowedCalldataTerms, +} from '../../src/caveats/allowedCalldata'; import { toHexString } from '../../src/internalUtils'; import type { Hex } from '../../src/types'; @@ -132,7 +135,9 @@ describe('AllowedCalldata', () => { startIndex, value: functionSelector, }); - expect(result).toStrictEqual(prefixWithIndex(startIndex, functionSelector)); + expect(result).toStrictEqual( + prefixWithIndex(startIndex, functionSelector), + ); }); it('handles calldata with odd length', () => { @@ -383,54 +388,57 @@ describe('AllowedCalldata', () => { describe('decodeAllowedCalldataTerms', () => { it('decodes simple value and start index', () => { const original = { startIndex: 0, value: '0x1234567890abcdef' as Hex }; - expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); it('decodes empty value with non-zero start index', () => { const original = { startIndex: 5, value: '0x' as Hex }; - expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); it('decodes function call shaped calldata', () => { const value = '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000' as Hex; const original = { startIndex: 12, value }; - expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); it('preserves hex casing in the value slice', () => { const original = { startIndex: 0, value: '0x1234567890ABCDEF' as Hex }; - expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); it('decodes odd-length hex value', () => { const original = { startIndex: 0, value: '0x123' as Hex }; - expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); it('decodes very long value', () => { - const value = `0x${'a'.repeat(1000)}` as Hex; + const value: Hex = `0x${'a'.repeat(1000)}`; const original = { startIndex: 31, value }; - expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); it('decodes large start index', () => { - const original = { startIndex: Number.MAX_SAFE_INTEGER, value: '0x00' as Hex }; - expect(decodeAllowedCalldataTerms(createAllowedCalldataTerms(original))).toStrictEqual( - original, - ); + const original = { + startIndex: Number.MAX_SAFE_INTEGER, + value: '0x00' as Hex, + }; + expect( + decodeAllowedCalldataTerms(createAllowedCalldataTerms(original)), + ).toStrictEqual(original); }); it('decodes terms created from Uint8Array value', () => { @@ -438,7 +446,10 @@ describe('AllowedCalldata', () => { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, ]); const startIndex = 3; - const encoded = createAllowedCalldataTerms({ startIndex, value: valueBytes }); + const encoded = createAllowedCalldataTerms({ + startIndex, + value: valueBytes, + }); expect(decodeAllowedCalldataTerms(encoded)).toStrictEqual({ startIndex, value: '0x1234567890abcdef', diff --git a/packages/delegation-core/test/caveats/allowedMethods.test.ts b/packages/delegation-core/test/caveats/allowedMethods.test.ts index 408ebc49..e22844d5 100644 --- a/packages/delegation-core/test/caveats/allowedMethods.test.ts +++ b/packages/delegation-core/test/caveats/allowedMethods.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createAllowedMethodsTerms, decodeAllowedMethodsTerms } from '../../src/caveats/allowedMethods'; +import { + createAllowedMethodsTerms, + decodeAllowedMethodsTerms, +} from '../../src/caveats/allowedMethods'; describe('AllowedMethods', () => { describe('createAllowedMethodsTerms', () => { @@ -66,16 +69,16 @@ describe('AllowedMethods', () => { it('decodes multiple selectors', () => { const original = { selectors: [selectorA, selectorB] }; - expect(decodeAllowedMethodsTerms(createAllowedMethodsTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedMethodsTerms(createAllowedMethodsTerms(original)), + ).toStrictEqual(original); }); it('decodes a single selector', () => { const original = { selectors: [selectorA] }; - expect(decodeAllowedMethodsTerms(createAllowedMethodsTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedMethodsTerms(createAllowedMethodsTerms(original)), + ).toStrictEqual(original); }); it('accepts Uint8Array terms from the encoder', () => { diff --git a/packages/delegation-core/test/caveats/allowedTargets.test.ts b/packages/delegation-core/test/caveats/allowedTargets.test.ts index 459a136b..6ddf138b 100644 --- a/packages/delegation-core/test/caveats/allowedTargets.test.ts +++ b/packages/delegation-core/test/caveats/allowedTargets.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createAllowedTargetsTerms, decodeAllowedTargetsTerms } from '../../src/caveats/allowedTargets'; +import { + createAllowedTargetsTerms, + decodeAllowedTargetsTerms, +} from '../../src/caveats/allowedTargets'; describe('AllowedTargets', () => { describe('createAllowedTargetsTerms', () => { @@ -8,7 +11,9 @@ describe('AllowedTargets', () => { const addressB = '0x0000000000000000000000000000000000000002'; it('creates valid terms for multiple addresses', () => { - const result = createAllowedTargetsTerms({ targets: [addressA, addressB] }); + const result = createAllowedTargetsTerms({ + targets: [addressA, addressB], + }); expect(result).toStrictEqual( '0x00000000000000000000000000000000000000010000000000000000000000000000000000000002', @@ -49,21 +54,23 @@ describe('AllowedTargets', () => { }); describe('decodeAllowedTargetsTerms', () => { - const addressA = '0x0000000000000000000000000000000000000001' as `0x${string}`; - const addressB = '0x0000000000000000000000000000000000000002' as `0x${string}`; + 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, - ); + expect( + decodeAllowedTargetsTerms(createAllowedTargetsTerms(original)), + ).toStrictEqual(original); }); it('decodes a single target', () => { const original = { targets: [addressA] }; - expect(decodeAllowedTargetsTerms(createAllowedTargetsTerms(original))).toStrictEqual( - original, - ); + expect( + decodeAllowedTargetsTerms(createAllowedTargetsTerms(original)), + ).toStrictEqual(original); }); it('accepts Uint8Array terms from the encoder', () => { diff --git a/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts b/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts index 733ee424..80ecfd6c 100644 --- a/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts +++ b/packages/delegation-core/test/caveats/argsEqualityCheck.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createArgsEqualityCheckTerms, decodeArgsEqualityCheckTerms } from '../../src/caveats/argsEqualityCheck'; +import { + createArgsEqualityCheckTerms, + decodeArgsEqualityCheckTerms, +} from '../../src/caveats/argsEqualityCheck'; describe('ArgsEqualityCheck', () => { describe('createArgsEqualityCheckTerms', () => { @@ -35,14 +38,18 @@ describe('ArgsEqualityCheck', () => { describe('decodeArgsEqualityCheckTerms', () => { it('decodes arbitrary args hex', () => { const args = '0x1234abcd' as `0x${string}`; - expect(decodeArgsEqualityCheckTerms(createArgsEqualityCheckTerms({ args }))).toStrictEqual({ + expect( + decodeArgsEqualityCheckTerms(createArgsEqualityCheckTerms({ args })), + ).toStrictEqual({ args, }); }); it('decodes empty args', () => { expect( - decodeArgsEqualityCheckTerms(createArgsEqualityCheckTerms({ args: '0x' })), + decodeArgsEqualityCheckTerms( + createArgsEqualityCheckTerms({ args: '0x' }), + ), ).toStrictEqual({ args: '0x' }); }); diff --git a/packages/delegation-core/test/caveats/blockNumber.test.ts b/packages/delegation-core/test/caveats/blockNumber.test.ts index 8ef4515f..430252b6 100644 --- a/packages/delegation-core/test/caveats/blockNumber.test.ts +++ b/packages/delegation-core/test/caveats/blockNumber.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createBlockNumberTerms, decodeBlockNumberTerms } from '../../src/caveats/blockNumber'; +import { + createBlockNumberTerms, + decodeBlockNumberTerms, +} from '../../src/caveats/blockNumber'; describe('BlockNumber', () => { describe('createBlockNumberTerms', () => { diff --git a/packages/delegation-core/test/caveats/deployed.test.ts b/packages/delegation-core/test/caveats/deployed.test.ts index 71324729..5929a04b 100644 --- a/packages/delegation-core/test/caveats/deployed.test.ts +++ b/packages/delegation-core/test/caveats/deployed.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createDeployedTerms, decodeDeployedTerms } from '../../src/caveats/deployed'; +import { + createDeployedTerms, + decodeDeployedTerms, +} from '../../src/caveats/deployed'; describe('Deployed', () => { describe('createDeployedTerms', () => { diff --git a/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts index 1f23fc0f..d32d5bee 100644 --- a/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createERC1155BalanceChangeTerms, decodeERC1155BalanceChangeTerms } from '../../src/caveats/erc1155BalanceChange'; +import { + createERC1155BalanceChangeTerms, + decodeERC1155BalanceChangeTerms, +} from '../../src/caveats/erc1155BalanceChange'; import { BalanceChangeType } from '../../src/caveats/types'; describe('ERC1155BalanceChange', () => { diff --git a/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts index af38757d..750bb9b1 100644 --- a/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createERC20BalanceChangeTerms, decodeERC20BalanceChangeTerms } from '../../src/caveats/erc20BalanceChange'; +import { + createERC20BalanceChangeTerms, + decodeERC20BalanceChangeTerms, +} from '../../src/caveats/erc20BalanceChange'; import { BalanceChangeType } from '../../src/caveats/types'; describe('ERC20BalanceChange', () => { diff --git a/packages/delegation-core/test/caveats/erc20Streaming.test.ts b/packages/delegation-core/test/caveats/erc20Streaming.test.ts index 4fd26709..a060975e 100644 --- a/packages/delegation-core/test/caveats/erc20Streaming.test.ts +++ b/packages/delegation-core/test/caveats/erc20Streaming.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createERC20StreamingTerms, decodeERC20StreamingTerms } from '../../src/caveats/erc20Streaming'; +import { + createERC20StreamingTerms, + decodeERC20StreamingTerms, +} from '../../src/caveats/erc20Streaming'; describe('ERC20Streaming', () => { describe('createERC20StreamingTerms', () => { @@ -383,7 +386,9 @@ describe('ERC20Streaming', () => { amountPerSecond, startTime, }), - ).toThrow('Invalid startTime: must be less than or equal to 253402300799'); + ).toThrow( + 'Invalid startTime: must be less than or equal to 253402300799', + ); }); it('throws an error for undefined tokenAddress', () => { @@ -600,31 +605,31 @@ describe('ERC20Streaming', () => { // 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, + 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, + 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, + 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, + 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([ @@ -680,8 +685,8 @@ describe('ERC20Streaming', () => { 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, + 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 @@ -690,9 +695,9 @@ describe('ERC20Streaming', () => { // 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, + 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 @@ -756,8 +761,8 @@ describe('ERC20Streaming', () => { // 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, + 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, + 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, 0xef, 0xab, 0xcd, ]; expect(addressBytes).toEqual(expectedAddressBytes); }); @@ -807,8 +812,8 @@ describe('ERC20Streaming', () => { // 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, + 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 @@ -828,8 +833,8 @@ describe('ERC20Streaming', () => { 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, + 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 @@ -860,7 +865,8 @@ describe('ERC20Streaming', () => { }); describe('decodeERC20StreamingTerms', () => { - const tokenAddress = '0x1234567890123456789012345678901234567890' as `0x${string}`; + const tokenAddress = + '0x1234567890123456789012345678901234567890' as `0x${string}`; it('decodes standard streaming parameters', () => { const original = { diff --git a/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts index 1ee73678..220858b4 100644 --- a/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts +++ b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createERC20TransferAmountTerms, decodeERC20TransferAmountTerms } from '../../src/caveats/erc20TransferAmount'; +import { + createERC20TransferAmountTerms, + decodeERC20TransferAmountTerms, +} from '../../src/caveats/erc20TransferAmount'; describe('ERC20TransferAmount', () => { describe('createERC20TransferAmountTerms', () => { @@ -54,7 +57,9 @@ describe('ERC20TransferAmount', () => { it('decodes token address and max amount', () => { const original = { tokenAddress, maxAmount: 10n }; expect( - decodeERC20TransferAmountTerms(createERC20TransferAmountTerms(original)), + decodeERC20TransferAmountTerms( + createERC20TransferAmountTerms(original), + ), ).toStrictEqual(original); }); diff --git a/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts index bab03dbf..8a39d2bb 100644 --- a/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createERC721BalanceChangeTerms, decodeERC721BalanceChangeTerms } from '../../src/caveats/erc721BalanceChange'; +import { + createERC721BalanceChangeTerms, + decodeERC721BalanceChangeTerms, +} from '../../src/caveats/erc721BalanceChange'; import { BalanceChangeType } from '../../src/caveats/types'; describe('ERC721BalanceChange', () => { diff --git a/packages/delegation-core/test/caveats/erc721Transfer.test.ts b/packages/delegation-core/test/caveats/erc721Transfer.test.ts index 9d1c4e76..baf634cb 100644 --- a/packages/delegation-core/test/caveats/erc721Transfer.test.ts +++ b/packages/delegation-core/test/caveats/erc721Transfer.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createERC721TransferTerms, decodeERC721TransferTerms } from '../../src/caveats/erc721Transfer'; +import { + createERC721TransferTerms, + decodeERC721TransferTerms, +} from '../../src/caveats/erc721Transfer'; describe('ERC721Transfer', () => { describe('createERC721TransferTerms', () => { @@ -53,16 +56,16 @@ describe('ERC721Transfer', () => { it('decodes token address and token id', () => { const original = { tokenAddress, tokenId: 42n }; - expect(decodeERC721TransferTerms(createERC721TransferTerms(original))).toStrictEqual( - original, - ); + expect( + decodeERC721TransferTerms(createERC721TransferTerms(original)), + ).toStrictEqual(original); }); it('decodes token id zero', () => { const original = { tokenAddress, tokenId: 0n }; - expect(decodeERC721TransferTerms(createERC721TransferTerms(original))).toStrictEqual( - original, - ); + expect( + decodeERC721TransferTerms(createERC721TransferTerms(original)), + ).toStrictEqual(original); }); it('accepts Uint8Array terms from the encoder', () => { diff --git a/packages/delegation-core/test/caveats/exactCalldata.test.ts b/packages/delegation-core/test/caveats/exactCalldata.test.ts index f693653b..dc2dff02 100644 --- a/packages/delegation-core/test/caveats/exactCalldata.test.ts +++ b/packages/delegation-core/test/caveats/exactCalldata.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createExactCalldataTerms, decodeExactCalldataTerms } from '../../src/caveats/exactCalldata'; +import { + createExactCalldataTerms, + decodeExactCalldataTerms, +} from '../../src/caveats/exactCalldata'; import type { Hex } from '../../src/types'; describe('ExactCalldata', () => { @@ -95,7 +98,9 @@ describe('ExactCalldata', () => { }); it('throws an error for non-string non-Uint8Array callData', () => { - expect(() => createExactCalldataTerms({ calldata: 1234 as any })).toThrow(); + expect(() => + createExactCalldataTerms({ calldata: 1234 as any }), + ).toThrow(); }); it('handles single function selector', () => { @@ -255,13 +260,17 @@ describe('ExactCalldata', () => { describe('decodeExactCalldataTerms', () => { it('decodes simple calldata', () => { const calldata = '0x1234567890abcdef' as Hex; - expect(decodeExactCalldataTerms(createExactCalldataTerms({ calldata }))).toStrictEqual({ + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ calldata, }); }); it('decodes empty calldata', () => { - expect(decodeExactCalldataTerms(createExactCalldataTerms({ calldata: '0x' }))).toStrictEqual({ + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata: '0x' })), + ).toStrictEqual({ calldata: '0x', }); }); @@ -269,35 +278,45 @@ describe('ExactCalldata', () => { it('decodes transfer calldata', () => { const calldata = '0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000' as Hex; - expect(decodeExactCalldataTerms(createExactCalldataTerms({ calldata }))).toStrictEqual({ + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ calldata, }); }); it('preserves hex casing', () => { const calldata = '0x1234567890AbCdEf' as Hex; - expect(decodeExactCalldataTerms(createExactCalldataTerms({ calldata }))).toStrictEqual({ + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ calldata, }); }); it('decodes very long calldata', () => { - const calldata = `0x${'a'.repeat(1000)}` as Hex; - expect(decodeExactCalldataTerms(createExactCalldataTerms({ calldata }))).toStrictEqual({ + const calldata: Hex = `0x${'a'.repeat(1000)}`; + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ calldata, }); }); it('decodes selector-only calldata', () => { const calldata = '0xa9059cbb' as Hex; - expect(decodeExactCalldataTerms(createExactCalldataTerms({ calldata }))).toStrictEqual({ + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ calldata, }); }); it('decodes odd-length calldata', () => { const calldata = '0x123' as Hex; - expect(decodeExactCalldataTerms(createExactCalldataTerms({ calldata }))).toStrictEqual({ + expect( + decodeExactCalldataTerms(createExactCalldataTerms({ calldata })), + ).toStrictEqual({ calldata, }); }); @@ -307,7 +326,9 @@ describe('ExactCalldata', () => { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, ]); expect( - decodeExactCalldataTerms(createExactCalldataTerms({ calldata: callDataBytes })), + decodeExactCalldataTerms( + createExactCalldataTerms({ calldata: callDataBytes }), + ), ).toStrictEqual({ calldata: '0x1234567890abcdef' }); }); diff --git a/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts b/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts index 71d40b4c..af52aa45 100644 --- a/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts +++ b/packages/delegation-core/test/caveats/exactCalldataBatch.test.ts @@ -2,7 +2,10 @@ import { encodeSingle } from '@metamask/abi-utils'; import { bytesToHex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { createExactCalldataBatchTerms, decodeExactCalldataBatchTerms } from '../../src/caveats/exactCalldataBatch'; +import { + createExactCalldataBatchTerms, + decodeExactCalldataBatchTerms, +} from '../../src/caveats/exactCalldataBatch'; import type { Hex } from '../../src/types'; describe('ExactCalldataBatch', () => { diff --git a/packages/delegation-core/test/caveats/exactExecution.test.ts b/packages/delegation-core/test/caveats/exactExecution.test.ts index 75ee963a..f2b84ae8 100644 --- a/packages/delegation-core/test/caveats/exactExecution.test.ts +++ b/packages/delegation-core/test/caveats/exactExecution.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createExactExecutionTerms, decodeExactExecutionTerms } from '../../src/caveats/exactExecution'; +import { + createExactExecutionTerms, + decodeExactExecutionTerms, +} from '../../src/caveats/exactExecution'; describe('ExactExecution', () => { describe('createExactExecutionTerms', () => { @@ -76,7 +79,8 @@ describe('ExactExecution', () => { }); describe('decodeExactExecutionTerms', () => { - const target = '0x00000000000000000000000000000000000000ab' as `0x${string}`; + const target = + '0x00000000000000000000000000000000000000ab' as `0x${string}`; it('decodes target, value, and calldata', () => { const original = { @@ -86,9 +90,9 @@ describe('ExactExecution', () => { callData: '0x1234' as `0x${string}`, }, }; - expect(decodeExactExecutionTerms(createExactExecutionTerms(original))).toStrictEqual( - original, - ); + expect( + decodeExactExecutionTerms(createExactExecutionTerms(original)), + ).toStrictEqual(original); }); it('decodes zero value and empty calldata', () => { @@ -99,9 +103,9 @@ describe('ExactExecution', () => { callData: '0x' as `0x${string}`, }, }; - expect(decodeExactExecutionTerms(createExactExecutionTerms(original))).toStrictEqual( - original, - ); + expect( + decodeExactExecutionTerms(createExactExecutionTerms(original)), + ).toStrictEqual(original); }); it('accepts Uint8Array terms from the encoder', () => { diff --git a/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts b/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts index ada79589..03f1db68 100644 --- a/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts +++ b/packages/delegation-core/test/caveats/exactExecutionBatch.test.ts @@ -2,7 +2,10 @@ import { encodeSingle } from '@metamask/abi-utils'; import { bytesToHex } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { createExactExecutionBatchTerms, decodeExactExecutionBatchTerms } from '../../src/caveats/exactExecutionBatch'; +import { + createExactExecutionBatchTerms, + decodeExactExecutionBatchTerms, +} from '../../src/caveats/exactExecutionBatch'; import type { Hex } from '../../src/types'; describe('ExactExecutionBatch', () => { @@ -63,7 +66,9 @@ describe('ExactExecutionBatch', () => { it('decodes multiple executions', () => { const original = { executions }; expect( - decodeExactExecutionBatchTerms(createExactExecutionBatchTerms(original)), + decodeExactExecutionBatchTerms( + createExactExecutionBatchTerms(original), + ), ).toStrictEqual(original); }); diff --git a/packages/delegation-core/test/caveats/limitedCalls.test.ts b/packages/delegation-core/test/caveats/limitedCalls.test.ts index 6dbe153b..691b8dd5 100644 --- a/packages/delegation-core/test/caveats/limitedCalls.test.ts +++ b/packages/delegation-core/test/caveats/limitedCalls.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createLimitedCallsTerms, decodeLimitedCallsTerms } from '../../src/caveats/limitedCalls'; +import { + createLimitedCallsTerms, + decodeLimitedCallsTerms, +} from '../../src/caveats/limitedCalls'; describe('LimitedCalls', () => { describe('createLimitedCallsTerms', () => { @@ -35,16 +38,16 @@ describe('LimitedCalls', () => { describe('decodeLimitedCallsTerms', () => { it('decodes a positive limit', () => { const original = { limit: 5 }; - expect(decodeLimitedCallsTerms(createLimitedCallsTerms(original))).toStrictEqual( - original, - ); + expect( + decodeLimitedCallsTerms(createLimitedCallsTerms(original)), + ).toStrictEqual(original); }); it('decodes a larger limit', () => { const original = { limit: 999_999 }; - expect(decodeLimitedCallsTerms(createLimitedCallsTerms(original))).toStrictEqual( - original, - ); + expect( + decodeLimitedCallsTerms(createLimitedCallsTerms(original)), + ).toStrictEqual(original); }); it('accepts Uint8Array terms from the encoder', () => { diff --git a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts index d6637530..ac711d98 100644 --- a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts +++ b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createMultiTokenPeriodTerms, decodeMultiTokenPeriodTerms } from '../../src/caveats/multiTokenPeriod'; +import { + createMultiTokenPeriodTerms, + decodeMultiTokenPeriodTerms, +} from '../../src/caveats/multiTokenPeriod'; describe('MultiTokenPeriod', () => { describe('createMultiTokenPeriodTerms', () => { @@ -114,7 +117,8 @@ describe('MultiTokenPeriod', () => { describe('decodeMultiTokenPeriodTerms', () => { const token = '0x0000000000000000000000000000000000000011' as `0x${string}`; - const token2 = '0x0000000000000000000000000000000000000022' as `0x${string}`; + const token2 = + '0x0000000000000000000000000000000000000022' as `0x${string}`; it('decodes a single token config', () => { const original = { diff --git a/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts index 7d85c82f..83e6527f 100644 --- a/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createNativeBalanceChangeTerms, decodeNativeBalanceChangeTerms } from '../../src/caveats/nativeBalanceChange'; +import { + createNativeBalanceChangeTerms, + decodeNativeBalanceChangeTerms, +} from '../../src/caveats/nativeBalanceChange'; import { BalanceChangeType } from '../../src/caveats/types'; describe('NativeBalanceChange', () => { @@ -67,7 +70,8 @@ describe('NativeBalanceChange', () => { }); describe('decodeNativeBalanceChangeTerms', () => { - const recipient = '0x00000000000000000000000000000000000000cc' as `0x${string}`; + const recipient = + '0x00000000000000000000000000000000000000cc' as `0x${string}`; it('decodes increase balance change', () => { const original = { @@ -79,7 +83,9 @@ describe('NativeBalanceChange', () => { createNativeBalanceChangeTerms(original), ); expect(decoded.changeType).toBe(original.changeType); - expect((decoded.recipient as string).toLowerCase()).toBe(recipient.toLowerCase()); + expect((decoded.recipient as string).toLowerCase()).toBe( + recipient.toLowerCase(), + ); expect(decoded.balance).toBe(original.balance); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts index 86d48fa1..fe2b15e9 100644 --- a/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createNativeTokenPaymentTerms, decodeNativeTokenPaymentTerms } from '../../src/caveats/nativeTokenPayment'; +import { + createNativeTokenPaymentTerms, + decodeNativeTokenPaymentTerms, +} from '../../src/caveats/nativeTokenPayment'; describe('NativeTokenPayment', () => { describe('createNativeTokenPaymentTerms', () => { @@ -48,14 +51,17 @@ describe('NativeTokenPayment', () => { }); describe('decodeNativeTokenPaymentTerms', () => { - const recipient = '0x00000000000000000000000000000000000000bb' as `0x${string}`; + 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.recipient as string).toLowerCase()).toBe( + recipient.toLowerCase(), + ); expect(decoded.amount).toBe(original.amount); }); @@ -63,7 +69,9 @@ describe('NativeTokenPayment', () => { 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.recipient as string).toLowerCase()).toBe( + recipient.toLowerCase(), + ); expect(decoded.amount).toBe(5n); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts b/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts index 96d4cfd6..fbb13036 100644 --- a/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts @@ -1,7 +1,10 @@ import { isStrictHexString } from '@metamask/utils'; import { describe, it, expect } from 'vitest'; -import { createNativeTokenPeriodTransferTerms, decodeNativeTokenPeriodTransferTerms } from '../../src/caveats/nativeTokenPeriodTransfer'; +import { + createNativeTokenPeriodTransferTerms, + decodeNativeTokenPeriodTransferTerms, +} from '../../src/caveats/nativeTokenPeriodTransfer'; describe('NativeTokenPeriodTransfer', () => { describe('createNativeTokenPeriodTransferTerms', () => { @@ -360,8 +363,12 @@ describe('NativeTokenPeriodTransfer', () => { periodDuration: 3600, startDate: 1640995200, }; - const bytes = createNativeTokenPeriodTransferTerms(original, { out: 'bytes' }); - expect(decodeNativeTokenPeriodTransferTerms(bytes)).toStrictEqual(original); + const bytes = createNativeTokenPeriodTransferTerms(original, { + out: 'bytes', + }); + expect(decodeNativeTokenPeriodTransferTerms(bytes)).toStrictEqual( + original, + ); }); }); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts b/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts index e5803905..1beddc28 100644 --- a/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createNativeTokenStreamingTerms, decodeNativeTokenStreamingTerms } from '../../src/caveats/nativeTokenStreaming'; +import { + createNativeTokenStreamingTerms, + decodeNativeTokenStreamingTerms, +} from '../../src/caveats/nativeTokenStreaming'; describe('NativeTokenStreaming', () => { describe('createNativeTokenStreamingTerms', () => { @@ -274,7 +277,9 @@ describe('NativeTokenStreaming', () => { amountPerSecond, startTime, }), - ).toThrow('Invalid startTime: must be less than or equal to 253402300799'); + ).toThrow( + 'Invalid startTime: must be less than or equal to 253402300799', + ); }); it('throws an error for undefined initialAmount', () => { @@ -559,7 +564,9 @@ describe('NativeTokenStreaming', () => { 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(0, 32))).toEqual( + new Array(32).fill(0xff), + ); expect(Array.from(result.slice(32, 64))).toEqual( new Array(32).fill(0xff), ); @@ -579,7 +586,9 @@ describe('NativeTokenStreaming', () => { startTime: 1640995200, }; expect( - decodeNativeTokenStreamingTerms(createNativeTokenStreamingTerms(original)), + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), ).toStrictEqual(original); }); @@ -591,7 +600,9 @@ describe('NativeTokenStreaming', () => { startTime: 1672531200, }; expect( - decodeNativeTokenStreamingTerms(createNativeTokenStreamingTerms(original)), + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), ).toStrictEqual(original); }); @@ -603,7 +614,9 @@ describe('NativeTokenStreaming', () => { startTime: 1640995200, }; expect( - decodeNativeTokenStreamingTerms(createNativeTokenStreamingTerms(original)), + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), ).toStrictEqual(original); }); @@ -615,7 +628,9 @@ describe('NativeTokenStreaming', () => { startTime: 1, }; expect( - decodeNativeTokenStreamingTerms(createNativeTokenStreamingTerms(original)), + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), ).toStrictEqual(original); }); @@ -627,7 +642,9 @@ describe('NativeTokenStreaming', () => { startTime: 253402300799, }; expect( - decodeNativeTokenStreamingTerms(createNativeTokenStreamingTerms(original)), + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), ).toStrictEqual(original); }); @@ -641,7 +658,9 @@ describe('NativeTokenStreaming', () => { startTime: 1640995200, }; expect( - decodeNativeTokenStreamingTerms(createNativeTokenStreamingTerms(original)), + decodeNativeTokenStreamingTerms( + createNativeTokenStreamingTerms(original), + ), ).toStrictEqual(original); }); diff --git a/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts index 63d4bff1..3009c529 100644 --- a/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createNativeTokenTransferAmountTerms, decodeNativeTokenTransferAmountTerms } from '../../src/caveats/nativeTokenTransferAmount'; +import { + createNativeTokenTransferAmountTerms, + decodeNativeTokenTransferAmountTerms, +} from '../../src/caveats/nativeTokenTransferAmount'; describe('NativeTokenTransferAmount', () => { describe('createNativeTokenTransferAmountTerms', () => { @@ -59,7 +62,9 @@ describe('NativeTokenTransferAmount', () => { { maxAmount: 1n }, { out: 'bytes' }, ); - expect(decodeNativeTokenTransferAmountTerms(bytes)).toStrictEqual({ maxAmount: 1n }); + expect(decodeNativeTokenTransferAmountTerms(bytes)).toStrictEqual({ + maxAmount: 1n, + }); }); }); }); diff --git a/packages/delegation-core/test/caveats/nonce.test.ts b/packages/delegation-core/test/caveats/nonce.test.ts index f64e091b..b1ac682a 100644 --- a/packages/delegation-core/test/caveats/nonce.test.ts +++ b/packages/delegation-core/test/caveats/nonce.test.ts @@ -166,9 +166,9 @@ describe('Nonce', () => { 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, + 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 }); @@ -256,9 +256,9 @@ describe('Nonce', () => { 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, + 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); }); @@ -442,10 +442,14 @@ describe('Nonce', () => { }); it('decodes minimal and zero-equivalent hex forms', () => { - expect(decodeNonceTerms(createNonceTerms({ nonce: '0x1' })).nonce).toStrictEqual( + expect( + decodeNonceTerms(createNonceTerms({ nonce: '0x1' })).nonce, + ).toStrictEqual( '0x0000000000000000000000000000000000000000000000000000000000000001', ); - expect(decodeNonceTerms(createNonceTerms({ nonce: '0x0' })).nonce).toStrictEqual( + expect( + decodeNonceTerms(createNonceTerms({ nonce: '0x0' })).nonce, + ).toStrictEqual( '0x0000000000000000000000000000000000000000000000000000000000000000', ); }); diff --git a/packages/delegation-core/test/caveats/ownershipTransfer.test.ts b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts index 6e54d902..9f177802 100644 --- a/packages/delegation-core/test/caveats/ownershipTransfer.test.ts +++ b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createOwnershipTransferTerms, decodeOwnershipTransferTerms } from '../../src/caveats/ownershipTransfer'; +import { + createOwnershipTransferTerms, + decodeOwnershipTransferTerms, +} from '../../src/caveats/ownershipTransfer'; describe('OwnershipTransfer', () => { describe('createOwnershipTransferTerms', () => { @@ -35,13 +38,20 @@ describe('OwnershipTransfer', () => { it('decodes contract address', () => { expect( - decodeOwnershipTransferTerms(createOwnershipTransferTerms({ contractAddress })), + decodeOwnershipTransferTerms( + createOwnershipTransferTerms({ contractAddress }), + ), ).toStrictEqual({ contractAddress }); }); it('accepts Uint8Array terms from the encoder', () => { - const bytes = createOwnershipTransferTerms({ contractAddress }, { out: 'bytes' }); - expect(decodeOwnershipTransferTerms(bytes)).toStrictEqual({ contractAddress }); + const bytes = createOwnershipTransferTerms( + { contractAddress }, + { out: 'bytes' }, + ); + expect(decodeOwnershipTransferTerms(bytes)).toStrictEqual({ + contractAddress, + }); }); }); }); diff --git a/packages/delegation-core/test/caveats/redeemer.test.ts b/packages/delegation-core/test/caveats/redeemer.test.ts index 0ed72474..6ea64226 100644 --- a/packages/delegation-core/test/caveats/redeemer.test.ts +++ b/packages/delegation-core/test/caveats/redeemer.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createRedeemerTerms, decodeRedeemerTerms } from '../../src/caveats/redeemer'; +import { + createRedeemerTerms, + decodeRedeemerTerms, +} from '../../src/caveats/redeemer'; describe('Redeemer', () => { describe('createRedeemerTerms', () => { @@ -18,7 +21,9 @@ describe('Redeemer', () => { it('throws when redeemers is undefined', () => { expect(() => createRedeemerTerms({} as Parameters[0]), - ).toThrow('Invalid redeemers: must specify at least one redeemer address'); + ).toThrow( + 'Invalid redeemers: must specify at least one redeemer address', + ); }); it('throws for empty redeemers', () => { @@ -45,17 +50,23 @@ describe('Redeemer', () => { }); describe('decodeRedeemerTerms', () => { - const redeemerA = '0x0000000000000000000000000000000000000001' as `0x${string}`; - const redeemerB = '0x0000000000000000000000000000000000000002' as `0x${string}`; + 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); + expect(decodeRedeemerTerms(createRedeemerTerms(original))).toStrictEqual( + original, + ); }); it('decodes a single redeemer', () => { const original = { redeemers: [redeemerA] }; - expect(decodeRedeemerTerms(createRedeemerTerms(original))).toStrictEqual(original); + expect(decodeRedeemerTerms(createRedeemerTerms(original))).toStrictEqual( + original, + ); }); it('accepts Uint8Array terms from the encoder', () => { diff --git a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts index c6fc0295..e4f1e371 100644 --- a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts +++ b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createSpecificActionERC20TransferBatchTerms, decodeSpecificActionERC20TransferBatchTerms } from '../../src/caveats/specificActionERC20TransferBatch'; +import { + createSpecificActionERC20TransferBatchTerms, + decodeSpecificActionERC20TransferBatchTerms, +} from '../../src/caveats/specificActionERC20TransferBatch'; describe('SpecificActionERC20TransferBatch', () => { describe('createSpecificActionERC20TransferBatchTerms', () => { @@ -113,7 +116,9 @@ describe('SpecificActionERC20TransferBatch', () => { const bytes = createSpecificActionERC20TransferBatchTerms(original, { out: 'bytes', }); - expect(decodeSpecificActionERC20TransferBatchTerms(bytes)).toStrictEqual(original); + expect(decodeSpecificActionERC20TransferBatchTerms(bytes)).toStrictEqual( + original, + ); }); }); }); diff --git a/packages/delegation-core/test/caveats/timestamp.test.ts b/packages/delegation-core/test/caveats/timestamp.test.ts index 80a4f9ca..24612565 100644 --- a/packages/delegation-core/test/caveats/timestamp.test.ts +++ b/packages/delegation-core/test/caveats/timestamp.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createTimestampTerms, decodeTimestampTerms } from '../../src/caveats/timestamp'; +import { + createTimestampTerms, + decodeTimestampTerms, +} from '../../src/caveats/timestamp'; describe('Timestamp', () => { describe('createTimestampTerms', () => { @@ -268,13 +271,13 @@ describe('Timestamp', () => { // 1640995200 == 0x61cf9980 const afterThresholdBytes = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x61, 0xcf, 0x99, 0x80, + 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, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x63, 0xb0, 0xcd, 0x00, ]; const expectedButes = new Uint8Array([ ...afterThresholdBytes, @@ -305,7 +308,9 @@ describe('Timestamp', () => { timestampAfterThreshold: 1640995200, timestampBeforeThreshold: 1672531200, }; - expect(decodeTimestampTerms(createTimestampTerms(original))).toStrictEqual(original); + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); it('decodes both thresholds zero', () => { @@ -313,7 +318,9 @@ describe('Timestamp', () => { timestampAfterThreshold: 0, timestampBeforeThreshold: 0, }; - expect(decodeTimestampTerms(createTimestampTerms(original))).toStrictEqual(original); + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); it('decodes only after threshold set', () => { @@ -321,7 +328,9 @@ describe('Timestamp', () => { timestampAfterThreshold: 1640995200, timestampBeforeThreshold: 0, }; - expect(decodeTimestampTerms(createTimestampTerms(original))).toStrictEqual(original); + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); it('decodes only before threshold set', () => { @@ -329,7 +338,9 @@ describe('Timestamp', () => { timestampAfterThreshold: 0, timestampBeforeThreshold: 1672531200, }; - expect(decodeTimestampTerms(createTimestampTerms(original))).toStrictEqual(original); + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); it('decodes small positive timestamps', () => { @@ -337,7 +348,9 @@ describe('Timestamp', () => { timestampAfterThreshold: 1, timestampBeforeThreshold: 2, }; - expect(decodeTimestampTerms(createTimestampTerms(original))).toStrictEqual(original); + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); it('decodes maximum allowed before threshold', () => { @@ -346,7 +359,9 @@ describe('Timestamp', () => { timestampAfterThreshold: 0, timestampBeforeThreshold: maxTimestamp, }; - expect(decodeTimestampTerms(createTimestampTerms(original))).toStrictEqual(original); + expect( + decodeTimestampTerms(createTimestampTerms(original)), + ).toStrictEqual(original); }); it('accepts Uint8Array terms from the encoder', () => { diff --git a/packages/delegation-core/test/caveats/valueLte.test.ts b/packages/delegation-core/test/caveats/valueLte.test.ts index 7254b650..7b8b1c08 100644 --- a/packages/delegation-core/test/caveats/valueLte.test.ts +++ b/packages/delegation-core/test/caveats/valueLte.test.ts @@ -1,6 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { createValueLteTerms, decodeValueLteTerms } from '../../src/caveats/valueLte'; +import { + createValueLteTerms, + decodeValueLteTerms, +} from '../../src/caveats/valueLte'; describe('ValueLte', () => { describe('createValueLteTerms', () => { @@ -68,7 +71,9 @@ describe('ValueLte', () => { }); it('throws an error for undefined maxValue', () => { - expect(() => createValueLteTerms({ maxValue: undefined as any })).toThrow(); + expect(() => + createValueLteTerms({ maxValue: undefined as any }), + ).toThrow(); }); it('throws an error for null maxValue', () => { @@ -136,26 +141,34 @@ describe('ValueLte', () => { describe('decodeValueLteTerms', () => { it('decodes zero maxValue', () => { - expect(decodeValueLteTerms(createValueLteTerms({ maxValue: 0n }))).toStrictEqual({ + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue: 0n })), + ).toStrictEqual({ maxValue: 0n, }); }); it('decodes one wei', () => { - expect(decodeValueLteTerms(createValueLteTerms({ maxValue: 1n }))).toStrictEqual({ + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue: 1n })), + ).toStrictEqual({ maxValue: 1n, }); }); it('decodes 1 ETH in wei', () => { const maxValue = 1000000000000000000n; - expect(decodeValueLteTerms(createValueLteTerms({ maxValue }))).toStrictEqual({ + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue })), + ).toStrictEqual({ maxValue, }); }); it('decodes 255 padded as uint256', () => { - expect(decodeValueLteTerms(createValueLteTerms({ maxValue: 255n }))).toStrictEqual({ + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue: 255n })), + ).toStrictEqual({ maxValue: 255n, }); }); @@ -163,7 +176,9 @@ describe('ValueLte', () => { it('decodes maximum uint256', () => { const maxValue = 115792089237316195423570985008687907853269984665640564039457584007913129639935n; - expect(decodeValueLteTerms(createValueLteTerms({ maxValue }))).toStrictEqual({ + expect( + decodeValueLteTerms(createValueLteTerms({ maxValue })), + ).toStrictEqual({ maxValue, }); }); diff --git a/packages/delegation-core/test/internalUtils.test.ts b/packages/delegation-core/test/internalUtils.test.ts index eeea35a7..45cc3c96 100644 --- a/packages/delegation-core/test/internalUtils.test.ts +++ b/packages/delegation-core/test/internalUtils.test.ts @@ -95,9 +95,7 @@ describe('internal utils', () => { 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`, - ); + expect(toHexString({ value: 1, size: 32 })).toBe(`${'0'.repeat(62)}01`); }); it('works with bigint', () => { @@ -117,11 +115,11 @@ describe('internal utils', () => { }); describe('extractBigInt', () => { - const hex = + const value = '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' as const; it('reads uint256 at offset 0', () => { - expect(extractBigInt(hex, 0, 32)).toBe(1000000000000000000n); + expect(extractBigInt(value, 0, 32)).toBe(1000000000000000000n); }); it('reads a slice at a non-zero offset', () => { From 45b7b90fb6deedc93df7312934c59604108279c9 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:44:18 +1300 Subject: [PATCH 08/15] Add caveat terms decoder to smart-accounts-kit - Add generic TBytesLike parameter to core caveat config types - Rename timestampBuilder config properties in core to match smart-accounts-kit --- packages/delegation-core/README.md | 16 +- .../src/caveats/allowedCalldata.ts | 31 +- .../src/caveats/allowedMethods.ts | 32 +- .../src/caveats/allowedTargets.ts | 32 +- .../src/caveats/argsEqualityCheck.ts | 31 +- .../delegation-core/src/caveats/deployed.ts | 44 ++- .../src/caveats/erc1155BalanceChange.ts | 42 ++- .../src/caveats/erc20BalanceChange.ts | 54 +++- .../src/caveats/erc20Streaming.ts | 36 ++- .../src/caveats/erc20TokenPeriodTransfer.ts | 37 ++- .../src/caveats/erc20TransferAmount.ts | 42 ++- .../src/caveats/erc721BalanceChange.ts | 54 +++- .../src/caveats/erc721Transfer.ts | 33 +- .../src/caveats/exactCalldata.ts | 33 +- .../src/caveats/exactCalldataBatch.ts | 45 ++- .../src/caveats/exactExecution.ts | 38 ++- .../src/caveats/exactExecutionBatch.ts | 45 ++- .../src/caveats/multiTokenPeriod.ts | 41 ++- .../src/caveats/nativeBalanceChange.ts | 47 ++- .../src/caveats/nativeTokenPayment.ts | 42 ++- packages/delegation-core/src/caveats/nonce.ts | 30 +- .../src/caveats/ownershipTransfer.ts | 32 +- .../delegation-core/src/caveats/redeemer.ts | 34 ++- .../specificActionERC20TransferBatch.ts | 50 +++- .../delegation-core/src/caveats/timestamp.ts | 43 ++- packages/delegation-core/src/returns.ts | 7 + .../test/caveats/decoders.test.ts | 8 +- .../test/caveats/timestamp.test.ts | 184 ++++++------ .../src/caveatBuilder/timestampBuilder.ts | 4 +- packages/smart-accounts-kit/src/caveats.ts | 169 ++++++++++- packages/smart-accounts-kit/src/index.ts | 2 +- .../caveatBuilder/timestampBuilder.test.ts | 18 +- .../smart-accounts-kit/test/caveats.test.ts | 281 ++++++++++++++++++ 33 files changed, 1312 insertions(+), 325 deletions(-) create mode 100644 packages/smart-accounts-kit/test/caveats.test.ts 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 07d5ffad..293da9c3 100644 --- a/packages/delegation-core/src/caveats/allowedCalldata.ts +++ b/packages/delegation-core/src/caveats/allowedCalldata.ts @@ -17,6 +17,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -25,9 +26,9 @@ import type { Hex } from '../types'; /** * Terms for configuring an AllowedCalldata caveat. */ -export type AllowedCalldataTerms = { +export type AllowedCalldataTerms = { startIndex: number; - value: BytesLike; + value: TBytesLike; }; /** @@ -90,15 +91,35 @@ export function createAllowedCalldataTerms( * 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, -): AllowedCalldataTerms { + encodingOptions?: EncodingOptions<'hex'>, +): AllowedCalldataTerms>; +export function decodeAllowedCalldataTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): AllowedCalldataTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeAllowedCalldataTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | AllowedCalldataTerms> + | AllowedCalldataTerms> { const hexTerms = bytesLikeToHex(terms); const startIndex = extractNumber(hexTerms, 0, 32); - const value = extractRemainingHex(hexTerms, 32); + const valueHex = extractRemainingHex(hexTerms, 32); + const value = prepareResult(valueHex, encodingOptions); - return { startIndex, value }; + 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 96b4cad9..06f34758 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -13,6 +13,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -21,9 +22,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 @@ -90,22 +91,41 @@ export function createAllowedMethodsTerms( * 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, -): AllowedMethodsTerms { + encodingOptions?: EncodingOptions<'hex'>, +): AllowedMethodsTerms>; +export function decodeAllowedMethodsTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): AllowedMethodsTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeAllowedMethodsTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | AllowedMethodsTerms> + | AllowedMethodsTerms> { const hexTerms = bytesLikeToHex(terms); const selectorSize = 4; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const selectorCount = totalBytes / selectorSize; - const selectors: `0x${string}`[] = []; + const selectors: (Hex | Uint8Array)[] = []; for (let i = 0; i < selectorCount; i++) { const selector = extractHex(hexTerms, i * selectorSize, selectorSize); - selectors.push(selector); + selectors.push(prepareResult(selector, encodingOptions)); } - return { selectors }; + return { selectors } as + | AllowedMethodsTerms> + | AllowedMethodsTerms>; } diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index 1a1a59fe..e3d51c02 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -13,6 +13,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -21,9 +22,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[]; }; /** @@ -74,22 +75,41 @@ export function createAllowedTargetsTerms( * 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, -): AllowedTargetsTerms { + encodingOptions?: EncodingOptions<'hex'>, +): AllowedTargetsTerms>; +export function decodeAllowedTargetsTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): AllowedTargetsTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeAllowedTargetsTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | AllowedTargetsTerms> + | AllowedTargetsTerms> { const hexTerms = bytesLikeToHex(terms); const addressSize = 20; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const addressCount = totalBytes / addressSize; - const targets: `0x${string}`[] = []; + const targets: (Hex | Uint8Array)[] = []; for (let i = 0; i < addressCount; i++) { const target = extractAddress(hexTerms, i * addressSize); - targets.push(target); + targets.push(prepareResult(target, encodingOptions)); } - return { targets }; + return { targets } as + | AllowedTargetsTerms> + | AllowedTargetsTerms>; } diff --git a/packages/delegation-core/src/caveats/argsEqualityCheck.ts b/packages/delegation-core/src/caveats/argsEqualityCheck.ts index 607bdaa2..1c569ae3 100644 --- a/packages/delegation-core/src/caveats/argsEqualityCheck.ts +++ b/packages/delegation-core/src/caveats/argsEqualityCheck.ts @@ -13,6 +13,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -21,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; }; /** @@ -72,11 +73,31 @@ export function createArgsEqualityCheckTerms( * 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, -): ArgsEqualityCheckTerms { - const args = bytesLikeToHex(terms); - return { args }; + encodingOptions?: EncodingOptions<'hex'>, +): ArgsEqualityCheckTerms>; +export function decodeArgsEqualityCheckTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ArgsEqualityCheckTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +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/deployed.ts b/packages/delegation-core/src/caveats/deployed.ts index 11f7544b..de708477 100644 --- a/packages/delegation-core/src/caveats/deployed.ts +++ b/packages/delegation-core/src/caveats/deployed.ts @@ -21,6 +21,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -29,13 +30,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; }; /** @@ -95,14 +96,39 @@ export function createDeployedTerms( * 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): DeployedTerms { +export function decodeDeployedTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): DeployedTerms>; +export function decodeDeployedTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): DeployedTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeDeployedTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | DeployedTerms> + | DeployedTerms> { const hexTerms = bytesLikeToHex(terms); - const contractAddress = extractAddress(hexTerms, 0); - const salt = extractHex(hexTerms, 20, 32); - const bytecode = extractRemainingHex(hexTerms, 52); + const contractAddressHex = extractAddress(hexTerms, 0); + const saltHex = extractHex(hexTerms, 20, 32); + const bytecodeHex = extractRemainingHex(hexTerms, 52); - return { contractAddress, salt, bytecode }; + 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 27827513..a2650438 100644 --- a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts @@ -20,6 +20,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -29,11 +30,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. */ @@ -122,18 +125,43 @@ export function createERC1155BalanceChangeTerms( * 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, -): ERC1155BalanceChangeTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ERC1155BalanceChangeTerms>; +export function decodeERC1155BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC1155BalanceChangeTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeERC1155BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC1155BalanceChangeTerms> + | ERC1155BalanceChangeTerms> { const hexTerms = bytesLikeToHex(terms); const changeType = extractNumber(hexTerms, 0, 1); - const tokenAddress = extractAddress(hexTerms, 1); - const recipient = extractAddress(hexTerms, 21); + 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, recipient, tokenId, balance }; + 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 850f0cc7..b580f2e9 100644 --- a/packages/delegation-core/src/caveats/erc20BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc20BalanceChange.ts @@ -20,6 +20,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -29,16 +30,17 @@ 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. @@ -113,17 +115,41 @@ export function createERC20BalanceChangeTerms( * 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, -): ERC20BalanceChangeTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ERC20BalanceChangeTerms>; +export function decodeERC20BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC20BalanceChangeTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeERC20BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC20BalanceChangeTerms> + | ERC20BalanceChangeTerms> { const hexTerms = bytesLikeToHex(terms); const changeType = extractNumber(hexTerms, 0, 1); - const tokenAddress = extractAddress(hexTerms, 1); - const recipient = extractAddress(hexTerms, 21); + const tokenAddressHex = extractAddress(hexTerms, 1); + const recipientHex = extractAddress(hexTerms, 21); const balance = extractBigInt(hexTerms, 41, 32); - return { changeType, tokenAddress, recipient, balance }; + 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 520296bc..d371a11a 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -18,6 +18,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -29,9 +30,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. */ @@ -139,18 +140,43 @@ export function createERC20StreamingTerms( * 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, -): ERC20StreamingTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ERC20StreamingTerms>; +export function decodeERC20StreamingTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC20StreamingTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeERC20StreamingTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC20StreamingTerms> + | ERC20StreamingTerms> { const hexTerms = bytesLikeToHex(terms); - const tokenAddress = extractAddress(hexTerms, 0); + 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, initialAmount, maxAmount, amountPerSecond, startTime }; + 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 04e9a7cc..8a49300f 100644 --- a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -18,6 +18,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -26,9 +27,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. */ @@ -113,17 +116,41 @@ export function createERC20TokenPeriodTransferTerms( * 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, -): ERC20TokenPeriodTransferTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ERC20TokenPeriodTransferTerms>; +export function decodeERC20TokenPeriodTransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC20TokenPeriodTransferTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeERC20TokenPeriodTransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC20TokenPeriodTransferTerms> + | ERC20TokenPeriodTransferTerms> { const hexTerms = bytesLikeToHex(terms); - const tokenAddress = extractAddress(hexTerms, 0); + 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, periodAmount, periodDuration, startDate }; + 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 ef1149ce..46485247 100644 --- a/packages/delegation-core/src/caveats/erc20TransferAmount.ts +++ b/packages/delegation-core/src/caveats/erc20TransferAmount.ts @@ -19,6 +19,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -27,12 +28,13 @@ 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. @@ -83,15 +85,37 @@ export function createERC20TransferAmountTerms( * 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, -): ERC20TransferAmountTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ERC20TransferAmountTerms>; +export function decodeERC20TransferAmountTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC20TransferAmountTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeERC20TransferAmountTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC20TransferAmountTerms> + | ERC20TransferAmountTerms> { const hexTerms = bytesLikeToHex(terms); - const tokenAddress = extractAddress(hexTerms, 0); + const tokenAddressHex = extractAddress(hexTerms, 0); const maxAmount = extractBigInt(hexTerms, 20, 32); - return { tokenAddress, maxAmount }; + 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 f2761877..87526223 100644 --- a/packages/delegation-core/src/caveats/erc721BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc721BalanceChange.ts @@ -20,6 +20,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -29,16 +30,17 @@ 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. @@ -113,17 +115,41 @@ export function createERC721BalanceChangeTerms( * 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, -): ERC721BalanceChangeTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ERC721BalanceChangeTerms>; +export function decodeERC721BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC721BalanceChangeTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeERC721BalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC721BalanceChangeTerms> + | ERC721BalanceChangeTerms> { const hexTerms = bytesLikeToHex(terms); const changeType = extractNumber(hexTerms, 0, 1); - const tokenAddress = extractAddress(hexTerms, 1); - const recipient = extractAddress(hexTerms, 21); + const tokenAddressHex = extractAddress(hexTerms, 1); + const recipientHex = extractAddress(hexTerms, 21); const amount = extractBigInt(hexTerms, 41, 32); - return { changeType, tokenAddress, recipient, amount }; + 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 918099e3..21cf71df 100644 --- a/packages/delegation-core/src/caveats/erc721Transfer.ts +++ b/packages/delegation-core/src/caveats/erc721Transfer.ts @@ -19,6 +19,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -27,9 +28,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; }; @@ -83,15 +84,37 @@ export function createERC721TransferTerms( * 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, -): ERC721TransferTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ERC721TransferTerms>; +export function decodeERC721TransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ERC721TransferTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeERC721TransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ERC721TransferTerms> + | ERC721TransferTerms> { const hexTerms = bytesLikeToHex(terms); - const tokenAddress = extractAddress(hexTerms, 0); + const tokenAddressHex = extractAddress(hexTerms, 0); const tokenId = extractBigInt(hexTerms, 20, 32); - return { tokenAddress, tokenId }; + 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 24a0484c..a20c0ed3 100644 --- a/packages/delegation-core/src/caveats/exactCalldata.ts +++ b/packages/delegation-core/src/caveats/exactCalldata.ts @@ -12,6 +12,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -20,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; }; /** @@ -72,9 +73,31 @@ export function createExactCalldataTerms( * 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): ExactCalldataTerms { - const calldata = bytesLikeToHex(terms); - return { calldata }; +export function decodeExactCalldataTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): ExactCalldataTerms>; +export function decodeExactCalldataTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ExactCalldataTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +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 012cf556..8d743870 100644 --- a/packages/delegation-core/src/caveats/exactCalldataBatch.ts +++ b/packages/delegation-core/src/caveats/exactCalldataBatch.ts @@ -14,6 +14,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -22,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)[]'; @@ -100,22 +102,41 @@ export function createExactCalldataBatchTerms( * 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, -): ExactCalldataBatchTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ExactCalldataBatchTerms>; +export function decodeExactCalldataBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ExactCalldataBatchTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +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: target as `0x${string}`, + target: prepareResult(target, encodingOptions), value, - callData: bytesToHex(callData), + callData: prepareResult(bytesToHex(callData), encodingOptions), }), ); - return { executions }; + return { executions } as + | ExactCalldataBatchTerms> + | ExactCalldataBatchTerms>; } diff --git a/packages/delegation-core/src/caveats/exactExecution.ts b/packages/delegation-core/src/caveats/exactExecution.ts index 98404247..70ca8f58 100644 --- a/packages/delegation-core/src/caveats/exactExecution.ts +++ b/packages/delegation-core/src/caveats/exactExecution.ts @@ -20,6 +20,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -28,12 +29,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; }; }; @@ -98,22 +99,41 @@ export function createExactExecutionTerms( * 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, -): ExactExecutionTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ExactExecutionTerms>; +export function decodeExactExecutionTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ExactExecutionTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeExactExecutionTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | ExactExecutionTerms> + | ExactExecutionTerms> { const hexTerms = bytesLikeToHex(terms); - const target = extractAddress(hexTerms, 0); + const targetHex = extractAddress(hexTerms, 0); const value = extractBigInt(hexTerms, 20, 32); - const callData = extractRemainingHex(hexTerms, 52); + const callDataHex = extractRemainingHex(hexTerms, 52); return { execution: { - target, + target: prepareResult(targetHex, encodingOptions), value, - callData, + 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 8ec629d3..43c79736 100644 --- a/packages/delegation-core/src/caveats/exactExecutionBatch.ts +++ b/packages/delegation-core/src/caveats/exactExecutionBatch.ts @@ -14,6 +14,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -22,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)[]'; @@ -100,22 +102,41 @@ export function createExactExecutionBatchTerms( * 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, -): ExactExecutionBatchTerms { + encodingOptions?: EncodingOptions<'hex'>, +): ExactExecutionBatchTerms>; +export function decodeExactExecutionBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): ExactExecutionBatchTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +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: target as `0x${string}`, + target: prepareResult(target, encodingOptions), value, - callData: bytesToHex(callData), + callData: prepareResult(bytesToHex(callData), encodingOptions), }), ); - return { executions }; + return { executions } as + | ExactExecutionBatchTerms> + | ExactExecutionBatchTerms>; } diff --git a/packages/delegation-core/src/caveats/multiTokenPeriod.ts b/packages/delegation-core/src/caveats/multiTokenPeriod.ts index 047e1844..922fbf92 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -20,6 +20,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -28,8 +29,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; @@ -38,8 +39,8 @@ export type TokenPeriodConfig = { /** * Terms for configuring a MultiTokenPeriod caveat. */ -export type MultiTokenPeriodTerms = { - tokenConfigs: TokenPeriodConfig[]; +export type MultiTokenPeriodTerms = { + tokenConfigs: TokenPeriodConfig[]; }; /** @@ -114,11 +115,28 @@ export function createMultiTokenPeriodTerms( * 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, -): MultiTokenPeriodTerms { + encodingOptions?: EncodingOptions<'hex'>, +): MultiTokenPeriodTerms>; +export function decodeMultiTokenPeriodTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): MultiTokenPeriodTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeMultiTokenPeriodTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | MultiTokenPeriodTerms> + | MultiTokenPeriodTerms> { const hexTerms = bytesLikeToHex(terms); const configSize = 116; @@ -128,13 +146,20 @@ export function decodeMultiTokenPeriodTerms( const tokenConfigs: TokenPeriodConfig[] = []; for (let i = 0; i < configCount; i++) { const offset = i * configSize; - const token = extractAddress(hexTerms, offset); + 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, periodAmount, periodDuration, startDate }); + tokenConfigs.push({ + token: prepareResult(tokenHex, encodingOptions), + periodAmount, + periodDuration, + startDate, + }); } - return { tokenConfigs }; + return { tokenConfigs } as + | MultiTokenPeriodTerms> + | MultiTokenPeriodTerms>; } diff --git a/packages/delegation-core/src/caveats/nativeBalanceChange.ts b/packages/delegation-core/src/caveats/nativeBalanceChange.ts index 89f054ec..fd9fbe94 100644 --- a/packages/delegation-core/src/caveats/nativeBalanceChange.ts +++ b/packages/delegation-core/src/caveats/nativeBalanceChange.ts @@ -20,6 +20,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -29,14 +30,15 @@ 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. @@ -97,16 +99,39 @@ export function createNativeBalanceChangeTerms( * 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, -): NativeBalanceChangeTerms { + encodingOptions?: EncodingOptions<'hex'>, +): NativeBalanceChangeTerms>; +export function decodeNativeBalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): NativeBalanceChangeTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeNativeBalanceChangeTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | NativeBalanceChangeTerms> + | NativeBalanceChangeTerms> { const hexTerms = bytesLikeToHex(terms); const changeType = extractNumber(hexTerms, 0, 1); - const recipient = extractAddress(hexTerms, 1); + const recipientHex = extractAddress(hexTerms, 1); const balance = extractBigInt(hexTerms, 21, 32); - return { changeType, recipient, balance }; + 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 f8b1fb9a..a3db0b7a 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPayment.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPayment.ts @@ -19,6 +19,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -27,12 +28,13 @@ 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. @@ -83,15 +85,37 @@ export function createNativeTokenPaymentTerms( * 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, -): NativeTokenPaymentTerms { + encodingOptions?: EncodingOptions<'hex'>, +): NativeTokenPaymentTerms>; +export function decodeNativeTokenPaymentTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): NativeTokenPaymentTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeNativeTokenPaymentTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | NativeTokenPaymentTerms> + | NativeTokenPaymentTerms> { const hexTerms = bytesLikeToHex(terms); - const recipient = extractAddress(hexTerms, 0); + const recipientHex = extractAddress(hexTerms, 0); const amount = extractBigInt(hexTerms, 20, 32); - return { recipient, amount }; + return { + recipient: prepareResult(recipientHex, encodingOptions), + amount, + } as + | NativeTokenPaymentTerms> + | NativeTokenPaymentTerms>; } diff --git a/packages/delegation-core/src/caveats/nonce.ts b/packages/delegation-core/src/caveats/nonce.ts index 787b17aa..55ba1f52 100644 --- a/packages/delegation-core/src/caveats/nonce.ts +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -13,6 +13,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -24,9 +25,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; }; /** @@ -97,12 +98,31 @@ export function createNonceTerms( * 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): NonceTerms { +export function decodeNonceTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): NonceTerms>; +export function decodeNonceTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): NonceTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeNonceTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): NonceTerms> | NonceTerms> { const hexTerms = bytesLikeToHex(terms); - const nonce = hexTerms; + const nonce = prepareResult(hexTerms, encodingOptions); - return { nonce }; + return { nonce } as + | NonceTerms> + | NonceTerms>; } diff --git a/packages/delegation-core/src/caveats/ownershipTransfer.ts b/packages/delegation-core/src/caveats/ownershipTransfer.ts index dcaa73b7..e4cae8da 100644 --- a/packages/delegation-core/src/caveats/ownershipTransfer.ts +++ b/packages/delegation-core/src/caveats/ownershipTransfer.ts @@ -13,6 +13,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -21,9 +22,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; }; /** @@ -68,12 +69,33 @@ export function createOwnershipTransferTerms( * 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, -): OwnershipTransferTerms { + encodingOptions?: EncodingOptions<'hex'>, +): OwnershipTransferTerms>; +export function decodeOwnershipTransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): OwnershipTransferTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeOwnershipTransferTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | OwnershipTransferTerms> + | OwnershipTransferTerms> { const hexTerms = bytesLikeToHex(terms); - const contractAddress = extractAddress(hexTerms, 0); - return { contractAddress }; + 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 2598c059..71c77c3c 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -13,6 +13,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -21,9 +22,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[]; }; /** @@ -74,20 +75,41 @@ export function createRedeemerTerms( * 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): RedeemerTerms { +export function decodeRedeemerTerms( + terms: BytesLike, + encodingOptions?: EncodingOptions<'hex'>, +): RedeemerTerms>; +export function decodeRedeemerTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): RedeemerTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeRedeemerTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | RedeemerTerms> + | RedeemerTerms> { const hexTerms = bytesLikeToHex(terms); const addressSize = 20; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 const addressCount = totalBytes / addressSize; - const redeemers: `0x${string}`[] = []; + const redeemers: (Hex | Uint8Array)[] = []; for (let i = 0; i < addressCount; i++) { const redeemer = extractAddress(hexTerms, i * addressSize); - redeemers.push(redeemer); + redeemers.push(prepareResult(redeemer, encodingOptions)); } - return { redeemers }; + return { redeemers } as + | RedeemerTerms> + | RedeemerTerms>; } diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts index 666856b0..2206d4e6 100644 --- a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -20,6 +20,7 @@ import { bytesLikeToHex, defaultOptions, prepareResult, + type DecodedBytesLike, type EncodingOptions, type ResultValue, } from '../returns'; @@ -28,17 +29,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; + target: TBytesLike; /** The calldata for the first transaction. */ - calldata: BytesLike; + calldata: TBytesLike; }; /** @@ -119,18 +122,43 @@ export function createSpecificActionERC20TransferBatchTerms( * 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, -): SpecificActionERC20TransferBatchTerms { + encodingOptions?: EncodingOptions<'hex'>, +): SpecificActionERC20TransferBatchTerms>; +export function decodeSpecificActionERC20TransferBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions<'bytes'>, +): SpecificActionERC20TransferBatchTerms>; +/** + * + * @param terms + * @param encodingOptions + */ +export function decodeSpecificActionERC20TransferBatchTerms( + terms: BytesLike, + encodingOptions: EncodingOptions = defaultOptions, +): + | SpecificActionERC20TransferBatchTerms> + | SpecificActionERC20TransferBatchTerms> { const hexTerms = bytesLikeToHex(terms); - const tokenAddress = extractAddress(hexTerms, 0); - const recipient = extractAddress(hexTerms, 20); + const tokenAddressHex = extractAddress(hexTerms, 0); + const recipientHex = extractAddress(hexTerms, 20); const amount = extractBigInt(hexTerms, 40, 32); - const target = extractAddress(hexTerms, 72); - const calldata = extractRemainingHex(hexTerms, 92); + const targetHex = extractAddress(hexTerms, 72); + const calldataHex = extractRemainingHex(hexTerms, 92); - return { tokenAddress, recipient, amount, target, calldata }; + 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 5a32a112..45db3200 100644 --- a/packages/delegation-core/src/caveats/timestamp.ts +++ b/packages/delegation-core/src/caveats/timestamp.ts @@ -26,9 +26,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; }; /** @@ -59,47 +59,40 @@ 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, }); @@ -116,7 +109,7 @@ export function createTimestampTerms( */ export function decodeTimestampTerms(terms: BytesLike): TimestampTerms { const hexTerms = bytesLikeToHex(terms); - const timestampAfterThreshold = extractNumber(hexTerms, 0, 16); - const timestampBeforeThreshold = extractNumber(hexTerms, 16, 16); - return { timestampAfterThreshold, timestampBeforeThreshold }; + const afterThreshold = extractNumber(hexTerms, 0, 16); + const beforeThreshold = extractNumber(hexTerms, 16, 16); + return { afterThreshold, beforeThreshold }; } 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/decoders.test.ts b/packages/delegation-core/test/caveats/decoders.test.ts index 9f15237b..3db0fc96 100644 --- a/packages/delegation-core/test/caveats/decoders.test.ts +++ b/packages/delegation-core/test/caveats/decoders.test.ts @@ -85,8 +85,8 @@ describe('Terms Decoders', () => { describe('decodeTimestampTerms', () => { it('correctly decodes encoded terms', () => { const original = { - timestampAfterThreshold: 1640995200, - timestampBeforeThreshold: 1672531200, + afterThreshold: 1640995200, + beforeThreshold: 1672531200, }; const encoded = createTimestampTerms(original); const decoded = decodeTimestampTerms(encoded); @@ -95,8 +95,8 @@ describe('Terms Decoders', () => { it('decodes zero thresholds', () => { const original = { - timestampAfterThreshold: 0, - timestampBeforeThreshold: 0, + afterThreshold: 0, + beforeThreshold: 0, }; const encoded = createTimestampTerms(original); const decoded = decodeTimestampTerms(encoded); diff --git a/packages/delegation-core/test/caveats/timestamp.test.ts b/packages/delegation-core/test/caveats/timestamp.test.ts index 24612565..89b0bc43 100644 --- a/packages/delegation-core/test/caveats/timestamp.test.ts +++ b/packages/delegation-core/test/caveats/timestamp.test.ts @@ -9,11 +9,11 @@ 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 timestampAfterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC - const timestampBeforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const afterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC + const beforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }); expect(result).toStrictEqual( @@ -22,11 +22,11 @@ describe('Timestamp', () => { }); it('creates valid terms for zero thresholds', () => { - const timestampAfterThreshold = 0; - const timestampBeforeThreshold = 0; + const afterThreshold = 0; + const beforeThreshold = 0; const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }); expect(result).toStrictEqual( @@ -35,11 +35,11 @@ describe('Timestamp', () => { }); it('creates valid terms when only after threshold is set', () => { - const timestampAfterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC - const timestampBeforeThreshold = 0; + const afterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC + const beforeThreshold = 0; const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }); expect(result).toStrictEqual( @@ -48,11 +48,11 @@ describe('Timestamp', () => { }); it('creates valid terms when only before threshold is set', () => { - const timestampAfterThreshold = 0; - const timestampBeforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const afterThreshold = 0; + const beforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }); expect(result).toStrictEqual( @@ -61,11 +61,11 @@ describe('Timestamp', () => { }); it('creates valid terms for small timestamp values', () => { - const timestampAfterThreshold = 1; - const timestampBeforeThreshold = 2; + const afterThreshold = 1; + const beforeThreshold = 2; const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }); expect(result).toStrictEqual( @@ -76,8 +76,8 @@ describe('Timestamp', () => { it('creates valid terms for maximum allowed timestamps', () => { const maxTimestamp = 253402300799; // January 1, 10000 CE const result = createTimestampTerms({ - timestampAfterThreshold: maxTimestamp, - timestampBeforeThreshold: 0, + afterThreshold: maxTimestamp, + beforeThreshold: 0, }); expect(result).toStrictEqual( @@ -88,30 +88,30 @@ describe('Timestamp', () => { it('throws an error for negative after threshold', () => { expect(() => createTimestampTerms({ - timestampAfterThreshold: -1, - timestampBeforeThreshold: 0, + afterThreshold: -1, + beforeThreshold: 0, }), - ).toThrow('Invalid timestampAfterThreshold: must be zero or positive'); + ).toThrow('Invalid afterThreshold: must be zero or positive'); }); it('throws an error for negative before threshold', () => { expect(() => createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: -1, + afterThreshold: 0, + beforeThreshold: -1, }), - ).toThrow('Invalid timestampBeforeThreshold: must be zero or positive'); + ).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, + afterThreshold: 0, + beforeThreshold: overBound, }), ).toThrow( - 'Invalid timestampBeforeThreshold: must be less than or equal to 253402300799', + 'Invalid beforeThreshold: must be less than or equal to 253402300799', ); }); @@ -119,11 +119,11 @@ describe('Timestamp', () => { const overBound = 253402300800; // One second past January 1, 10000 CE expect(() => createTimestampTerms({ - timestampAfterThreshold: overBound, - timestampBeforeThreshold: 0, + afterThreshold: overBound, + beforeThreshold: 0, }), ).toThrow( - 'Invalid timestampAfterThreshold: must be less than or equal to 253402300799', + 'Invalid afterThreshold: must be less than or equal to 253402300799', ); }); @@ -131,89 +131,89 @@ describe('Timestamp', () => { const timestamp = 1640995200; expect(() => createTimestampTerms({ - timestampAfterThreshold: timestamp, - timestampBeforeThreshold: timestamp, + afterThreshold: timestamp, + beforeThreshold: timestamp, }), ).toThrow( - 'Invalid thresholds: timestampBeforeThreshold must be greater than timestampAfterThreshold when both are specified', + '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 + const afterThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const beforeThreshold = 1640995200; // 2022-01-01 00:00:00 UTC expect(() => createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }), ).toThrow( - 'Invalid thresholds: timestampBeforeThreshold must be greater than timestampAfterThreshold when both are specified', + 'Invalid thresholds: beforeThreshold must be greater than afterThreshold when both are specified', ); }); - it('throws an error for undefined timestampAfterThreshold', () => { + it('throws an error for undefined afterThreshold', () => { expect(() => createTimestampTerms({ - timestampAfterThreshold: undefined as any, - timestampBeforeThreshold: 0, + afterThreshold: undefined as any, + beforeThreshold: 0, }), ).toThrow(); }); - it('throws an error for null timestampAfterThreshold', () => { + it('throws an error for null afterThreshold', () => { expect(() => createTimestampTerms({ - timestampAfterThreshold: null as any, - timestampBeforeThreshold: 0, + afterThreshold: null as any, + beforeThreshold: 0, }), ).toThrow(); }); - it('throws an error for undefined timestampBeforeThreshold', () => { + it('throws an error for undefined beforeThreshold', () => { expect(() => createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: undefined as any, + afterThreshold: 0, + beforeThreshold: undefined as any, }), ).toThrow(); }); - it('throws an error for null timestampBeforeThreshold', () => { + it('throws an error for null beforeThreshold', () => { expect(() => createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: null as any, + afterThreshold: 0, + beforeThreshold: null as any, }), ).toThrow(); }); - it('throws an error for Infinity timestampAfterThreshold', () => { + it('throws an error for Infinity afterThreshold', () => { expect(() => createTimestampTerms({ - timestampAfterThreshold: Infinity, - timestampBeforeThreshold: 0, + afterThreshold: Infinity, + beforeThreshold: 0, }), ).toThrow(); }); - it('throws an error for Infinity timestampBeforeThreshold', () => { + it('throws an error for Infinity beforeThreshold', () => { expect(() => createTimestampTerms({ - timestampAfterThreshold: 0, - timestampBeforeThreshold: Infinity, + 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; + const afterThreshold = 1672531200; // 2023-01-01 00:00:00 UTC + const beforeThreshold = 0; // Should not throw const result = createTimestampTerms({ - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }); expect(result).toStrictEqual( '0x00000000000000000000000063b0cd0000000000000000000000000000000000', @@ -223,12 +223,12 @@ describe('Timestamp', () => { // 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 afterThreshold = 1640995200; // 2022-01-01 00:00:00 UTC + const beforeThreshold = 1672531200; // 2023-01-01 00:00:00 UTC const result = createTimestampTerms( { - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }, { out: 'bytes' }, ); @@ -238,12 +238,12 @@ describe('Timestamp', () => { }); it('returns Uint8Array for zero thresholds with bytes encoding', () => { - const timestampAfterThreshold = 0; - const timestampBeforeThreshold = 0; + const afterThreshold = 0; + const beforeThreshold = 0; const result = createTimestampTerms( { - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }, { out: 'bytes' }, ); @@ -256,12 +256,12 @@ describe('Timestamp', () => { }); it('returns Uint8Array for single timestamp with bytes encoding', () => { - const timestampAfterThreshold = 1640995200; - const timestampBeforeThreshold = 1672531200; + const afterThreshold = 1640995200; + const beforeThreshold = 1672531200; const result = createTimestampTerms( { - timestampAfterThreshold, - timestampBeforeThreshold, + afterThreshold, + beforeThreshold, }, { out: 'bytes' }, ); @@ -290,8 +290,8 @@ describe('Timestamp', () => { const maxTimestamp = 253402300799; // January 1, 10000 CE const result = createTimestampTerms( { - timestampAfterThreshold: maxTimestamp, - timestampBeforeThreshold: 0, + afterThreshold: maxTimestamp, + beforeThreshold: 0, }, { out: 'bytes' }, ); @@ -305,8 +305,8 @@ describe('Timestamp', () => { describe('decodeTimestampTerms', () => { it('decodes a valid range', () => { const original = { - timestampAfterThreshold: 1640995200, - timestampBeforeThreshold: 1672531200, + afterThreshold: 1640995200, + beforeThreshold: 1672531200, }; expect( decodeTimestampTerms(createTimestampTerms(original)), @@ -315,8 +315,8 @@ describe('Timestamp', () => { it('decodes both thresholds zero', () => { const original = { - timestampAfterThreshold: 0, - timestampBeforeThreshold: 0, + afterThreshold: 0, + beforeThreshold: 0, }; expect( decodeTimestampTerms(createTimestampTerms(original)), @@ -325,8 +325,8 @@ describe('Timestamp', () => { it('decodes only after threshold set', () => { const original = { - timestampAfterThreshold: 1640995200, - timestampBeforeThreshold: 0, + afterThreshold: 1640995200, + beforeThreshold: 0, }; expect( decodeTimestampTerms(createTimestampTerms(original)), @@ -335,8 +335,8 @@ describe('Timestamp', () => { it('decodes only before threshold set', () => { const original = { - timestampAfterThreshold: 0, - timestampBeforeThreshold: 1672531200, + afterThreshold: 0, + beforeThreshold: 1672531200, }; expect( decodeTimestampTerms(createTimestampTerms(original)), @@ -345,8 +345,8 @@ describe('Timestamp', () => { it('decodes small positive timestamps', () => { const original = { - timestampAfterThreshold: 1, - timestampBeforeThreshold: 2, + afterThreshold: 1, + beforeThreshold: 2, }; expect( decodeTimestampTerms(createTimestampTerms(original)), @@ -356,8 +356,8 @@ describe('Timestamp', () => { it('decodes maximum allowed before threshold', () => { const maxTimestamp = 253402300799; const original = { - timestampAfterThreshold: 0, - timestampBeforeThreshold: maxTimestamp, + afterThreshold: 0, + beforeThreshold: maxTimestamp, }; expect( decodeTimestampTerms(createTimestampTerms(original)), @@ -366,8 +366,8 @@ describe('Timestamp', () => { it('accepts Uint8Array terms from the encoder', () => { const original = { - timestampAfterThreshold: 1640995200, - timestampBeforeThreshold: 1672531200, + afterThreshold: 1640995200, + beforeThreshold: 1672531200, }; const bytes = createTimestampTerms(original, { out: 'bytes' }); expect(decodeTimestampTerms(bytes)).toStrictEqual(original); 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..2eab96be 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) { + case caveatEnforcers.AllowedCalldataEnforcer: + return { type: 'allowedCalldata', ...decodeAllowedCalldataTerms(terms) }; + case caveatEnforcers.AllowedMethodsEnforcer: + return { type: 'allowedMethods', ...decodeAllowedMethodsTerms(terms) }; + case caveatEnforcers.AllowedTargetsEnforcer: + return { type: 'allowedTargets', ...decodeAllowedTargetsTerms(terms) }; + case caveatEnforcers.ArgsEqualityCheckEnforcer: + return { + type: 'argsEqualityCheck', + ...decodeArgsEqualityCheckTerms(terms), + }; + case caveatEnforcers.BlockNumberEnforcer: + return { type: 'blockNumber', ...decodeBlockNumberTerms(terms) }; + case caveatEnforcers.DeployedEnforcer: + return { type: 'deployed', ...decodeDeployedTerms(terms) }; + case caveatEnforcers.ERC20BalanceChangeEnforcer: + return { + type: 'erc20BalanceChange', + ...decodeERC20BalanceChangeTerms(terms), + }; + case caveatEnforcers.ERC20TransferAmountEnforcer: + return { + type: 'erc20TransferAmount', + ...decodeERC20TransferAmountTerms(terms), + }; + case caveatEnforcers.ERC20StreamingEnforcer: + return { type: 'erc20Streaming', ...decodeERC20StreamingTerms(terms) }; + case caveatEnforcers.ERC721BalanceChangeEnforcer: + return { + type: 'erc721BalanceChange', + ...decodeERC721BalanceChangeTerms(terms), + }; + case caveatEnforcers.ERC721TransferEnforcer: + return { type: 'erc721Transfer', ...decodeERC721TransferTerms(terms) }; + case caveatEnforcers.ERC1155BalanceChangeEnforcer: + return { + type: 'erc1155BalanceChange', + ...decodeERC1155BalanceChangeTerms(terms), + }; + case caveatEnforcers.IdEnforcer: + return { type: 'id', ...decodeIdTerms(terms) }; + case caveatEnforcers.LimitedCallsEnforcer: + return { type: 'limitedCalls', ...decodeLimitedCallsTerms(terms) }; + case caveatEnforcers.NonceEnforcer: + return { type: 'nonce', ...decodeNonceTerms(terms) }; + case caveatEnforcers.TimestampEnforcer: + return { type: 'timestamp', ...decodeTimestampTerms(terms) }; + case caveatEnforcers.ValueLteEnforcer: + return { type: 'valueLte', ...decodeValueLteTerms(terms) }; + case caveatEnforcers.NativeTokenTransferAmountEnforcer: + return { + type: 'nativeTokenTransferAmount', + ...decodeNativeTokenTransferAmountTerms(terms), + }; + case caveatEnforcers.NativeBalanceChangeEnforcer: + return { + type: 'nativeBalanceChange', + ...decodeNativeBalanceChangeTerms(terms), + }; + case caveatEnforcers.NativeTokenStreamingEnforcer: + return { + type: 'nativeTokenStreaming', + ...decodeNativeTokenStreamingTerms(terms), + }; + case caveatEnforcers.NativeTokenPaymentEnforcer: + return { + type: 'nativeTokenPayment', + ...decodeNativeTokenPaymentTerms(terms), + }; + case caveatEnforcers.RedeemerEnforcer: + return { type: 'redeemer', ...decodeRedeemerTerms(terms) }; + case caveatEnforcers.SpecificActionERC20TransferBatchEnforcer: + return { + type: 'specificActionERC20TransferBatch', + ...decodeSpecificActionERC20TransferBatchTerms(terms), + }; + case caveatEnforcers.ERC20PeriodTransferEnforcer: + return { + type: 'erc20PeriodTransfer', + ...decodeERC20TokenPeriodTransferTerms(terms), + }; + case caveatEnforcers.NativeTokenPeriodTransferEnforcer: + return { + type: 'nativeTokenPeriodTransfer', + ...decodeNativeTokenPeriodTransferTerms(terms), + }; + case caveatEnforcers.ExactCalldataBatchEnforcer: + return { + type: 'exactCalldataBatch', + ...decodeExactCalldataBatchTerms(terms), + }; + case caveatEnforcers.ExactCalldataEnforcer: + return { type: 'exactCalldata', ...decodeExactCalldataTerms(terms) }; + case caveatEnforcers.ExactExecutionEnforcer: + return { type: 'exactExecution', ...decodeExactExecutionTerms(terms) }; + case caveatEnforcers.ExactExecutionBatchEnforcer: + return { + type: 'exactExecutionBatch', + ...decodeExactExecutionBatchTerms(terms), + }; + case caveatEnforcers.MultiTokenPeriodEnforcer: + return { + type: 'multiTokenPeriod', + ...decodeMultiTokenPeriodTerms(terms), + }; + case caveatEnforcers.OwnershipTransferEnforcer: + return { + type: 'ownershipTransfer', + ...decodeOwnershipTransferTerms(terms), + }; + default: + throw new Error(`Unknown enforcer address: ${enforcer}`); + } +}; diff --git a/packages/smart-accounts-kit/src/index.ts b/packages/smart-accounts-kit/src/index.ts index 5ac166f5..d028118f 100644 --- a/packages/smart-accounts-kit/src/index.ts +++ b/packages/smart-accounts-kit/src/index.ts @@ -50,7 +50,7 @@ export type { ExecutionStruct, CreateExecutionArgs } from './executions'; export type { Caveats } from './caveatBuilder'; -export { createCaveat } from './caveats'; +export { createCaveat, decodeCaveat } from './caveats'; export { BalanceChangeType } from './caveatBuilder/types'; 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}`); + }); +}); From 3f2af91c683d6f64ce44956718b3ef4f4e0d21d1 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:47:56 +1300 Subject: [PATCH 09/15] Fix tsdocs comments for terms decoders --- .../src/caveats/allowedCalldata.ts | 6 +++--- .../src/caveats/allowedMethods.ts | 6 +++--- .../src/caveats/allowedTargets.ts | 6 +++--- .../src/caveats/argsEqualityCheck.ts | 6 +++--- .../src/caveats/blockNumber.ts | 4 ++-- .../delegation-core/src/caveats/deployed.ts | 6 +++--- .../src/caveats/erc1155BalanceChange.ts | 8 ++++---- .../src/caveats/erc20BalanceChange.ts | 8 ++++---- .../src/caveats/erc20Streaming.ts | 6 +++--- .../src/caveats/erc20TokenPeriodTransfer.ts | 6 +++--- .../src/caveats/erc20TransferAmount.ts | 6 +++--- .../src/caveats/erc721BalanceChange.ts | 8 ++++---- .../src/caveats/erc721Transfer.ts | 6 +++--- .../src/caveats/exactCalldata.ts | 6 +++--- .../src/caveats/exactCalldataBatch.ts | 10 +++++----- .../src/caveats/exactExecution.ts | 6 +++--- .../src/caveats/exactExecutionBatch.ts | 6 +++--- .../src/caveats/multiTokenPeriod.ts | 10 +++++----- .../src/caveats/nativeBalanceChange.ts | 8 ++++---- .../src/caveats/nativeTokenPayment.ts | 6 +++--- packages/delegation-core/src/caveats/nonce.ts | 6 +++--- .../src/caveats/ownershipTransfer.ts | 6 +++--- .../delegation-core/src/caveats/redeemer.ts | 6 +++--- .../specificActionERC20TransferBatch.ts | 20 ++++++++++++------- .../delegation-core/src/caveats/timestamp.ts | 4 ++-- 25 files changed, 91 insertions(+), 85 deletions(-) diff --git a/packages/delegation-core/src/caveats/allowedCalldata.ts b/packages/delegation-core/src/caveats/allowedCalldata.ts index 293da9c3..cb099c2a 100644 --- a/packages/delegation-core/src/caveats/allowedCalldata.ts +++ b/packages/delegation-core/src/caveats/allowedCalldata.ts @@ -103,9 +103,9 @@ export function decodeAllowedCalldataTerms( encodingOptions: EncodingOptions<'bytes'>, ): AllowedCalldataTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index 06f34758..f02d43b8 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -103,9 +103,9 @@ export function decodeAllowedMethodsTerms( encodingOptions: EncodingOptions<'bytes'>, ): AllowedMethodsTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index e3d51c02..2520f0b8 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -87,9 +87,9 @@ export function decodeAllowedTargetsTerms( encodingOptions: EncodingOptions<'bytes'>, ): AllowedTargetsTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/argsEqualityCheck.ts b/packages/delegation-core/src/caveats/argsEqualityCheck.ts index 1c569ae3..3ae764e8 100644 --- a/packages/delegation-core/src/caveats/argsEqualityCheck.ts +++ b/packages/delegation-core/src/caveats/argsEqualityCheck.ts @@ -85,9 +85,9 @@ export function decodeArgsEqualityCheckTerms( encodingOptions: EncodingOptions<'bytes'>, ): ArgsEqualityCheckTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/blockNumber.ts b/packages/delegation-core/src/caveats/blockNumber.ts index 00035a80..508d4914 100644 --- a/packages/delegation-core/src/caveats/blockNumber.ts +++ b/packages/delegation-core/src/caveats/blockNumber.ts @@ -1,9 +1,9 @@ /** * ## BlockNumberEnforcer * - * Restricts redemption to a block number range. + * 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). + * 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'; diff --git a/packages/delegation-core/src/caveats/deployed.ts b/packages/delegation-core/src/caveats/deployed.ts index de708477..5875854b 100644 --- a/packages/delegation-core/src/caveats/deployed.ts +++ b/packages/delegation-core/src/caveats/deployed.ts @@ -108,9 +108,9 @@ export function decodeDeployedTerms( encodingOptions: EncodingOptions<'bytes'>, ): DeployedTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts index a2650438..0c500697 100644 --- a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts @@ -3,7 +3,7 @@ * * Constrains ERC-1155 balance change for a token id and recipient. * - * Terms are encoded as 1-byte change type, 20-byte token and recipient (normalized lowercase), then two 32-byte big-endian uint256 words: token id and 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 token id and 32-byte balance (each big-endian uint256). */ import type { BytesLike } from '@metamask/utils'; @@ -137,9 +137,9 @@ export function decodeERC1155BalanceChangeTerms( encodingOptions: EncodingOptions<'bytes'>, ): ERC1155BalanceChangeTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/erc20BalanceChange.ts b/packages/delegation-core/src/caveats/erc20BalanceChange.ts index b580f2e9..0afe1326 100644 --- a/packages/delegation-core/src/caveats/erc20BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc20BalanceChange.ts @@ -3,7 +3,7 @@ * * Constrains ERC-20 balance change for a recipient relative to a reference balance. * - * Terms are encoded as 1-byte change type, 20-byte token and recipient (each normalized lowercase), then 32-byte big-endian 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'; @@ -127,9 +127,9 @@ export function decodeERC20BalanceChangeTerms( encodingOptions: EncodingOptions<'bytes'>, ): ERC20BalanceChangeTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/erc20Streaming.ts b/packages/delegation-core/src/caveats/erc20Streaming.ts index d371a11a..ceddb8db 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -152,9 +152,9 @@ export function decodeERC20StreamingTerms( encodingOptions: EncodingOptions<'bytes'>, ): ERC20StreamingTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts index 8a49300f..1479f2d6 100644 --- a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -128,9 +128,9 @@ export function decodeERC20TokenPeriodTransferTerms( encodingOptions: EncodingOptions<'bytes'>, ): ERC20TokenPeriodTransferTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/erc20TransferAmount.ts b/packages/delegation-core/src/caveats/erc20TransferAmount.ts index 46485247..3cc930c8 100644 --- a/packages/delegation-core/src/caveats/erc20TransferAmount.ts +++ b/packages/delegation-core/src/caveats/erc20TransferAmount.ts @@ -97,9 +97,9 @@ export function decodeERC20TransferAmountTerms( encodingOptions: EncodingOptions<'bytes'>, ): ERC20TransferAmountTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/erc721BalanceChange.ts b/packages/delegation-core/src/caveats/erc721BalanceChange.ts index 87526223..dbbb1605 100644 --- a/packages/delegation-core/src/caveats/erc721BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc721BalanceChange.ts @@ -3,7 +3,7 @@ * * Constrains ERC-721 balance (id count) change for a recipient. * - * Terms are encoded as 1-byte change type, 20-byte token and recipient (each normalized lowercase), then 32-byte big-endian amount. + * 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'; @@ -127,9 +127,9 @@ export function decodeERC721BalanceChangeTerms( encodingOptions: EncodingOptions<'bytes'>, ): ERC721BalanceChangeTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/erc721Transfer.ts b/packages/delegation-core/src/caveats/erc721Transfer.ts index 21cf71df..4445e539 100644 --- a/packages/delegation-core/src/caveats/erc721Transfer.ts +++ b/packages/delegation-core/src/caveats/erc721Transfer.ts @@ -96,9 +96,9 @@ export function decodeERC721TransferTerms( encodingOptions: EncodingOptions<'bytes'>, ): ERC721TransferTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/exactCalldata.ts b/packages/delegation-core/src/caveats/exactCalldata.ts index a20c0ed3..ba5802ab 100644 --- a/packages/delegation-core/src/caveats/exactCalldata.ts +++ b/packages/delegation-core/src/caveats/exactCalldata.ts @@ -85,9 +85,9 @@ export function decodeExactCalldataTerms( encodingOptions: EncodingOptions<'bytes'>, ): ExactCalldataTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/exactCalldataBatch.ts b/packages/delegation-core/src/caveats/exactCalldataBatch.ts index 8d743870..c3d54944 100644 --- a/packages/delegation-core/src/caveats/exactCalldataBatch.ts +++ b/packages/delegation-core/src/caveats/exactCalldataBatch.ts @@ -1,9 +1,9 @@ /** * ## ExactCalldataBatchEnforcer * - * Requires a batch of executions to match exactly on calldata and metadata. + * 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)[]. + * 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'; @@ -114,9 +114,9 @@ export function decodeExactCalldataBatchTerms( encodingOptions: EncodingOptions<'bytes'>, ): ExactCalldataBatchTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/exactExecution.ts b/packages/delegation-core/src/caveats/exactExecution.ts index 70ca8f58..2ad41ad4 100644 --- a/packages/delegation-core/src/caveats/exactExecution.ts +++ b/packages/delegation-core/src/caveats/exactExecution.ts @@ -111,9 +111,9 @@ export function decodeExactExecutionTerms( encodingOptions: EncodingOptions<'bytes'>, ): ExactExecutionTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/exactExecutionBatch.ts b/packages/delegation-core/src/caveats/exactExecutionBatch.ts index 43c79736..9093c474 100644 --- a/packages/delegation-core/src/caveats/exactExecutionBatch.ts +++ b/packages/delegation-core/src/caveats/exactExecutionBatch.ts @@ -114,9 +114,9 @@ export function decodeExactExecutionBatchTerms( encodingOptions: EncodingOptions<'bytes'>, ): ExactExecutionBatchTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/multiTokenPeriod.ts b/packages/delegation-core/src/caveats/multiTokenPeriod.ts index 922fbf92..ab11b0a6 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -1,9 +1,9 @@ /** * ## MultiTokenPeriodEnforcer * - * Sets independent periodic transfer limits for multiple ERC-20 tokens. + * Sets independent periodic transfer limits for multiple tokens (ERC-20 or native). * - * Terms are encoded by repeating, per token: 20-byte token address then three 32-byte big-endian uint256 words (period amount, period duration, start date). + * 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'; @@ -127,9 +127,9 @@ export function decodeMultiTokenPeriodTerms( encodingOptions: EncodingOptions<'bytes'>, ): MultiTokenPeriodTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/nativeBalanceChange.ts b/packages/delegation-core/src/caveats/nativeBalanceChange.ts index fd9fbe94..34a796ea 100644 --- a/packages/delegation-core/src/caveats/nativeBalanceChange.ts +++ b/packages/delegation-core/src/caveats/nativeBalanceChange.ts @@ -3,7 +3,7 @@ * * Constrains native balance change for a recipient relative to a reference balance. * - * Terms are encoded as 1-byte change type, 20-byte recipient (normalized lowercase), then 32-byte big-endian balance in wei. + * 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'; @@ -111,9 +111,9 @@ export function decodeNativeBalanceChangeTerms( encodingOptions: EncodingOptions<'bytes'>, ): NativeBalanceChangeTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/nativeTokenPayment.ts b/packages/delegation-core/src/caveats/nativeTokenPayment.ts index a3db0b7a..c20edf2f 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPayment.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPayment.ts @@ -97,9 +97,9 @@ export function decodeNativeTokenPaymentTerms( encodingOptions: EncodingOptions<'bytes'>, ): NativeTokenPaymentTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/nonce.ts b/packages/delegation-core/src/caveats/nonce.ts index 55ba1f52..13974190 100644 --- a/packages/delegation-core/src/caveats/nonce.ts +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -110,9 +110,9 @@ export function decodeNonceTerms( encodingOptions: EncodingOptions<'bytes'>, ): NonceTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/ownershipTransfer.ts b/packages/delegation-core/src/caveats/ownershipTransfer.ts index e4cae8da..093e147c 100644 --- a/packages/delegation-core/src/caveats/ownershipTransfer.ts +++ b/packages/delegation-core/src/caveats/ownershipTransfer.ts @@ -81,9 +81,9 @@ export function decodeOwnershipTransferTerms( encodingOptions: EncodingOptions<'bytes'>, ): OwnershipTransferTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index 71c77c3c..cfa12449 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -87,9 +87,9 @@ export function decodeRedeemerTerms( encodingOptions: EncodingOptions<'bytes'>, ): RedeemerTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts index 2206d4e6..7104aa76 100644 --- a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -1,9 +1,15 @@ /** * ## SpecificActionERC20TransferBatchEnforcer * - * Ties an ERC-20 transfer batch to a specific preceding action (target and calldata). + * 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`). * - * Terms are encoded as 20-byte token, 20-byte recipient, 32-byte amount, 20-byte target, then calldata bytes without a separate ABI length prefix. + * - 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'; @@ -38,9 +44,9 @@ export type SpecificActionERC20TransferBatchTerms< recipient: TBytesLike; /** The amount of tokens to transfer. */ amount: bigint; - /** The target address for the first transaction. */ + /** The target address for the first batch execution (`firstTarget` in the enforcer). */ target: TBytesLike; - /** The calldata for the first transaction. */ + /** Calldata for the first execution only, without an ABI length prefix (`firstCalldata` on-chain). */ calldata: TBytesLike; }; @@ -134,9 +140,9 @@ export function decodeSpecificActionERC20TransferBatchTerms( encodingOptions: EncodingOptions<'bytes'>, ): SpecificActionERC20TransferBatchTerms>; /** - * - * @param terms - * @param encodingOptions + * @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, diff --git a/packages/delegation-core/src/caveats/timestamp.ts b/packages/delegation-core/src/caveats/timestamp.ts index 45db3200..2b9e900f 100644 --- a/packages/delegation-core/src/caveats/timestamp.ts +++ b/packages/delegation-core/src/caveats/timestamp.ts @@ -1,9 +1,9 @@ /** * ## TimestampEnforcer * - * Restricts redemption to a unix timestamp window (after / before thresholds). + * 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). + * 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'; From 5cbf69b2d121ae86022c916e0960ccff2ecb7b88 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:27:40 +1300 Subject: [PATCH 10/15] Improvements from code review: - throw if extracting a number from hex that exceeds Number.MAX_SAFE_INTEGER - validate Hex length when decoding allowedTargets terms - update argument type to hex for extractBigInt util --- .../delegation-core/src/caveats/allowedTargets.ts | 4 ++++ packages/delegation-core/src/internalUtils.ts | 14 ++++++++------ .../test/caveats/allowedTargets.test.ts | 9 +++++++++ .../delegation-core/test/internalUtils.test.ts | 7 +++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index 2520f0b8..b2a3ea09 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -101,6 +101,10 @@ export function decodeAllowedTargetsTerms( const addressSize = 20; const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 + if (totalBytes % addressSize !== 0) { + throw new Error('Invalid targets: must be a multiple of 20'); + } + const addressCount = totalBytes / addressSize; const targets: (Hex | Uint8Array)[] = []; diff --git a/packages/delegation-core/src/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts index ab57b21c..98b9854c 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -81,7 +81,7 @@ export const normalizeAddress = ( /** * Normalizes an address into a lowercased hex string. * - * @param value - The address as a hex string or bytes. + * @param value - The address as a hex strin0 or bytes. * @param errorMessage - Error message used for invalid input. * @returns The address as a lowercased 0x-prefixed hex string. * @throws Error if the input is not a 20-byte address. @@ -123,7 +123,7 @@ export const concatHex = (parts: string[]): string => { * @returns The extracted bigint value. */ export const extractBigInt = ( - value: string, + value: Hex, offset: number, size: number, ): bigint => { @@ -147,11 +147,13 @@ export const extractNumber = ( offset: number, size: number, ): number => { - const start = 2 + offset * 2; - const end = start + size * 2; - const slice = value.slice(start, end); + const bigIntValue = extractBigInt(value, offset, size); + + if (bigIntValue > Number.MAX_SAFE_INTEGER) { + throw new Error('Number is too large'); + } - return Number.parseInt(slice, 16); + return Number(bigIntValue); }; /** diff --git a/packages/delegation-core/test/caveats/allowedTargets.test.ts b/packages/delegation-core/test/caveats/allowedTargets.test.ts index 6ddf138b..2e264ac5 100644 --- a/packages/delegation-core/test/caveats/allowedTargets.test.ts +++ b/packages/delegation-core/test/caveats/allowedTargets.test.ts @@ -4,6 +4,7 @@ import { createAllowedTargetsTerms, decodeAllowedTargetsTerms, } from '../../src/caveats/allowedTargets'; +import type { Hex } from 'src/types'; describe('AllowedTargets', () => { describe('createAllowedTargetsTerms', () => { @@ -78,5 +79,13 @@ describe('AllowedTargets', () => { const bytes = createAllowedTargetsTerms(original, { out: 'bytes' }); expect(decodeAllowedTargetsTerms(bytes)).toStrictEqual(original); }); + + it('throws when encoded terms length is not a multiple of 20 bytes', () => { + // 20 bytes + 19 bytes + const thirtyNineBytes = `0x${'00'.repeat(39)}`; + expect(() => decodeAllowedTargetsTerms(thirtyNineBytes)).toThrow( + 'Invalid targets: must be a multiple of 20', + ); + }); }); }); diff --git a/packages/delegation-core/test/internalUtils.test.ts b/packages/delegation-core/test/internalUtils.test.ts index 45cc3c96..8886e62f 100644 --- a/packages/delegation-core/test/internalUtils.test.ts +++ b/packages/delegation-core/test/internalUtils.test.ts @@ -151,6 +151,13 @@ describe('internal utils', () => { 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', () => { From f92ca307530a7c34f2286f80d8b093828d57bb65 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:49:44 +1300 Subject: [PATCH 11/15] Add terms length validation to all terms decoders --- .../src/caveats/allowedCalldata.ts | 6 ++ .../src/caveats/allowedMethods.ts | 15 ++++- .../src/caveats/allowedTargets.ts | 20 ++++--- .../src/caveats/blockNumber.ts | 11 +++- .../delegation-core/src/caveats/deployed.ts | 6 ++ .../src/caveats/erc1155BalanceChange.ts | 6 ++ .../src/caveats/erc20BalanceChange.ts | 6 ++ .../src/caveats/erc20Streaming.ts | 6 ++ .../src/caveats/erc20TokenPeriodTransfer.ts | 6 ++ .../src/caveats/erc20TransferAmount.ts | 6 ++ .../src/caveats/erc721BalanceChange.ts | 6 ++ .../src/caveats/erc721Transfer.ts | 6 ++ .../src/caveats/exactExecution.ts | 6 ++ packages/delegation-core/src/caveats/id.ts | 11 +++- .../src/caveats/limitedCalls.ts | 11 +++- .../src/caveats/multiTokenPeriod.ts | 10 +++- .../src/caveats/nativeBalanceChange.ts | 6 ++ .../src/caveats/nativeTokenPayment.ts | 6 ++ .../src/caveats/nativeTokenPeriodTransfer.ts | 12 +++- .../src/caveats/nativeTokenStreaming.ts | 12 +++- .../src/caveats/nativeTokenTransferAmount.ts | 11 +++- packages/delegation-core/src/caveats/nonce.ts | 6 ++ .../src/caveats/ownershipTransfer.ts | 11 +++- .../delegation-core/src/caveats/redeemer.ts | 16 ++++- .../specificActionERC20TransferBatch.ts | 6 ++ .../delegation-core/src/caveats/timestamp.ts | 11 +++- .../delegation-core/src/caveats/valueLte.ts | 11 +++- packages/delegation-core/src/internalUtils.ts | 58 ++++++++++++++++++- .../test/caveats/allowedCalldata.test.ts | 6 ++ .../test/caveats/allowedMethods.test.ts | 6 ++ .../test/caveats/allowedTargets.test.ts | 1 - .../test/caveats/blockNumber.test.ts | 6 ++ .../test/caveats/decoders.test.ts | 8 +++ .../test/caveats/deployed.test.ts | 6 ++ .../test/caveats/erc1155BalanceChange.test.ts | 8 +++ .../test/caveats/erc20BalanceChange.test.ts | 6 ++ .../test/caveats/erc20Streaming.test.ts | 6 ++ .../test/caveats/erc20TransferAmount.test.ts | 6 ++ .../test/caveats/erc721BalanceChange.test.ts | 6 ++ .../test/caveats/erc721Transfer.test.ts | 6 ++ .../test/caveats/exactExecution.test.ts | 6 ++ .../delegation-core/test/caveats/id.test.ts | 6 ++ .../test/caveats/limitedCalls.test.ts | 6 ++ .../test/caveats/multiTokenPeriod.test.ts | 8 +++ .../test/caveats/nativeBalanceChange.test.ts | 6 ++ .../test/caveats/nativeTokenPayment.test.ts | 6 ++ .../caveats/nativeTokenPeriodTransfer.test.ts | 8 +++ .../test/caveats/nativeTokenStreaming.test.ts | 8 +++ .../caveats/nativeTokenTransferAmount.test.ts | 8 +++ .../test/caveats/nonce.test.ts | 6 ++ .../test/caveats/ownershipTransfer.test.ts | 6 ++ .../test/caveats/redeemer.test.ts | 6 ++ .../specificActionERC20TransferBatch.test.ts | 8 +++ .../test/caveats/timestamp.test.ts | 6 ++ .../test/caveats/valueLte.test.ts | 6 ++ .../test/internalUtils.test.ts | 58 +++++++++++++++++++ 56 files changed, 507 insertions(+), 26 deletions(-) diff --git a/packages/delegation-core/src/caveats/allowedCalldata.ts b/packages/delegation-core/src/caveats/allowedCalldata.ts index cb099c2a..878a01d2 100644 --- a/packages/delegation-core/src/caveats/allowedCalldata.ts +++ b/packages/delegation-core/src/caveats/allowedCalldata.ts @@ -9,6 +9,7 @@ import { bytesToHex, remove0x, type BytesLike } from '@metamask/utils'; import { + assertHexBytesMinLength, extractNumber, extractRemainingHex, toHexString, @@ -114,6 +115,11 @@ export function decodeAllowedCalldataTerms( | 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); diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index f02d43b8..0194b146 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -8,7 +8,12 @@ import { bytesToHex, isHexString, type BytesLike } from '@metamask/utils'; -import { concatHex, extractHex } from '../internalUtils'; +import { + assertHexByteLengthMultipleOf, + concatHex, + extractHex, + getByteLength, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -116,8 +121,12 @@ export function decodeAllowedMethodsTerms( const hexTerms = bytesLikeToHex(terms); const selectorSize = 4; - const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 - const selectorCount = totalBytes / selectorSize; + assertHexByteLengthMultipleOf( + 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++) { diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index b2a3ea09..3c87448d 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -8,7 +8,13 @@ import type { BytesLike } from '@metamask/utils'; -import { concatHex, extractAddress, normalizeAddress } from '../internalUtils'; +import { + assertHexByteLengthMultipleOf, + concatHex, + extractAddress, + getByteLength, + normalizeAddress, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -100,12 +106,12 @@ export function decodeAllowedTargetsTerms( const hexTerms = bytesLikeToHex(terms); const addressSize = 20; - const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 - if (totalBytes % addressSize !== 0) { - throw new Error('Invalid targets: must be a multiple of 20'); - } - - const addressCount = totalBytes / addressSize; + assertHexByteLengthMultipleOf( + 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++) { diff --git a/packages/delegation-core/src/caveats/blockNumber.ts b/packages/delegation-core/src/caveats/blockNumber.ts index 508d4914..e37d5a99 100644 --- a/packages/delegation-core/src/caveats/blockNumber.ts +++ b/packages/delegation-core/src/caveats/blockNumber.ts @@ -8,7 +8,11 @@ import type { BytesLike } from '@metamask/utils'; -import { extractBigInt, toHexString } from '../internalUtils'; +import { + assertHexByteExactLength, + extractBigInt, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -89,6 +93,11 @@ export function createBlockNumberTerms( */ 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 5875854b..a75cb78e 100644 --- a/packages/delegation-core/src/caveats/deployed.ts +++ b/packages/delegation-core/src/caveats/deployed.ts @@ -10,6 +10,7 @@ import type { BytesLike } from '@metamask/utils'; import { remove0x } from '@metamask/utils'; import { + assertHexBytesMinLength, concatHex, extractAddress, extractHex, @@ -119,6 +120,11 @@ export function decodeDeployedTerms( | 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); diff --git a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts index 0c500697..a9323b66 100644 --- a/packages/delegation-core/src/caveats/erc1155BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc1155BalanceChange.ts @@ -9,6 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, extractAddress, extractBigInt, @@ -148,6 +149,11 @@ export function decodeERC1155BalanceChangeTerms( | 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); diff --git a/packages/delegation-core/src/caveats/erc20BalanceChange.ts b/packages/delegation-core/src/caveats/erc20BalanceChange.ts index 0afe1326..fe0d301d 100644 --- a/packages/delegation-core/src/caveats/erc20BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc20BalanceChange.ts @@ -9,6 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, extractAddress, extractBigInt, @@ -138,6 +139,11 @@ export function decodeERC20BalanceChangeTerms( | 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); diff --git a/packages/delegation-core/src/caveats/erc20Streaming.ts b/packages/delegation-core/src/caveats/erc20Streaming.ts index ceddb8db..9b8b9e0f 100644 --- a/packages/delegation-core/src/caveats/erc20Streaming.ts +++ b/packages/delegation-core/src/caveats/erc20Streaming.ts @@ -9,6 +9,7 @@ import { type BytesLike, bytesToHex, isHexString } from '@metamask/utils'; import { + assertHexByteExactLength, extractAddress, extractBigInt, extractNumber, @@ -163,6 +164,11 @@ export function decodeERC20StreamingTerms( | 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); diff --git a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts index 1479f2d6..ee0f1aa1 100644 --- a/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/erc20TokenPeriodTransfer.ts @@ -9,6 +9,7 @@ import { type BytesLike, isHexString, bytesToHex } from '@metamask/utils'; import { + assertHexByteExactLength, extractAddress, extractBigInt, extractNumber, @@ -139,6 +140,11 @@ export function decodeERC20TokenPeriodTransferTerms( | 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); diff --git a/packages/delegation-core/src/caveats/erc20TransferAmount.ts b/packages/delegation-core/src/caveats/erc20TransferAmount.ts index 3cc930c8..76e33e55 100644 --- a/packages/delegation-core/src/caveats/erc20TransferAmount.ts +++ b/packages/delegation-core/src/caveats/erc20TransferAmount.ts @@ -9,6 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, extractAddress, extractBigInt, @@ -108,6 +109,11 @@ export function decodeERC20TransferAmountTerms( | 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); diff --git a/packages/delegation-core/src/caveats/erc721BalanceChange.ts b/packages/delegation-core/src/caveats/erc721BalanceChange.ts index dbbb1605..5f6f90d8 100644 --- a/packages/delegation-core/src/caveats/erc721BalanceChange.ts +++ b/packages/delegation-core/src/caveats/erc721BalanceChange.ts @@ -9,6 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, extractAddress, extractBigInt, @@ -138,6 +139,11 @@ export function decodeERC721BalanceChangeTerms( | 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); diff --git a/packages/delegation-core/src/caveats/erc721Transfer.ts b/packages/delegation-core/src/caveats/erc721Transfer.ts index 4445e539..dfc8fec1 100644 --- a/packages/delegation-core/src/caveats/erc721Transfer.ts +++ b/packages/delegation-core/src/caveats/erc721Transfer.ts @@ -9,6 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, extractAddress, extractBigInt, @@ -107,6 +108,11 @@ export function decodeERC721TransferTerms( | 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); diff --git a/packages/delegation-core/src/caveats/exactExecution.ts b/packages/delegation-core/src/caveats/exactExecution.ts index 2ad41ad4..d852a6b0 100644 --- a/packages/delegation-core/src/caveats/exactExecution.ts +++ b/packages/delegation-core/src/caveats/exactExecution.ts @@ -9,6 +9,7 @@ import { bytesToHex, type BytesLike } from '@metamask/utils'; import { + assertHexBytesMinLength, concatHex, extractAddress, extractBigInt, @@ -122,6 +123,11 @@ export function decodeExactExecutionTerms( | 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); diff --git a/packages/delegation-core/src/caveats/id.ts b/packages/delegation-core/src/caveats/id.ts index 7c4bf356..90a3d630 100644 --- a/packages/delegation-core/src/caveats/id.ts +++ b/packages/delegation-core/src/caveats/id.ts @@ -8,7 +8,11 @@ import type { BytesLike } from '@metamask/utils'; -import { extractBigInt, toHexString } from '../internalUtils'; +import { + assertHexByteExactLength, + extractBigInt, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -91,6 +95,11 @@ export function createIdTerms( */ 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/limitedCalls.ts b/packages/delegation-core/src/caveats/limitedCalls.ts index 1937ce32..122b8409 100644 --- a/packages/delegation-core/src/caveats/limitedCalls.ts +++ b/packages/delegation-core/src/caveats/limitedCalls.ts @@ -8,7 +8,11 @@ import type { BytesLike } from '@metamask/utils'; -import { extractNumber, toHexString } from '../internalUtils'; +import { + assertHexByteExactLength, + extractNumber, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -76,6 +80,11 @@ export function createLimitedCallsTerms( */ 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 ab11b0a6..38f9621e 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -9,10 +9,12 @@ import type { BytesLike } from '@metamask/utils'; import { + assertHexByteLengthMultipleOf, concatHex, extractAddress, extractBigInt, extractNumber, + getByteLength, normalizeAddress, toHexString, } from '../internalUtils'; @@ -140,8 +142,12 @@ export function decodeMultiTokenPeriodTerms( const hexTerms = bytesLikeToHex(terms); const configSize = 116; - const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 - const configCount = totalBytes / configSize; + assertHexByteLengthMultipleOf( + 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++) { diff --git a/packages/delegation-core/src/caveats/nativeBalanceChange.ts b/packages/delegation-core/src/caveats/nativeBalanceChange.ts index 34a796ea..de937abe 100644 --- a/packages/delegation-core/src/caveats/nativeBalanceChange.ts +++ b/packages/delegation-core/src/caveats/nativeBalanceChange.ts @@ -9,6 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, extractAddress, extractBigInt, @@ -122,6 +123,11 @@ export function decodeNativeBalanceChangeTerms( | 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); diff --git a/packages/delegation-core/src/caveats/nativeTokenPayment.ts b/packages/delegation-core/src/caveats/nativeTokenPayment.ts index c20edf2f..32c7d6ed 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPayment.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPayment.ts @@ -9,6 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { + assertHexByteExactLength, concatHex, extractAddress, extractBigInt, @@ -108,6 +109,11 @@ export function decodeNativeTokenPaymentTerms( | 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); diff --git a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts index 56264714..b916567b 100644 --- a/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts +++ b/packages/delegation-core/src/caveats/nativeTokenPeriodTransfer.ts @@ -8,7 +8,12 @@ import type { BytesLike } from '@metamask/utils'; -import { extractBigInt, extractNumber, toHexString } from '../internalUtils'; +import { + assertHexByteExactLength, + extractBigInt, + extractNumber, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -94,6 +99,11 @@ 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); diff --git a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts index 53ead3c5..f96ac89f 100644 --- a/packages/delegation-core/src/caveats/nativeTokenStreaming.ts +++ b/packages/delegation-core/src/caveats/nativeTokenStreaming.ts @@ -8,7 +8,12 @@ import type { BytesLike } from '@metamask/utils'; -import { extractBigInt, extractNumber, toHexString } from '../internalUtils'; +import { + assertHexByteExactLength, + extractBigInt, + extractNumber, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -118,6 +123,11 @@ 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); diff --git a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts index dfbd1c5d..827af4f9 100644 --- a/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts +++ b/packages/delegation-core/src/caveats/nativeTokenTransferAmount.ts @@ -8,7 +8,11 @@ import type { BytesLike } from '@metamask/utils'; -import { extractBigInt, toHexString } from '../internalUtils'; +import { + assertHexByteExactLength, + extractBigInt, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -74,6 +78,11 @@ 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 13974190..1e4da376 100644 --- a/packages/delegation-core/src/caveats/nonce.ts +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -9,6 +9,7 @@ import { isHexString } from '@metamask/utils'; import type { BytesLike } from '@metamask/utils'; +import { assertHexByteExactLength } from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -119,6 +120,11 @@ export function decodeNonceTerms( 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); diff --git a/packages/delegation-core/src/caveats/ownershipTransfer.ts b/packages/delegation-core/src/caveats/ownershipTransfer.ts index 093e147c..baf741b7 100644 --- a/packages/delegation-core/src/caveats/ownershipTransfer.ts +++ b/packages/delegation-core/src/caveats/ownershipTransfer.ts @@ -8,7 +8,11 @@ import type { BytesLike } from '@metamask/utils'; -import { extractAddress, normalizeAddress } from '../internalUtils'; +import { + assertHexByteExactLength, + extractAddress, + normalizeAddress, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -92,6 +96,11 @@ export function decodeOwnershipTransferTerms( | 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), diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index cfa12449..1a81d51a 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -8,7 +8,13 @@ import type { BytesLike } from '@metamask/utils'; -import { concatHex, extractAddress, normalizeAddress } from '../internalUtils'; +import { + assertHexByteLengthMultipleOf, + concatHex, + extractAddress, + getByteLength, + normalizeAddress, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -100,8 +106,12 @@ export function decodeRedeemerTerms( const hexTerms = bytesLikeToHex(terms); const addressSize = 20; - const totalBytes = (hexTerms.length - 2) / 2; // Remove '0x' and divide by 2 - const addressCount = totalBytes / addressSize; + assertHexByteLengthMultipleOf( + 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++) { diff --git a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts index 7104aa76..17b5edaf 100644 --- a/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts +++ b/packages/delegation-core/src/caveats/specificActionERC20TransferBatch.ts @@ -15,6 +15,7 @@ import { bytesToHex, type BytesLike } from '@metamask/utils'; import { + assertHexBytesMinLength, concatHex, extractAddress, extractBigInt, @@ -151,6 +152,11 @@ export function decodeSpecificActionERC20TransferBatchTerms( | 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); diff --git a/packages/delegation-core/src/caveats/timestamp.ts b/packages/delegation-core/src/caveats/timestamp.ts index 2b9e900f..26368086 100644 --- a/packages/delegation-core/src/caveats/timestamp.ts +++ b/packages/delegation-core/src/caveats/timestamp.ts @@ -8,7 +8,11 @@ import type { BytesLike } from '@metamask/utils'; -import { extractNumber, toHexString } from '../internalUtils'; +import { + assertHexByteExactLength, + extractNumber, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -109,6 +113,11 @@ export function createTimestampTerms( */ 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 741587ee..07488f0b 100644 --- a/packages/delegation-core/src/caveats/valueLte.ts +++ b/packages/delegation-core/src/caveats/valueLte.ts @@ -8,7 +8,11 @@ import type { BytesLike } from '@metamask/utils'; -import { extractBigInt, toHexString } from '../internalUtils'; +import { + assertHexByteExactLength, + extractBigInt, + toHexString, +} from '../internalUtils'; import { bytesLikeToHex, defaultOptions, @@ -72,6 +76,11 @@ export function createValueLteTerms( */ 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/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts index 98b9854c..6eaa159d 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -81,7 +81,7 @@ export const normalizeAddress = ( /** * Normalizes an address into a lowercased hex string. * - * @param value - The address as a hex strin0 or bytes. + * @param value - The address as a hex string or bytes. * @param errorMessage - Error message used for invalid input. * @returns The address as a lowercased 0x-prefixed hex string. * @throws Error if the input is not a 20-byte address. @@ -197,3 +197,59 @@ export const extractRemainingHex = (value: Hex, offset: number): Hex => { 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 a multiple of `unitBytes`. + */ +export function assertHexByteLengthMultipleOf( + hexTerms: Hex, + unitBytes: number, + errorMessage: string, +): void { + if (getByteLength(hexTerms) % 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/test/caveats/allowedCalldata.test.ts b/packages/delegation-core/test/caveats/allowedCalldata.test.ts index ba68f124..5e13c7ad 100644 --- a/packages/delegation-core/test/caveats/allowedCalldata.test.ts +++ b/packages/delegation-core/test/caveats/allowedCalldata.test.ts @@ -461,5 +461,11 @@ describe('AllowedCalldata', () => { const bytes = createAllowedCalldataTerms(original, { out: 'bytes' }); expect(decodeAllowedCalldataTerms(bytes)).toStrictEqual(original); }); + + 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', + ); + }); }); }); diff --git a/packages/delegation-core/test/caveats/allowedMethods.test.ts b/packages/delegation-core/test/caveats/allowedMethods.test.ts index e22844d5..1cf926e0 100644 --- a/packages/delegation-core/test/caveats/allowedMethods.test.ts +++ b/packages/delegation-core/test/caveats/allowedMethods.test.ts @@ -90,5 +90,11 @@ describe('AllowedMethods', () => { 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 2e264ac5..93d2258a 100644 --- a/packages/delegation-core/test/caveats/allowedTargets.test.ts +++ b/packages/delegation-core/test/caveats/allowedTargets.test.ts @@ -4,7 +4,6 @@ import { createAllowedTargetsTerms, decodeAllowedTargetsTerms, } from '../../src/caveats/allowedTargets'; -import type { Hex } from 'src/types'; describe('AllowedTargets', () => { describe('createAllowedTargetsTerms', () => { diff --git a/packages/delegation-core/test/caveats/blockNumber.test.ts b/packages/delegation-core/test/caveats/blockNumber.test.ts index 430252b6..340b3499 100644 --- a/packages/delegation-core/test/caveats/blockNumber.test.ts +++ b/packages/delegation-core/test/caveats/blockNumber.test.ts @@ -82,5 +82,11 @@ describe('BlockNumber', () => { beforeThreshold: 2n, }); }); + + 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 index 3db0fc96..9fc7738c 100644 --- a/packages/delegation-core/test/caveats/decoders.test.ts +++ b/packages/delegation-core/test/caveats/decoders.test.ts @@ -383,6 +383,14 @@ describe('Terms Decoders', () => { 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', () => { diff --git a/packages/delegation-core/test/caveats/deployed.test.ts b/packages/delegation-core/test/caveats/deployed.test.ts index 5929a04b..85ee74ec 100644 --- a/packages/delegation-core/test/caveats/deployed.test.ts +++ b/packages/delegation-core/test/caveats/deployed.test.ts @@ -84,5 +84,11 @@ describe('Deployed', () => { const decoded = decodeDeployedTerms(bytes); expect(decoded.bytecode).toBe(bytecode); }); + + 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 d32d5bee..d5edc4c9 100644 --- a/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc1155BalanceChange.test.ts @@ -108,5 +108,13 @@ describe('ERC1155BalanceChange', () => { expect(decoded.tokenId).toBe(1n); expect(decoded.balance).toBe(1n); }); + + 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 750bb9b1..88331303 100644 --- a/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc20BalanceChange.test.ts @@ -116,5 +116,11 @@ describe('ERC20BalanceChange', () => { const decoded = decodeERC20BalanceChangeTerms(bytes); expect(decoded.balance).toBe(1n); }); + + 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 a060975e..36cc91ad 100644 --- a/packages/delegation-core/test/caveats/erc20Streaming.test.ts +++ b/packages/delegation-core/test/caveats/erc20Streaming.test.ts @@ -972,5 +972,11 @@ describe('ERC20Streaming', () => { 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 220858b4..43beca7a 100644 --- a/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts +++ b/packages/delegation-core/test/caveats/erc20TransferAmount.test.ts @@ -68,5 +68,11 @@ describe('ERC20TransferAmount', () => { const bytes = createERC20TransferAmountTerms(original, { out: 'bytes' }); expect(decodeERC20TransferAmountTerms(bytes)).toStrictEqual(original); }); + + 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 8a39d2bb..724bc822 100644 --- a/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/erc721BalanceChange.test.ts @@ -115,5 +115,11 @@ describe('ERC721BalanceChange', () => { const bytes = createERC721BalanceChangeTerms(original, { out: 'bytes' }); expect(decodeERC721BalanceChangeTerms(bytes).amount).toBe(2n); }); + + 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 baf634cb..f02e6522 100644 --- a/packages/delegation-core/test/caveats/erc721Transfer.test.ts +++ b/packages/delegation-core/test/caveats/erc721Transfer.test.ts @@ -73,5 +73,11 @@ describe('ERC721Transfer', () => { const bytes = createERC721TransferTerms(original, { out: 'bytes' }); expect(decodeERC721TransferTerms(bytes)).toStrictEqual(original); }); + + 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/exactExecution.test.ts b/packages/delegation-core/test/caveats/exactExecution.test.ts index f2b84ae8..e7ce0b08 100644 --- a/packages/delegation-core/test/caveats/exactExecution.test.ts +++ b/packages/delegation-core/test/caveats/exactExecution.test.ts @@ -119,5 +119,11 @@ describe('ExactExecution', () => { const bytes = createExactExecutionTerms(original, { out: 'bytes' }); expect(decodeExactExecutionTerms(bytes)).toStrictEqual(original); }); + + 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/id.test.ts b/packages/delegation-core/test/caveats/id.test.ts index fb919503..f8364107 100644 --- a/packages/delegation-core/test/caveats/id.test.ts +++ b/packages/delegation-core/test/caveats/id.test.ts @@ -76,5 +76,11 @@ describe('Id', () => { const bytes = createIdTerms({ id: 42n }, { out: 'bytes' }); expect(decodeIdTerms(bytes)).toStrictEqual({ id: 42n }); }); + + 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 691b8dd5..19bafc66 100644 --- a/packages/delegation-core/test/caveats/limitedCalls.test.ts +++ b/packages/delegation-core/test/caveats/limitedCalls.test.ts @@ -54,5 +54,11 @@ describe('LimitedCalls', () => { const bytes = createLimitedCallsTerms({ limit: 3 }, { out: 'bytes' }); expect(decodeLimitedCallsTerms(bytes)).toStrictEqual({ limit: 3 }); }); + + 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 ac711d98..a344fbb6 100644 --- a/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts +++ b/packages/delegation-core/test/caveats/multiTokenPeriod.test.ts @@ -172,5 +172,13 @@ describe('MultiTokenPeriod', () => { const bytes = createMultiTokenPeriodTerms(original, { out: 'bytes' }); expect(decodeMultiTokenPeriodTerms(bytes)).toStrictEqual(original); }); + + 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 83e6527f..e6d46f81 100644 --- a/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts +++ b/packages/delegation-core/test/caveats/nativeBalanceChange.test.ts @@ -113,5 +113,11 @@ describe('NativeBalanceChange', () => { expect(decoded.balance).toBe(2n); expect(decoded.changeType).toBe(BalanceChangeType.Decrease); }); + + 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 fe2b15e9..afe68b43 100644 --- a/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenPayment.test.ts @@ -74,5 +74,11 @@ describe('NativeTokenPayment', () => { ); 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 fbb13036..08f74064 100644 --- a/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenPeriodTransfer.test.ts @@ -370,5 +370,13 @@ describe('NativeTokenPeriodTransfer', () => { original, ); }); + + 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 1beddc28..0308c02b 100644 --- a/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenStreaming.test.ts @@ -674,5 +674,13 @@ describe('NativeTokenStreaming', () => { 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 3009c529..30568404 100644 --- a/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts +++ b/packages/delegation-core/test/caveats/nativeTokenTransferAmount.test.ts @@ -66,5 +66,13 @@ describe('NativeTokenTransferAmount', () => { maxAmount: 1n, }); }); + + 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 b1ac682a..839408e3 100644 --- a/packages/delegation-core/test/caveats/nonce.test.ts +++ b/packages/delegation-core/test/caveats/nonce.test.ts @@ -487,5 +487,11 @@ describe('Nonce', () => { '0x000000000000000000000000000000000000000000000000000000000000abcd', ); }); + + 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 9f177802..bbcd7c5e 100644 --- a/packages/delegation-core/test/caveats/ownershipTransfer.test.ts +++ b/packages/delegation-core/test/caveats/ownershipTransfer.test.ts @@ -53,5 +53,11 @@ describe('OwnershipTransfer', () => { contractAddress, }); }); + + 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 6ea64226..4b8efc00 100644 --- a/packages/delegation-core/test/caveats/redeemer.test.ts +++ b/packages/delegation-core/test/caveats/redeemer.test.ts @@ -74,5 +74,11 @@ describe('Redeemer', () => { const bytes = createRedeemerTerms(original, { out: 'bytes' }); expect(decodeRedeemerTerms(bytes)).toStrictEqual(original); }); + + 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 e4f1e371..a619a8e2 100644 --- a/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts +++ b/packages/delegation-core/test/caveats/specificActionERC20TransferBatch.test.ts @@ -120,5 +120,13 @@ describe('SpecificActionERC20TransferBatch', () => { original, ); }); + + 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 89b0bc43..e1f69490 100644 --- a/packages/delegation-core/test/caveats/timestamp.test.ts +++ b/packages/delegation-core/test/caveats/timestamp.test.ts @@ -372,5 +372,11 @@ describe('Timestamp', () => { const bytes = createTimestampTerms(original, { out: 'bytes' }); expect(decodeTimestampTerms(bytes)).toStrictEqual(original); }); + + 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 7b8b1c08..7981e313 100644 --- a/packages/delegation-core/test/caveats/valueLte.test.ts +++ b/packages/delegation-core/test/caveats/valueLte.test.ts @@ -187,5 +187,11 @@ describe('ValueLte', () => { const bytes = createValueLteTerms({ maxValue: 1000n }, { out: 'bytes' }); expect(decodeValueLteTerms(bytes)).toStrictEqual({ maxValue: 1000n }); }); + + 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 8886e62f..8f2c45ac 100644 --- a/packages/delegation-core/test/internalUtils.test.ts +++ b/packages/delegation-core/test/internalUtils.test.ts @@ -1,12 +1,16 @@ import { describe, it, expect } from 'vitest'; import { + assertHexBytesMinLength, + assertHexByteExactLength, + assertHexByteLengthMultipleOf, concatHex, extractAddress, extractBigInt, extractHex, extractNumber, extractRemainingHex, + getByteLength, normalizeAddress, normalizeAddressLowercase, normalizeHex, @@ -15,6 +19,60 @@ import { 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('assertHexByteLengthMultipleOf', () => { + it('does not throw when length is a multiple', () => { + expect(() => + assertHexByteLengthMultipleOf(`0x${'00'.repeat(40)}`, 20, 'err'), + ).not.toThrow(); + }); + + it('throws when length is not a multiple', () => { + expect(() => + assertHexByteLengthMultipleOf(`0x${'00'.repeat(19)}`, 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'; From 82a1e71c984ba3a5967fe0b9c8312a54dad58207 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:05:36 +1300 Subject: [PATCH 12/15] Fix typing of test data --- packages/delegation-core/test/caveats/allowedTargets.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/delegation-core/test/caveats/allowedTargets.test.ts b/packages/delegation-core/test/caveats/allowedTargets.test.ts index 93d2258a..87e5874c 100644 --- a/packages/delegation-core/test/caveats/allowedTargets.test.ts +++ b/packages/delegation-core/test/caveats/allowedTargets.test.ts @@ -81,7 +81,7 @@ describe('AllowedTargets', () => { it('throws when encoded terms length is not a multiple of 20 bytes', () => { // 20 bytes + 19 bytes - const thirtyNineBytes = `0x${'00'.repeat(39)}`; + const thirtyNineBytes = `0x${'00'.repeat(39)}` as const; expect(() => decodeAllowedTargetsTerms(thirtyNineBytes)).toThrow( 'Invalid targets: must be a multiple of 20', ); From 7c390d5a6c14ea8163c080f1bc0a1574b41d7eb8 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:49:19 +1300 Subject: [PATCH 13/15] Ensure that enforcer matching is case insensitive --- packages/smart-accounts-kit/src/caveats.ts | 64 +++++++++++----------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/smart-accounts-kit/src/caveats.ts b/packages/smart-accounts-kit/src/caveats.ts index 2eab96be..2d64ba4d 100644 --- a/packages/smart-accounts-kit/src/caveats.ts +++ b/packages/smart-accounts-kit/src/caveats.ts @@ -101,113 +101,113 @@ export const decodeCaveat = ({ caveat: Caveat; environment: SmartAccountsEnvironment; }): CoreCaveatConfiguration => { - switch (enforcer) { - case caveatEnforcers.AllowedCalldataEnforcer: + switch (enforcer.toLowerCase()) { + case caveatEnforcers.AllowedCalldataEnforcer?.toLowerCase(): return { type: 'allowedCalldata', ...decodeAllowedCalldataTerms(terms) }; - case caveatEnforcers.AllowedMethodsEnforcer: + case caveatEnforcers.AllowedMethodsEnforcer?.toLowerCase(): return { type: 'allowedMethods', ...decodeAllowedMethodsTerms(terms) }; - case caveatEnforcers.AllowedTargetsEnforcer: + case caveatEnforcers.AllowedTargetsEnforcer?.toLowerCase(): return { type: 'allowedTargets', ...decodeAllowedTargetsTerms(terms) }; - case caveatEnforcers.ArgsEqualityCheckEnforcer: + case caveatEnforcers.ArgsEqualityCheckEnforcer?.toLowerCase(): return { type: 'argsEqualityCheck', ...decodeArgsEqualityCheckTerms(terms), }; - case caveatEnforcers.BlockNumberEnforcer: + case caveatEnforcers.BlockNumberEnforcer?.toLowerCase(): return { type: 'blockNumber', ...decodeBlockNumberTerms(terms) }; - case caveatEnforcers.DeployedEnforcer: + case caveatEnforcers.DeployedEnforcer?.toLowerCase(): return { type: 'deployed', ...decodeDeployedTerms(terms) }; - case caveatEnforcers.ERC20BalanceChangeEnforcer: + case caveatEnforcers.ERC20BalanceChangeEnforcer?.toLowerCase(): return { type: 'erc20BalanceChange', ...decodeERC20BalanceChangeTerms(terms), }; - case caveatEnforcers.ERC20TransferAmountEnforcer: + case caveatEnforcers.ERC20TransferAmountEnforcer?.toLowerCase(): return { type: 'erc20TransferAmount', ...decodeERC20TransferAmountTerms(terms), }; - case caveatEnforcers.ERC20StreamingEnforcer: + case caveatEnforcers.ERC20StreamingEnforcer?.toLowerCase(): return { type: 'erc20Streaming', ...decodeERC20StreamingTerms(terms) }; - case caveatEnforcers.ERC721BalanceChangeEnforcer: + case caveatEnforcers.ERC721BalanceChangeEnforcer?.toLowerCase(): return { type: 'erc721BalanceChange', ...decodeERC721BalanceChangeTerms(terms), }; - case caveatEnforcers.ERC721TransferEnforcer: + case caveatEnforcers.ERC721TransferEnforcer?.toLowerCase(): return { type: 'erc721Transfer', ...decodeERC721TransferTerms(terms) }; - case caveatEnforcers.ERC1155BalanceChangeEnforcer: + case caveatEnforcers.ERC1155BalanceChangeEnforcer?.toLowerCase(): return { type: 'erc1155BalanceChange', ...decodeERC1155BalanceChangeTerms(terms), }; - case caveatEnforcers.IdEnforcer: + case caveatEnforcers.IdEnforcer?.toLowerCase(): return { type: 'id', ...decodeIdTerms(terms) }; - case caveatEnforcers.LimitedCallsEnforcer: + case caveatEnforcers.LimitedCallsEnforcer?.toLowerCase(): return { type: 'limitedCalls', ...decodeLimitedCallsTerms(terms) }; - case caveatEnforcers.NonceEnforcer: + case caveatEnforcers.NonceEnforcer?.toLowerCase(): return { type: 'nonce', ...decodeNonceTerms(terms) }; - case caveatEnforcers.TimestampEnforcer: + case caveatEnforcers.TimestampEnforcer?.toLowerCase(): return { type: 'timestamp', ...decodeTimestampTerms(terms) }; - case caveatEnforcers.ValueLteEnforcer: + case caveatEnforcers.ValueLteEnforcer?.toLowerCase(): return { type: 'valueLte', ...decodeValueLteTerms(terms) }; - case caveatEnforcers.NativeTokenTransferAmountEnforcer: + case caveatEnforcers.NativeTokenTransferAmountEnforcer?.toLowerCase(): return { type: 'nativeTokenTransferAmount', ...decodeNativeTokenTransferAmountTerms(terms), }; - case caveatEnforcers.NativeBalanceChangeEnforcer: + case caveatEnforcers.NativeBalanceChangeEnforcer?.toLowerCase(): return { type: 'nativeBalanceChange', ...decodeNativeBalanceChangeTerms(terms), }; - case caveatEnforcers.NativeTokenStreamingEnforcer: + case caveatEnforcers.NativeTokenStreamingEnforcer?.toLowerCase(): return { type: 'nativeTokenStreaming', ...decodeNativeTokenStreamingTerms(terms), }; - case caveatEnforcers.NativeTokenPaymentEnforcer: + case caveatEnforcers.NativeTokenPaymentEnforcer?.toLowerCase(): return { type: 'nativeTokenPayment', ...decodeNativeTokenPaymentTerms(terms), }; - case caveatEnforcers.RedeemerEnforcer: + case caveatEnforcers.RedeemerEnforcer?.toLowerCase(): return { type: 'redeemer', ...decodeRedeemerTerms(terms) }; - case caveatEnforcers.SpecificActionERC20TransferBatchEnforcer: + case caveatEnforcers.SpecificActionERC20TransferBatchEnforcer?.toLowerCase(): return { type: 'specificActionERC20TransferBatch', ...decodeSpecificActionERC20TransferBatchTerms(terms), }; - case caveatEnforcers.ERC20PeriodTransferEnforcer: + case caveatEnforcers.ERC20PeriodTransferEnforcer?.toLowerCase(): return { type: 'erc20PeriodTransfer', ...decodeERC20TokenPeriodTransferTerms(terms), }; - case caveatEnforcers.NativeTokenPeriodTransferEnforcer: + case caveatEnforcers.NativeTokenPeriodTransferEnforcer?.toLowerCase(): return { type: 'nativeTokenPeriodTransfer', ...decodeNativeTokenPeriodTransferTerms(terms), }; - case caveatEnforcers.ExactCalldataBatchEnforcer: + case caveatEnforcers.ExactCalldataBatchEnforcer?.toLowerCase(): return { type: 'exactCalldataBatch', ...decodeExactCalldataBatchTerms(terms), }; - case caveatEnforcers.ExactCalldataEnforcer: + case caveatEnforcers.ExactCalldataEnforcer?.toLowerCase(): return { type: 'exactCalldata', ...decodeExactCalldataTerms(terms) }; - case caveatEnforcers.ExactExecutionEnforcer: + case caveatEnforcers.ExactExecutionEnforcer?.toLowerCase(): return { type: 'exactExecution', ...decodeExactExecutionTerms(terms) }; - case caveatEnforcers.ExactExecutionBatchEnforcer: + case caveatEnforcers.ExactExecutionBatchEnforcer?.toLowerCase(): return { type: 'exactExecutionBatch', ...decodeExactExecutionBatchTerms(terms), }; - case caveatEnforcers.MultiTokenPeriodEnforcer: + case caveatEnforcers.MultiTokenPeriodEnforcer?.toLowerCase(): return { type: 'multiTokenPeriod', ...decodeMultiTokenPeriodTerms(terms), }; - case caveatEnforcers.OwnershipTransferEnforcer: + case caveatEnforcers.OwnershipTransferEnforcer?.toLowerCase(): return { type: 'ownershipTransfer', ...decodeOwnershipTransferTerms(terms), From 97f603862b9349193893455ee9890396d9a9ab36 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:56:34 +1300 Subject: [PATCH 14/15] Rename assertHexByteLengthMultipleOf to assertHexByteLengthAtLeastOneMultipleOf and ensure that bytelength is not 0 --- .../src/caveats/allowedMethods.ts | 4 ++-- .../src/caveats/allowedTargets.ts | 4 ++-- .../src/caveats/multiTokenPeriod.ts | 4 ++-- .../delegation-core/src/caveats/redeemer.ts | 4 ++-- packages/delegation-core/src/internalUtils.ts | 7 +++--- .../test/internalUtils.test.ts | 22 +++++++++++++++---- 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/delegation-core/src/caveats/allowedMethods.ts b/packages/delegation-core/src/caveats/allowedMethods.ts index 0194b146..703b067a 100644 --- a/packages/delegation-core/src/caveats/allowedMethods.ts +++ b/packages/delegation-core/src/caveats/allowedMethods.ts @@ -9,7 +9,7 @@ import { bytesToHex, isHexString, type BytesLike } from '@metamask/utils'; import { - assertHexByteLengthMultipleOf, + assertHexByteLengthAtLeastOneMultipleOf, concatHex, extractHex, getByteLength, @@ -121,7 +121,7 @@ export function decodeAllowedMethodsTerms( const hexTerms = bytesLikeToHex(terms); const selectorSize = 4; - assertHexByteLengthMultipleOf( + assertHexByteLengthAtLeastOneMultipleOf( hexTerms, selectorSize, 'Invalid selectors: must be a multiple of 4', diff --git a/packages/delegation-core/src/caveats/allowedTargets.ts b/packages/delegation-core/src/caveats/allowedTargets.ts index 3c87448d..0e3be495 100644 --- a/packages/delegation-core/src/caveats/allowedTargets.ts +++ b/packages/delegation-core/src/caveats/allowedTargets.ts @@ -9,7 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { - assertHexByteLengthMultipleOf, + assertHexByteLengthAtLeastOneMultipleOf, concatHex, extractAddress, getByteLength, @@ -106,7 +106,7 @@ export function decodeAllowedTargetsTerms( const hexTerms = bytesLikeToHex(terms); const addressSize = 20; - assertHexByteLengthMultipleOf( + assertHexByteLengthAtLeastOneMultipleOf( hexTerms, addressSize, 'Invalid targets: must be a multiple of 20', diff --git a/packages/delegation-core/src/caveats/multiTokenPeriod.ts b/packages/delegation-core/src/caveats/multiTokenPeriod.ts index 38f9621e..44486cc2 100644 --- a/packages/delegation-core/src/caveats/multiTokenPeriod.ts +++ b/packages/delegation-core/src/caveats/multiTokenPeriod.ts @@ -9,7 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { - assertHexByteLengthMultipleOf, + assertHexByteLengthAtLeastOneMultipleOf, concatHex, extractAddress, extractBigInt, @@ -142,7 +142,7 @@ export function decodeMultiTokenPeriodTerms( const hexTerms = bytesLikeToHex(terms); const configSize = 116; - assertHexByteLengthMultipleOf( + assertHexByteLengthAtLeastOneMultipleOf( hexTerms, configSize, 'Invalid MultiTokenPeriod terms: must be a multiple of 116 bytes', diff --git a/packages/delegation-core/src/caveats/redeemer.ts b/packages/delegation-core/src/caveats/redeemer.ts index 1a81d51a..81447e47 100644 --- a/packages/delegation-core/src/caveats/redeemer.ts +++ b/packages/delegation-core/src/caveats/redeemer.ts @@ -9,7 +9,7 @@ import type { BytesLike } from '@metamask/utils'; import { - assertHexByteLengthMultipleOf, + assertHexByteLengthAtLeastOneMultipleOf, concatHex, extractAddress, getByteLength, @@ -106,7 +106,7 @@ export function decodeRedeemerTerms( const hexTerms = bytesLikeToHex(terms); const addressSize = 20; - assertHexByteLengthMultipleOf( + assertHexByteLengthAtLeastOneMultipleOf( hexTerms, addressSize, 'Invalid redeemers: must be a multiple of 20', diff --git a/packages/delegation-core/src/internalUtils.ts b/packages/delegation-core/src/internalUtils.ts index 6eaa159d..5ec4295b 100644 --- a/packages/delegation-core/src/internalUtils.ts +++ b/packages/delegation-core/src/internalUtils.ts @@ -226,14 +226,15 @@ export function assertHexByteExactLength( * @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 a multiple of `unitBytes`. + * @throws Error if the payload length is not (at least one) a multiple of `unitBytes`. */ -export function assertHexByteLengthMultipleOf( +export function assertHexByteLengthAtLeastOneMultipleOf( hexTerms: Hex, unitBytes: number, errorMessage: string, ): void { - if (getByteLength(hexTerms) % unitBytes !== 0) { + const byteLength = getByteLength(hexTerms); + if (byteLength === 0 || byteLength % unitBytes !== 0) { throw new Error(errorMessage); } } diff --git a/packages/delegation-core/test/internalUtils.test.ts b/packages/delegation-core/test/internalUtils.test.ts index 8f2c45ac..3d576d68 100644 --- a/packages/delegation-core/test/internalUtils.test.ts +++ b/packages/delegation-core/test/internalUtils.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest'; import { assertHexBytesMinLength, assertHexByteExactLength, - assertHexByteLengthMultipleOf, + assertHexByteLengthAtLeastOneMultipleOf, concatHex, extractAddress, extractBigInt, @@ -42,16 +42,30 @@ describe('internal utils', () => { }); }); - describe('assertHexByteLengthMultipleOf', () => { + describe('assertHexByteLengthAtLeastOneMultipleOf', () => { it('does not throw when length is a multiple', () => { expect(() => - assertHexByteLengthMultipleOf(`0x${'00'.repeat(40)}`, 20, 'err'), + assertHexByteLengthAtLeastOneMultipleOf( + `0x${'00'.repeat(40)}`, + 20, + 'err', + ), ).not.toThrow(); }); it('throws when length is not a multiple', () => { expect(() => - assertHexByteLengthMultipleOf(`0x${'00'.repeat(19)}`, 20, 'bad'), + assertHexByteLengthAtLeastOneMultipleOf( + `0x${'00'.repeat(19)}`, + 20, + 'bad', + ), + ).toThrow('bad'); + }); + + it('throws when length is 0', () => { + expect(() => + assertHexByteLengthAtLeastOneMultipleOf(`0x`, 20, 'bad'), ).toThrow('bad'); }); }); From 7d0bd1d6d046d9cbe63d70f1a05258ae2e2a0131 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:10:09 +1300 Subject: [PATCH 15/15] Move decodeCaveat util from @metamask/smart-accounts-kit to @metamask/smart-accounts-kit/utils --- packages/smart-accounts-kit/src/index.ts | 2 +- packages/smart-accounts-kit/src/utils/index.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/smart-accounts-kit/src/index.ts b/packages/smart-accounts-kit/src/index.ts index d028118f..5ac166f5 100644 --- a/packages/smart-accounts-kit/src/index.ts +++ b/packages/smart-accounts-kit/src/index.ts @@ -50,7 +50,7 @@ export type { ExecutionStruct, CreateExecutionArgs } from './executions'; export type { Caveats } from './caveatBuilder'; -export { createCaveat, decodeCaveat } from './caveats'; +export { createCaveat } from './caveats'; export { BalanceChangeType } from './caveatBuilder/types'; 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 {