Skip to content

Commit 0c6518f

Browse files
committed
Add caveat terms builders to core package, and use them in the caveatBuilders.
- exactCalldata - nativeTokenPeriodTransfer - nativeTokenStreaming - timestamp - valueLte
1 parent 96ab40c commit 0c6518f

16 files changed

Lines changed: 1071 additions & 108 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Hex } from '../types';
2+
3+
/**
4+
* Creates terms for an ExactCalldata caveat that ensures the provided execution calldata
5+
* matches exactly the expected calldata.
6+
*
7+
* @param callData.callData
8+
* @param callData - The expected calldata to match against.
9+
* @returns The terms as the calldata itself.
10+
* @throws Error if the callData is invalid.
11+
*/
12+
export function createExactCalldataTerms({ callData }: { callData: Hex }): Hex {
13+
if (!callData.startsWith('0x')) {
14+
throw new Error('Invalid callData: must be a hex string starting with 0x');
15+
}
16+
17+
// For exact calldata, the terms are simply the expected calldata
18+
return callData;
19+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { createValueLteTerms } from './valueLte';
2+
export { createTimestampTerms } from './timestamp';
3+
export { createNativeTokenPeriodTransferTerms } from './nativeTokenPeriodTransfer';
4+
export { createExactCalldataTerms } from './exactCalldata';
5+
export { createNativeTokenStreamingTerms } from './nativeTokenStreaming';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Hex } from '../types';
2+
import { toHexString } from '../utils';
3+
4+
/**
5+
* Creates terms for a NativeTokenPeriodTransfer caveat that validates that native token (ETH) transfers
6+
* do not exceed a specified amount within a given time period. The transferable amount resets at the
7+
* beginning of each period, and any unused ETH is forfeited once the period ends.
8+
*
9+
* @param periodAmount.periodAmount
10+
* @param periodAmount - The maximum amount of ETH (in wei) that can be transferred per period.
11+
* @param periodDuration - The duration of each period in seconds.
12+
* @param startDate - The timestamp when the first period begins.
13+
* @param periodAmount.periodDuration
14+
* @param periodAmount.startDate
15+
* @returns The terms as a 96-byte hex string (32 bytes for each parameter).
16+
* @throws Error if any of the numeric parameters are invalid.
17+
*/
18+
export function createNativeTokenPeriodTransferTerms({
19+
periodAmount,
20+
periodDuration,
21+
startDate,
22+
}: {
23+
periodAmount: bigint;
24+
periodDuration: number;
25+
startDate: number;
26+
}): Hex {
27+
if (periodAmount <= 0n) {
28+
throw new Error('Invalid periodAmount: must be a positive number');
29+
}
30+
31+
if (periodDuration <= 0) {
32+
throw new Error('Invalid periodDuration: must be a positive number');
33+
}
34+
35+
if (startDate <= 0) {
36+
throw new Error('Invalid startDate: must be a positive number');
37+
}
38+
39+
const periodAmountHex = toHexString({ value: periodAmount, size: 32 });
40+
const periodDurationHex = toHexString({ value: periodDuration, size: 32 });
41+
const startDateHex = toHexString({ value: startDate, size: 32 });
42+
43+
return `0x${periodAmountHex}${periodDurationHex}${startDateHex}`;
44+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { Hex } from '../types';
2+
import { toHexString } from '../utils';
3+
4+
// Upper bound for timestamps (January 1, 10000 CE)
5+
const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799;
6+
7+
/**
8+
* Creates terms for the NativeTokenStreamingEnforcer caveat, configuring a linear
9+
* streaming allowance of native tokens.
10+
*
11+
* The native token streaming caveat allows streaming of native tokens with:
12+
* - initialAmount: Amount available immediately at start time
13+
* - maxAmount: Maximum total amount that can be transferred
14+
* - amountPerSecond: Rate at which additional allowance accrues per second
15+
* - startTime: Timestamp when streaming begins
16+
*
17+
* Available amount = min(initialAmount + amountPerSecond * (currentTime - startTime), maxAmount)
18+
*
19+
* @param initialAmount.initialAmount
20+
* @param initialAmount - The initial amount available immediately (in wei).
21+
* @param maxAmount - The maximum total amount that can be transferred (in wei).
22+
* @param amountPerSecond - The rate at which allowance increases per second (in wei).
23+
* @param startTime - Unix timestamp when streaming begins.
24+
* @param initialAmount.maxAmount
25+
* @param initialAmount.amountPerSecond
26+
* @param initialAmount.startTime
27+
* @returns Hex-encoded terms for the caveat (128 bytes).
28+
* @throws Error if initialAmount is negative.
29+
* @throws Error if maxAmount is not positive.
30+
* @throws Error if maxAmount is less than initialAmount.
31+
* @throws Error if amountPerSecond is not positive.
32+
* @throws Error if startTime is not positive.
33+
* @throws Error if startTime exceeds upper bound.
34+
*/
35+
export function createNativeTokenStreamingTerms({
36+
initialAmount,
37+
maxAmount,
38+
amountPerSecond,
39+
startTime,
40+
}: {
41+
initialAmount: bigint;
42+
maxAmount: bigint;
43+
amountPerSecond: bigint;
44+
startTime: number;
45+
}): Hex {
46+
if (initialAmount < 0n) {
47+
throw new Error('Invalid initialAmount: must be greater than zero');
48+
}
49+
50+
if (maxAmount <= 0n) {
51+
throw new Error('Invalid maxAmount: must be a positive number');
52+
}
53+
54+
if (maxAmount < initialAmount) {
55+
throw new Error('Invalid maxAmount: must be greater than initialAmount');
56+
}
57+
58+
if (amountPerSecond <= 0n) {
59+
throw new Error('Invalid amountPerSecond: must be a positive number');
60+
}
61+
62+
if (startTime <= 0) {
63+
throw new Error('Invalid startTime: must be a positive number');
64+
}
65+
66+
if (startTime > TIMESTAMP_UPPER_BOUND_SECONDS) {
67+
throw new Error(
68+
'Invalid startTime: must be less than or equal to 253402300799',
69+
);
70+
}
71+
72+
const initialAmountHex = toHexString({ value: initialAmount, size: 32 });
73+
const maxAmountHex = toHexString({ value: maxAmount, size: 32 });
74+
const amountPerSecondHex = toHexString({ value: amountPerSecond, size: 32 });
75+
const startTimeHex = toHexString({ value: startTime, size: 32 });
76+
77+
return `0x${initialAmountHex}${maxAmountHex}${amountPerSecondHex}${startTimeHex}`;
78+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { Hex } from '../types';
2+
import { toHexString } from '../utils';
3+
4+
// Upper bound for timestamps (equivalent to January 1, 10000 CE)
5+
const TIMESTAMP_UPPER_BOUND_SECONDS = 253402300799;
6+
7+
/**
8+
* Creates terms for a Timestamp caveat that enforces time-based constraints on delegation usage.
9+
*
10+
* @param timestampAfterThreshold.timestampAfterThreshold
11+
* @param timestampAfterThreshold - The timestamp (in seconds) after which the delegation can be used.
12+
* @param timestampBeforeThreshold - The timestamp (in seconds) before which the delegation can be used.
13+
* @param timestampAfterThreshold.timestampBeforeThreshold
14+
* @returns The terms as a 32-byte hex string (16 bytes for each timestamp).
15+
* @throws Error if the timestamps are invalid.
16+
*/
17+
export function createTimestampTerms({
18+
timestampAfterThreshold,
19+
timestampBeforeThreshold,
20+
}: {
21+
timestampAfterThreshold: number;
22+
timestampBeforeThreshold: number;
23+
}): Hex {
24+
if (timestampAfterThreshold < 0) {
25+
throw new Error(
26+
'Invalid timestampAfterThreshold: must be zero or positive',
27+
);
28+
}
29+
30+
if (timestampBeforeThreshold < 0) {
31+
throw new Error(
32+
'Invalid timestampBeforeThreshold: must be zero or positive',
33+
);
34+
}
35+
36+
if (timestampBeforeThreshold > TIMESTAMP_UPPER_BOUND_SECONDS) {
37+
throw new Error(
38+
`Invalid timestampBeforeThreshold: must be less than or equal to ${TIMESTAMP_UPPER_BOUND_SECONDS}`,
39+
);
40+
}
41+
42+
if (timestampAfterThreshold > TIMESTAMP_UPPER_BOUND_SECONDS) {
43+
throw new Error(
44+
`Invalid timestampAfterThreshold: must be less than or equal to ${TIMESTAMP_UPPER_BOUND_SECONDS}`,
45+
);
46+
}
47+
48+
if (
49+
timestampBeforeThreshold !== 0 &&
50+
timestampAfterThreshold >= timestampBeforeThreshold
51+
) {
52+
throw new Error(
53+
'Invalid thresholds: timestampBeforeThreshold must be greater than timestampAfterThreshold when both are specified',
54+
);
55+
}
56+
57+
const afterThresholdHex = toHexString({
58+
value: timestampAfterThreshold,
59+
size: 16,
60+
});
61+
const beforeThresholdHex = toHexString({
62+
value: timestampBeforeThreshold,
63+
size: 16,
64+
});
65+
66+
return `0x${afterThresholdHex}${beforeThresholdHex}`;
67+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Hex } from '../types';
2+
import { toHexString } from '../utils';
3+
4+
/**
5+
* Creates terms for a ValueLte caveat that limits the maximum value of native tokens that can be spent.
6+
*
7+
* @param maxValue.maxValue
8+
* @param maxValue - The maximum value allowed for the transaction as a bigint.
9+
* @returns The terms as a 32-byte hex string.
10+
* @throws Error if the maxValue is negative.
11+
*/
12+
export function createValueLteTerms({ maxValue }: { maxValue: bigint }): Hex {
13+
if (maxValue < 0n) {
14+
throw new Error('Invalid maxValue: must be greater than or equal to zero');
15+
}
16+
17+
const hexValue = toHexString({ value: maxValue, size: 32 });
18+
19+
return `0x${hexValue}`;
20+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import { createExactCalldataTerms } from '../../src/caveats/exactCalldata';
4+
import type { Hex } from '../../src/types';
5+
6+
describe('createExactCalldataTerms', () => {
7+
it('creates valid terms for simple calldata', () => {
8+
const callData = '0x1234567890abcdef';
9+
const result = createExactCalldataTerms({ callData });
10+
11+
expect(result).toStrictEqual(callData);
12+
});
13+
14+
it('creates valid terms for empty calldata', () => {
15+
const callData = '0x';
16+
const result = createExactCalldataTerms({ callData });
17+
18+
expect(result).toStrictEqual('0x');
19+
});
20+
21+
it('creates valid terms for function call with parameters', () => {
22+
// Example: transfer(address,uint256) function call
23+
const callData =
24+
'0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e0000000000000000000000000000000000000000000000000de0b6b3a7640000';
25+
const result = createExactCalldataTerms({ callData });
26+
27+
expect(result).toStrictEqual(callData);
28+
});
29+
30+
it('creates valid terms for complex calldata', () => {
31+
const callData =
32+
'0x23b872dd000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5e000000000000000000000000742d35cc6634c0532925a3b8d40ec49b0e8baa5f0000000000000000000000000000000000000000000000000de0b6b3a7640000';
33+
const result = createExactCalldataTerms({ callData });
34+
35+
expect(result).toStrictEqual(callData);
36+
});
37+
38+
it('creates valid terms for uppercase hex calldata', () => {
39+
const callData = '0x1234567890ABCDEF';
40+
const result = createExactCalldataTerms({ callData });
41+
42+
expect(result).toStrictEqual(callData);
43+
});
44+
45+
it('creates valid terms for mixed case hex calldata', () => {
46+
const callData = '0x1234567890AbCdEf';
47+
const result = createExactCalldataTerms({ callData });
48+
49+
expect(result).toStrictEqual(callData);
50+
});
51+
52+
it('creates valid terms for very long calldata', () => {
53+
const longCalldata: Hex = `0x${'a'.repeat(1000)}`;
54+
const result = createExactCalldataTerms({ callData: longCalldata });
55+
56+
expect(result).toStrictEqual(longCalldata);
57+
});
58+
59+
it('throws an error for calldata without 0x prefix', () => {
60+
const invalidCallData = '1234567890abcdef';
61+
62+
expect(() =>
63+
createExactCalldataTerms({ callData: invalidCallData as Hex }),
64+
).toThrow('Invalid callData: must be a hex string starting with 0x');
65+
});
66+
67+
it('throws an error for empty string', () => {
68+
const invalidCallData = '';
69+
70+
expect(() =>
71+
createExactCalldataTerms({ callData: invalidCallData as Hex }),
72+
).toThrow('Invalid callData: must be a hex string starting with 0x');
73+
});
74+
75+
it('throws an error for malformed hex prefix', () => {
76+
const invalidCallData = '0X1234'; // uppercase X
77+
78+
expect(() =>
79+
createExactCalldataTerms({ callData: invalidCallData as Hex }),
80+
).toThrow('Invalid callData: must be a hex string starting with 0x');
81+
});
82+
83+
it('handles single function selector', () => {
84+
const functionSelector = '0xa9059cbb'; // transfer(address,uint256) selector
85+
const result = createExactCalldataTerms({ callData: functionSelector });
86+
87+
expect(result).toStrictEqual(functionSelector);
88+
});
89+
90+
it('handles calldata with odd length', () => {
91+
const oddLengthCalldata = '0x123';
92+
const result = createExactCalldataTerms({ callData: oddLengthCalldata });
93+
94+
expect(result).toStrictEqual(oddLengthCalldata);
95+
});
96+
});

0 commit comments

Comments
 (0)