Skip to content

feat: add terms decoding utils#191

Open
jeffsmale90 wants to merge 15 commits intomainfrom
cursor/delegation-term-decoders-d66a
Open

feat: add terms decoding utils#191
jeffsmale90 wants to merge 15 commits intomainfrom
cursor/delegation-term-decoders-d66a

Conversation

@jeffsmale90
Copy link
Collaborator

@jeffsmale90 jeffsmale90 commented Mar 23, 2026

Implements terms decoders in @metamask/delegation-core for every caveat enforcer that already had a terms encoder, so encoded terms bytes can be turned back into structured objects. Adds decodeCaveat in @metamask/smart-accounts-kit, which uses SmartAccountsEnvironment.caveatEnforcers to map an on-chain enforcer address to the right decoder and returns a CoreCaveatConfiguration (discriminated by type) for builders and tooling.

Closes #184.

Changes

@metamask/delegation-core

  • Decoder APIs: For each caveat module, a decodeTerms(terms, encodingOptions?) companion to createTerms. Decoders accept BytesLike input and, where byte payloads are involved, support EncodingOptions (out: 'hex' | 'bytes') so callers can get 0x… strings or Uint8Array consistently with encoding helpers.
  • Shared parsing helpers in internalUtils.ts: extractBigInt, extractNumber, extractAddress, extractHex, extractRemainingHex (fixed slices and “rest of buffer” extraction from normalized hex).
  • returns.ts: DecodedBytesLike alias for clearer generics on decoded *Terms types.
  • Term types: Several *Terms types are now generic over bytes-like fields (e.g. AllowedMethodsTerms) so decoded shapes stay typed when choosing hex vs bytes output.
  • Docs: Short enforcer-oriented module headers on caveat files (what the enforcer does + on-wire layout). README updated for timestamp terms field names (see breaking change below).
  • Exports: All new decoders re-exported from caveats/index.ts and the package index.ts.
  • timestamp builder / types: Aligned with renamed timestamp term fields (afterThreshold / beforeThreshold).

@metamask/smart-accounts-kit/utils

  • decodeCaveat: { caveat, environment } → CoreCaveatConfiguration; throws if enforcer is not in environment.caveatEnforcers.

Note

Medium Risk
Adds a large new surface area of decoding functions and changes the TimestampTerms field names, which is a small breaking API change and could affect downstream callers if not updated. Most changes are deterministic parsing/validation logic with good test coverage, but mistakes would cause mis-decoding of on-chain terms.

Overview
Adds decode*Terms functions across delegation caveats so encoded terms (Hex/Uint8Array) can be parsed back into structured *Terms objects, with output configurable to hex or bytes to mirror existing encoding APIs.

Introduces shared slicing/validation utilities in internalUtils.ts (e.g., extractBigInt, extractAddress, length assertions) and a DecodedBytesLike alias to keep decoded term types correctly parameterized; many *Terms types are now generic over bytes-like fields.

Updates public exports (caveats/index.ts, package index.ts), expands unit tests to cover decoding and invalid payload lengths, and renames TimestampTerms fields to afterThreshold/beforeThreshold (docs updated accordingly).

Written by Cursor Bugbot for commit 7d0bd1d. This will update automatically on new commits. Configure here.

cursoragent and others added 7 commits March 23, 2026 21:59
- 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 <jeffsmale90@users.noreply.github.com>
- 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 <jeffsmale90@users.noreply.github.com>
- 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 <jeffsmale90@users.noreply.github.com>
- change type string to Hex
- remove unnecessary comments
- change parameter name hexString to value
- add test coverage for new utilities
@jeffsmale90 jeffsmale90 force-pushed the cursor/delegation-term-decoders-d66a branch from 6cc446f to c03605c Compare March 24, 2026 02:22
@jeffsmale90 jeffsmale90 changed the title feat: add terms decoders for all caveat enforcers feat: add terms decoding utils Mar 24, 2026
- Add generic TBytesLike parameter to core caveat config types
- Rename timestampBuilder config properties in core to match smart-accounts-kit
@jeffsmale90 jeffsmale90 force-pushed the cursor/delegation-term-decoders-d66a branch from b0a3392 to 578079e Compare March 24, 2026 22:27
@jeffsmale90 jeffsmale90 force-pushed the cursor/delegation-term-decoders-d66a branch from 578079e to 3f2af91 Compare March 25, 2026 02:24
@jeffsmale90 jeffsmale90 marked this pull request as ready for review March 25, 2026 02:58
@jeffsmale90 jeffsmale90 requested a review from a team as a code owner March 25, 2026 02:58
*/
export function decodeLimitedCallsTerms(terms: BytesLike): LimitedCallsTerms {
const hexTerms = bytesLikeToHex(terms);
const limit = extractNumber(hexTerms, 0, 32);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are extracting number here. But limit is uint256 so it can go over number limit. BigInt would be safer. (practically speaking it would be quite an edge case to get to a number that big to break it, but just something to consider - same for other usages of extractNumber). If this is intentional then maybe just add a comment? Or maybe a hybrid where we decode to bigint and if its in correct range we return number otherwise throw an error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a good point.

All of the values that are encoded as a type larger than MAX_SAFE_INTEGER are either a timestamp or duration (seconds) or a count (index or limit). I think for those number type is ergonomic - having to BigInt(Date.now() / 1000) constantly would get tedious.

Saying that, it's not technically correct, so I updated the extractNumber function to convert to BigInt, and throw if it's greater than Number.MAX_SAFE_INTEGER, to provide safety.

The vast majority of use cases would not necessitate a value > MAX_SAFE_INTEGER, and we're not prohibiting encoding values great - just not providing tooling to make it easier.

We can still consider converting them to number, but it'll be a breaking change for existing encoders, so I'd put it into a separate PR.

| AllowedTargetsTerms<DecodedBytesLike<'bytes'>> {
const hexTerms = bytesLikeToHex(terms);

const addressSize = 20;
Copy link
Member

@MoMannn MoMannn Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a question. But should we also check the terms length here to make sure they are correct?

On chain we are checking terms length: https://github.com/MetaMask/delegation-framework/blob/f1d5913120fc772a4b2cc7ba4179450ffa2988c1/src/enforcers/AllowedTargetsEnforcer.sol#L58

So it should not be possible to have different terms length here. But the function could be used on a wrong data or something like that and this would be able to detect this. 🤔

If we should check terms then this should be on other enforcers as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call - nice little sanity check that might save someone's sanity if they're having to debug!

I've added length checks to all terms decoders.

* @returns The extracted bigint value.
*/
export const extractBigInt = (
value: string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be Hex like with the others?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

absolutely it should be!

environment: SmartAccountsEnvironment;
}): CoreCaveatConfiguration => {
switch (enforcer) {
case caveatEnforcers.AllowedCalldataEnforcer:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make sure that addresses are normalized (always to lowerCase on both sides)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely we should - good call!

- 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
@jeffsmale90 jeffsmale90 requested a review from MoMannn March 25, 2026 21:50
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

@jeffsmale90 jeffsmale90 marked this pull request as draft March 26, 2026 04:09
@jeffsmale90 jeffsmale90 force-pushed the cursor/delegation-term-decoders-d66a branch from 2651ece to bdef1a6 Compare March 26, 2026 20:59
@jeffsmale90 jeffsmale90 marked this pull request as ready for review March 26, 2026 20:59
@jeffsmale90 jeffsmale90 force-pushed the cursor/delegation-term-decoders-d66a branch from bdef1a6 to 97f6038 Compare March 26, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Terms decoders

3 participants