Skip to content

Commit a84f872

Browse files
authored
Add createERC20TokenPeriodTransferTerms for @metamask/delegation-core (#15)
* Add erc20TokenPeriodTransfer terms builder * Fix incorrect terms length description in readme
1 parent 93c14db commit a84f872

6 files changed

Lines changed: 135 additions & 26 deletions

File tree

packages/delegation-core/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,33 @@ const terms = createERC20StreamingTerms({
184184
});
185185
```
186186

187+
#### `createERC20TokenPeriodTransferTerms(terms, options?)`
188+
189+
Creates terms for an ERC20TokenPeriodTransfer caveat that validates that ERC20 token transfers do not exceed a specified amount within a given time period.
190+
191+
**Parameters:**
192+
- `terms: ERC20TokenPeriodTransferTerms`
193+
- `tokenAddress: BytesLike` - The address of the ERC20 token.
194+
- `periodAmount: bigint` - The maximum amount that can be transferred within each period.
195+
- `periodDuration: number` - The duration of each period in seconds.
196+
- `startDate: number` - Unix timestamp when the first period begins.
197+
- `options?: EncodingOptions` - Optional encoding options
198+
199+
**Returns:** `Hex | Uint8Array` - 116-byte encoded terms (20 bytes for address + 32 bytes per parameter)
200+
201+
**Example:**
202+
```typescript
203+
import { createERC20TokenPeriodTransferTerms } from '@metamask/delegation-core';
204+
205+
// Allow 100 tokens per day starting from a specific date
206+
const terms = createERC20TokenPeriodTransferTerms({
207+
tokenAddress: '0xA0b86a33E6441E74C65c6BF2A6d73B895B9b34A2',
208+
periodAmount: 100n, // 100 tokens
209+
periodDuration: 86400, // 24 hours in seconds
210+
startDate: 1640995200 // 2022-01-01 00:00:00 UTC
211+
});
212+
```
213+
187214
### Delegation Utilities
188215

189216
#### `encodeDelegations(delegations)`

packages/delegation-core/src/caveats/erc20Streaming.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,18 @@ export function createERC20StreamingTerms(
7171
throw new Error('Invalid tokenAddress: must be a valid address');
7272
}
7373

74-
let tokenAddressHex: string;
74+
let prefixedTokenAddressHex: string;
7575

7676
if (typeof tokenAddress === 'string') {
7777
if (!isHexString(tokenAddress) || tokenAddress.length !== 42) {
7878
throw new Error('Invalid tokenAddress: must be a valid address');
7979
}
80-
tokenAddressHex = tokenAddress.slice(2);
80+
prefixedTokenAddressHex = tokenAddress;
8181
} else {
8282
if (tokenAddress.length !== 20) {
8383
throw new Error('Invalid tokenAddress: must be a valid address');
8484
}
85-
tokenAddressHex = bytesToHex(tokenAddress).slice(2);
85+
prefixedTokenAddressHex = bytesToHex(tokenAddress);
8686
}
8787

8888
if (initialAmount < 0n) {
@@ -116,7 +116,7 @@ export function createERC20StreamingTerms(
116116
const amountPerSecondHex = toHexString({ value: amountPerSecond, size: 32 });
117117
const startTimeHex = toHexString({ value: startTime, size: 32 });
118118

119-
const hexValue = `0x${tokenAddressHex}${initialAmountHex}${maxAmountHex}${amountPerSecondHex}${startTimeHex}`;
119+
const hexValue = `${prefixedTokenAddressHex}${initialAmountHex}${maxAmountHex}${amountPerSecondHex}${startTimeHex}`;
120120

121121
return prepareResult(hexValue, encodingOptions);
122122
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { type BytesLike, isHexString, bytesToHex } from '@metamask/utils';
2+
3+
import {
4+
defaultOptions,
5+
prepareResult,
6+
type EncodingOptions,
7+
type ResultValue,
8+
} from '../returns';
9+
import type { Hex } from '../types';
10+
import { toHexString } from '../utils';
11+
12+
/**
13+
* Terms for configuring a periodic transfer allowance of ERC20 tokens.
14+
*/
15+
export type ERC20TokenPeriodTransferTerms = {
16+
/** The address of the ERC20 token. */
17+
tokenAddress: BytesLike;
18+
/** The maximum amount that can be transferred within each period. */
19+
periodAmount: bigint;
20+
/** The duration of each period in seconds. */
21+
periodDuration: number;
22+
/** Unix timestamp when the first period begins. */
23+
startDate: number;
24+
};
25+
26+
/**
27+
* Creates terms for an ERC20TokenPeriodTransfer caveat that validates that ERC20 token transfers
28+
* do not exceed a specified amount within a given time period. The transferable amount resets at the
29+
* beginning of each period, and any unused tokens are forfeited once the period ends.
30+
*
31+
* @param terms - The terms for the ERC20TokenPeriodTransfer caveat.
32+
* @param encodingOptions - The encoding options for the result.
33+
* @returns The terms as a 128-byte hex string (32 bytes for each parameter).
34+
* @throws Error if any of the numeric parameters are invalid.
35+
*/
36+
export function createERC20TokenPeriodTransferTerms(
37+
terms: ERC20TokenPeriodTransferTerms,
38+
encodingOptions?: EncodingOptions<'hex'>,
39+
): Hex;
40+
export function createERC20TokenPeriodTransferTerms(
41+
terms: ERC20TokenPeriodTransferTerms,
42+
encodingOptions: EncodingOptions<'bytes'>,
43+
): Uint8Array;
44+
/**
45+
* Creates terms for an ERC20TokenPeriodTransfer caveat that validates that ERC20 token transfers
46+
* do not exceed a specified amount within a given time period.
47+
*
48+
* @param terms - The terms for the ERC20TokenPeriodTransfer caveat.
49+
* @param encodingOptions - The encoding options for the result.
50+
* @returns The terms as a 128-byte hex string (32 bytes for each parameter).
51+
* @throws Error if any of the numeric parameters are invalid.
52+
*/
53+
export function createERC20TokenPeriodTransferTerms(
54+
terms: ERC20TokenPeriodTransferTerms,
55+
encodingOptions: EncodingOptions<ResultValue> = defaultOptions,
56+
): Hex | Uint8Array {
57+
const { tokenAddress, periodAmount, periodDuration, startDate } = terms;
58+
59+
if (!tokenAddress) {
60+
throw new Error('Invalid tokenAddress: must be a valid address');
61+
}
62+
63+
let prefixedTokenAddressHex: string;
64+
65+
if (typeof tokenAddress === 'string') {
66+
if (!isHexString(tokenAddress) || tokenAddress.length !== 42) {
67+
throw new Error('Invalid tokenAddress: must be a valid address');
68+
}
69+
prefixedTokenAddressHex = tokenAddress;
70+
} else {
71+
if (tokenAddress.length !== 20) {
72+
throw new Error('Invalid tokenAddress: must be a valid address');
73+
}
74+
prefixedTokenAddressHex = bytesToHex(tokenAddress);
75+
}
76+
77+
if (periodAmount <= 0n) {
78+
throw new Error('Invalid periodAmount: must be a positive number');
79+
}
80+
81+
if (periodDuration <= 0) {
82+
throw new Error('Invalid periodDuration: must be a positive number');
83+
}
84+
85+
if (startDate <= 0) {
86+
throw new Error('Invalid startDate: must be a positive number');
87+
}
88+
89+
const periodAmountHex = toHexString({ value: periodAmount, size: 32 });
90+
const periodDurationHex = toHexString({ value: periodDuration, size: 32 });
91+
const startDateHex = toHexString({ value: startDate, size: 32 });
92+
93+
const hexValue = `${prefixedTokenAddressHex}${periodAmountHex}${periodDurationHex}${startDateHex}`;
94+
95+
return prepareResult(hexValue, encodingOptions);
96+
}

packages/delegation-core/src/caveats/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { createNativeTokenPeriodTransferTerms } from './nativeTokenPeriodTransfe
44
export { createExactCalldataTerms } from './exactCalldata';
55
export { createNativeTokenStreamingTerms } from './nativeTokenStreaming';
66
export { createERC20StreamingTerms } from './erc20Streaming';
7+
export { createERC20TokenPeriodTransferTerms } from './erc20TokenPeriodTransfer';

packages/delegation-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export {
1111
createExactCalldataTerms,
1212
createNativeTokenStreamingTerms,
1313
createERC20StreamingTerms,
14+
createERC20TokenPeriodTransferTerms,
1415
} from './caveats';
1516

1617
export {

packages/delegation-toolkit/src/caveatBuilder/erc20PeriodTransferBuilder.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { concat, isAddress, toHex } from 'viem';
1+
import { createERC20TokenPeriodTransferTerms } from '@metamask/delegation-core';
22
import type { Address } from 'viem';
33

44
import type { Caveat, DeleGatorEnvironment } from '../types';
@@ -26,28 +26,12 @@ export const erc20PeriodTransferBuilder = (
2626
periodDuration: number,
2727
startDate: number,
2828
): Caveat => {
29-
if (!isAddress(tokenAddress)) {
30-
throw new Error('Invalid tokenAddress: must be a valid address');
31-
}
32-
33-
if (periodAmount <= 0n) {
34-
throw new Error('Invalid periodAmount: must be a positive number');
35-
}
36-
37-
if (periodDuration <= 0) {
38-
throw new Error('Invalid periodDuration: must be a positive number');
39-
}
40-
41-
if (startDate <= 0) {
42-
throw new Error('Invalid startDate: must be a positive number');
43-
}
44-
45-
const terms = concat([
29+
const terms = createERC20TokenPeriodTransferTerms({
4630
tokenAddress,
47-
toHex(periodAmount, { size: 32 }),
48-
toHex(periodDuration, { size: 32 }),
49-
toHex(startDate, { size: 32 }),
50-
]);
31+
periodAmount,
32+
periodDuration,
33+
startDate,
34+
});
5135

5236
const {
5337
caveatEnforcers: { ERC20PeriodTransferEnforcer },

0 commit comments

Comments
 (0)