From ffb21fc85b4a77263d874d9aa6b1c96c31764fdf Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 30 Jul 2025 14:54:35 -0600 Subject: [PATCH 1/2] feat: added nonce enforcer as caveat in delegation core --- packages/delegation-core/src/caveats/index.ts | 1 + packages/delegation-core/src/caveats/nonce.ts | 75 +++++++ packages/delegation-core/src/index.ts | 1 + .../test/caveats/nonce.test.ts | 204 ++++++++++++++++++ .../src/caveatBuilder/nonceBuilder.ts | 20 +- 5 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 packages/delegation-core/src/caveats/nonce.ts create mode 100644 packages/delegation-core/test/caveats/nonce.test.ts diff --git a/packages/delegation-core/src/caveats/index.ts b/packages/delegation-core/src/caveats/index.ts index b6dd3487..ec47ba4e 100644 --- a/packages/delegation-core/src/caveats/index.ts +++ b/packages/delegation-core/src/caveats/index.ts @@ -5,3 +5,4 @@ export { createExactCalldataTerms } from './exactCalldata'; export { createNativeTokenStreamingTerms } from './nativeTokenStreaming'; export { createERC20StreamingTerms } from './erc20Streaming'; export { createERC20TokenPeriodTransferTerms } from './erc20TokenPeriodTransfer'; +export { createNonceTerms } from './nonce'; diff --git a/packages/delegation-core/src/caveats/nonce.ts b/packages/delegation-core/src/caveats/nonce.ts new file mode 100644 index 00000000..4c3b89d7 --- /dev/null +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -0,0 +1,75 @@ +import { isHexString } from '@metamask/utils'; +import type { BytesLike } from '@metamask/utils'; + +import { + defaultOptions, + prepareResult, + type EncodingOptions, + type ResultValue, +} from '../returns'; +import type { Hex } from '../types'; + +// char length of 32 byte hex string (including 0x prefix) +const MAX_NONCE_STRING_LENGTH = 66; + +/** + * Terms for configuring a Nonce caveat. + */ +export type NonceTerms = { + /** The nonce as a hex string to allow bulk revocation of delegations. */ + nonce: BytesLike; +}; + +/** + * Creates terms for a Nonce caveat that uses a nonce value for bulk revocation of delegations. + * + * @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. + * @throws Error if the nonce is invalid. + */ +export function createNonceTerms( + terms: NonceTerms, + encodingOptions?: EncodingOptions<'hex'>, +): Hex; +export function createNonceTerms( + terms: NonceTerms, + encodingOptions: EncodingOptions<'bytes'>, +): Uint8Array; +/** + * Creates terms for a Nonce caveat that uses a nonce value for bulk revocation of delegations. + * + * @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. + * @throws Error if the nonce is invalid. + */ +export function createNonceTerms( + terms: NonceTerms, + encodingOptions: EncodingOptions = defaultOptions, +): Hex | Uint8Array { + const { nonce } = terms; + + if (!nonce || nonce === '0x') { + throw new Error('Invalid nonce: must be a non-empty hex string'); + } + + if (typeof nonce !== 'string' || !nonce.startsWith('0x')) { + throw new Error('Invalid nonce: must be a valid hex string'); + } + + if (!isHexString(nonce)) { + throw new Error('Invalid nonce: must be a valid hex string'); + } + + if (nonce.length > MAX_NONCE_STRING_LENGTH) { + throw new Error('Invalid nonce: must be 32 bytes or less in length'); + } + + // Remove '0x' prefix for padding, then add it back + const nonceWithoutPrefix = nonce.slice(2); + const paddedNonce = nonceWithoutPrefix.padStart(64, '0'); // 64 hex chars = 32 bytes + const hexValue = `0x${paddedNonce}`; + + return prepareResult(hexValue, encodingOptions); +} diff --git a/packages/delegation-core/src/index.ts b/packages/delegation-core/src/index.ts index 7aec9042..56e9fcf7 100644 --- a/packages/delegation-core/src/index.ts +++ b/packages/delegation-core/src/index.ts @@ -12,6 +12,7 @@ export { createNativeTokenStreamingTerms, createERC20StreamingTerms, createERC20TokenPeriodTransferTerms, + createNonceTerms, } from './caveats'; export { diff --git a/packages/delegation-core/test/caveats/nonce.test.ts b/packages/delegation-core/test/caveats/nonce.test.ts new file mode 100644 index 00000000..793027ad --- /dev/null +++ b/packages/delegation-core/test/caveats/nonce.test.ts @@ -0,0 +1,204 @@ +import { describe, it, expect } from 'vitest'; + +import { createNonceTerms } from '../../src/caveats/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 be a non-empty hex string', + ); + }); + + it('throws an error for undefined nonce', () => { + expect(() => createNonceTerms({ nonce: undefined as any })).toThrow( + 'Invalid nonce: must be a non-empty hex string', + ); + }); + + it('throws an error for null nonce', () => { + expect(() => createNonceTerms({ nonce: null as any })).toThrow( + 'Invalid nonce: must be a non-empty hex string', + ); + }); + + it('throws an error for nonce without 0x prefix', () => { + const nonce = '1234567890abcdef' as any; + + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: must be a valid hex string', + ); + }); + + it('throws an error for invalid hex characters', () => { + const nonce = '0x1234567890abcdefg' as any; + + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: must be a valid hex string', + ); + }); + + it('throws an error for non-string nonce', () => { + const nonce = 123456 as any; + + expect(() => createNonceTerms({ nonce })).toThrow( + 'Invalid nonce: must be a valid hex string', + ); + }); + + 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 bytes return type + describe('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); + }); + }); +}); diff --git a/packages/delegation-toolkit/src/caveatBuilder/nonceBuilder.ts b/packages/delegation-toolkit/src/caveatBuilder/nonceBuilder.ts index 2c385108..f3759c3e 100644 --- a/packages/delegation-toolkit/src/caveatBuilder/nonceBuilder.ts +++ b/packages/delegation-toolkit/src/caveatBuilder/nonceBuilder.ts @@ -1,12 +1,10 @@ -import { type Hex, isHex, pad } from 'viem'; +import { createNonceTerms } from '@metamask/delegation-core'; +import { type Hex } from 'viem'; import type { DeleGatorEnvironment, Caveat } from '../types'; export const nonce = 'nonce'; -// char length of 32 byte hex string -const MAX_NONCE_STRING_LENGTH = 66; - export type NonceBuilderConfig = { /** * A nonce as a hex string to allow bulk revocation of delegations. @@ -28,19 +26,7 @@ export const nonceBuilder = ( ): Caveat => { const { nonce: nonceValue } = config; - if (!nonceValue || nonceValue === '0x') { - throw new Error('Invalid nonce: must be a non-empty hex string'); - } - - if (!isHex(nonceValue)) { - throw new Error('Invalid nonce: must be a valid hex string'); - } - - if (nonceValue.length > MAX_NONCE_STRING_LENGTH) { - throw new Error('Invalid nonce: must be 32 bytes or less in length'); - } - - const terms = pad(nonceValue, { size: 32 }); + const terms = createNonceTerms({ nonce: nonceValue }); const { caveatEnforcers: { NonceEnforcer }, From bdfa550a31e9a0e324989d95ebbbbb2d3dd960f8 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Wed, 30 Jul 2025 20:06:50 -0600 Subject: [PATCH 2/2] chore: supporting more types for the nonce caveat --- packages/delegation-core/src/caveats/nonce.ts | 33 ++- .../test/caveats/nonce.test.ts | 250 +++++++++++++++++- .../test/caveatBuilder/nonceBuilder.test.ts | 6 +- 3 files changed, 265 insertions(+), 24 deletions(-) diff --git a/packages/delegation-core/src/caveats/nonce.ts b/packages/delegation-core/src/caveats/nonce.ts index 4c3b89d7..923aa571 100644 --- a/packages/delegation-core/src/caveats/nonce.ts +++ b/packages/delegation-core/src/caveats/nonce.ts @@ -2,6 +2,7 @@ import { isHexString } from '@metamask/utils'; import type { BytesLike } from '@metamask/utils'; import { + bytesLikeToHex, defaultOptions, prepareResult, type EncodingOptions, @@ -16,7 +17,7 @@ const MAX_NONCE_STRING_LENGTH = 66; * Terms for configuring a Nonce caveat. */ export type NonceTerms = { - /** The nonce as a hex string to allow bulk revocation of delegations. */ + /** The nonce as BytesLike (0x-prefixed hex string or Uint8Array) to allow bulk revocation of delegations. */ nonce: BytesLike; }; @@ -41,8 +42,8 @@ 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 hex string. - * @throws Error if the nonce is invalid. + * @returns The terms as a 32-byte padded value in the specified encoding format. + * @throws Error if the nonce is invalid or empty. */ export function createNonceTerms( terms: NonceTerms, @@ -50,24 +51,34 @@ export function createNonceTerms( ): Hex | Uint8Array { const { nonce } = terms; - if (!nonce || nonce === '0x') { - throw new Error('Invalid nonce: must be a non-empty hex string'); + // Handle zero-length Uint8Array specifically + if (nonce instanceof Uint8Array && nonce.length === 0) { + throw new Error('Invalid nonce: Uint8Array must not be empty'); } - if (typeof nonce !== 'string' || !nonce.startsWith('0x')) { - throw new Error('Invalid nonce: must be a valid hex string'); + // Validate that strings have 0x prefix (as required by BytesLike) + if (typeof nonce === 'string' && !nonce.startsWith('0x')) { + throw new Error('Invalid nonce: string must have 0x prefix'); + } + + // Convert to hex string for consistent processing + const hexNonce = bytesLikeToHex(nonce); + + // Check for empty hex string (0x) first - more specific error + if (hexNonce === '0x') { + throw new Error('Invalid nonce: must not be empty'); } - if (!isHexString(nonce)) { - throw new Error('Invalid nonce: must be a valid hex string'); + if (!isHexString(hexNonce)) { + throw new Error('Invalid nonce: must be a valid BytesLike value'); } - if (nonce.length > MAX_NONCE_STRING_LENGTH) { + if (hexNonce.length > MAX_NONCE_STRING_LENGTH) { throw new Error('Invalid nonce: must be 32 bytes or less in length'); } // Remove '0x' prefix for padding, then add it back - const nonceWithoutPrefix = nonce.slice(2); + const nonceWithoutPrefix = hexNonce.slice(2); const paddedNonce = nonceWithoutPrefix.padStart(64, '0'); // 64 hex chars = 32 bytes const hexValue = `0x${paddedNonce}`; diff --git a/packages/delegation-core/test/caveats/nonce.test.ts b/packages/delegation-core/test/caveats/nonce.test.ts index 793027ad..6c7b7904 100644 --- a/packages/delegation-core/test/caveats/nonce.test.ts +++ b/packages/delegation-core/test/caveats/nonce.test.ts @@ -71,27 +71,27 @@ describe('createNonceTerms', () => { const nonce = '0x'; expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must be a non-empty hex string', + 'Invalid nonce: must not be empty', ); }); it('throws an error for undefined nonce', () => { expect(() => createNonceTerms({ nonce: undefined as any })).toThrow( - 'Invalid nonce: must be a non-empty hex string', + 'Value must be a Uint8Array', ); }); it('throws an error for null nonce', () => { expect(() => createNonceTerms({ nonce: null as any })).toThrow( - 'Invalid nonce: must be a non-empty hex string', + 'Value must be a Uint8Array', ); }); - it('throws an error for nonce without 0x prefix', () => { + it('throws an error for hex nonce without 0x prefix', () => { const nonce = '1234567890abcdef' as any; expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must be a valid hex string', + 'Invalid nonce: string must have 0x prefix', ); }); @@ -99,15 +99,15 @@ describe('createNonceTerms', () => { const nonce = '0x1234567890abcdefg' as any; expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must be a valid hex string', + 'Invalid nonce: must be a valid BytesLike value', ); }); - it('throws an error for non-string nonce', () => { + it('throws an error for non-BytesLike nonce', () => { const nonce = 123456 as any; expect(() => createNonceTerms({ nonce })).toThrow( - 'Invalid nonce: must be a valid hex string', + 'Value must be a Uint8Array', ); }); @@ -140,8 +140,80 @@ describe('createNonceTerms', () => { ); }); - // Tests for bytes return type - describe('bytes return type', () => { + // 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]); + }); + }); + + // 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' }); @@ -201,4 +273,162 @@ describe('createNonceTerms', () => { 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', + ); + }); + + 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 }); + 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 }); + 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 `0x${string}` }); + 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 `0x${string}` }); + 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', + ); + }); + }); }); diff --git a/packages/delegation-toolkit/test/caveatBuilder/nonceBuilder.test.ts b/packages/delegation-toolkit/test/caveatBuilder/nonceBuilder.test.ts index 73f13334..e54e406c 100644 --- a/packages/delegation-toolkit/test/caveatBuilder/nonceBuilder.test.ts +++ b/packages/delegation-toolkit/test/caveatBuilder/nonceBuilder.test.ts @@ -20,19 +20,19 @@ describe('nonceBuilder()', () => { describe('validation', () => { it('should fail with an empty nonce', () => { expect(() => buildWithNonce('0x')).to.throw( - 'Invalid nonce: must be a non-empty hex string', + 'Invalid nonce: must not be empty', ); }); it('should fail with a null nonce', () => { expect(() => buildWithNonce(null as any as Hex)).to.throw( - 'Invalid nonce: must be a non-empty hex string', + 'Value must be a Uint8Array', ); }); it('should fail with an invalid hex string', () => { expect(() => buildWithNonce('0xinvalid' as Hex)).to.throw( - 'Invalid nonce: must be a valid hex string', + 'Invalid nonce: must be a valid BytesLike value', ); });