Skip to content

Commit a331348

Browse files
committed
Add delegation encoding and decoding.
- Use these core methods in @metamask/delegation-toolkit
1 parent 0c6518f commit a331348

11 files changed

Lines changed: 874 additions & 28 deletions

File tree

packages/delegation-core/.eslintrc.cjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/** @type {import("eslint").Linter.Config} */
22
module.exports = {
3-
plugins: ['only-warn'],
43
root: true,
54
extends: ['../../shared/config/library.eslint.js'],
65
parser: '@typescript-eslint/parser',

packages/delegation-core/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,9 @@
6060
"tsup": "^7.2.0",
6161
"typescript": "5.0.4",
6262
"vitest": "^1.0.0"
63+
},
64+
"dependencies": {
65+
"@metamask/abi-utils": "^3.0.0",
66+
"@metamask/utils": "^11.4.0"
6367
}
6468
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { encodeSingle, decodeSingle } from '@metamask/abi-utils';
2+
import { type BytesLike } from '@metamask/utils';
3+
4+
import {
5+
bytesLikeToBytes,
6+
bytesLikeToHex,
7+
defaultOptions,
8+
prepareResult,
9+
type EncodingOptions,
10+
type ResultValue,
11+
} from './returns';
12+
import type { Delegation, Hex } from './types';
13+
14+
/**
15+
* To be used on a delegation as the root authority.
16+
*/
17+
export const ROOT_AUTHORITY =
18+
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
19+
20+
/**
21+
* The ABI types for an array of delegations.
22+
*/
23+
const DELEGATION_ARRAY_ABI_TYPES =
24+
'(address,address,bytes32,(address,bytes,bytes)[],uint256,bytes)[]' as const;
25+
26+
/**
27+
* Encodes an array of delegations into a permission context.
28+
* @param delegations - The delegations to encode.
29+
* @param options - Encoding options. Defaults to { out: 'hex' }.
30+
* @returns The encoded delegations as a hex string (default) or Uint8Array.
31+
*/
32+
export function encodeDelegations(
33+
delegations: Delegation[],
34+
options?: EncodingOptions<'hex'>,
35+
): Hex;
36+
export function encodeDelegations(
37+
delegations: Delegation[],
38+
options: EncodingOptions<'bytes'>,
39+
): Uint8Array;
40+
export function encodeDelegations(
41+
delegations: Delegation[],
42+
options: EncodingOptions<ResultValue> = defaultOptions,
43+
): Hex | Uint8Array {
44+
let result: Uint8Array;
45+
46+
if (delegations.length === 0) {
47+
// short circuit for empty delegations, derived from
48+
// `encode(['(address,address,bytes32,(address,bytes,bytes)[],uint256,bytes)[]'],[[]],);`
49+
result = new Uint8Array(64);
50+
result[31] = 0x20;
51+
} else {
52+
const encodableStructs = delegations.map((struct) => [
53+
struct.delegate,
54+
struct.delegator,
55+
struct.authority,
56+
struct.caveats.map((caveat) => [
57+
caveat.enforcer,
58+
caveat.terms,
59+
caveat.args,
60+
]),
61+
struct.salt,
62+
struct.signature,
63+
]);
64+
65+
result = encodeSingle(DELEGATION_ARRAY_ABI_TYPES, encodableStructs);
66+
}
67+
68+
return prepareResult(result, options);
69+
}
70+
71+
/**
72+
* Represents a decoded delegation as a tuple structure.
73+
* This type defines the structure of a delegation after it has been decoded from
74+
* an encoded format.
75+
*/
76+
type DecodedDelegation = [
77+
BytesLike,
78+
BytesLike,
79+
BytesLike,
80+
[BytesLike, BytesLike, BytesLike][],
81+
bigint,
82+
BytesLike,
83+
];
84+
85+
/**
86+
* Decodes an encoded permission context back into an array of delegations.
87+
* @param encoded - The encoded delegations as a hex string or Uint8Array.
88+
* @param options - Encoding options. Defaults to { out: 'hex' }.
89+
* @returns The decoded delegations array with types resolved based on options.
90+
*/
91+
export function decodeDelegations(
92+
encoded: BytesLike,
93+
options?: EncodingOptions<'hex'>,
94+
): Delegation<Hex>[];
95+
export function decodeDelegations(
96+
encoded: BytesLike,
97+
options: EncodingOptions<'bytes'>,
98+
): Delegation<Uint8Array>[];
99+
export function decodeDelegations(
100+
encoded: BytesLike,
101+
options: EncodingOptions<ResultValue> = defaultOptions,
102+
): Delegation<Hex>[] | Delegation<Uint8Array>[] {
103+
// it's possible to short circuit for empty delegations, but due to the
104+
// complexity of the input type, and the relative infrequency of empty delegations,
105+
// it's not worthwhile.
106+
107+
const decodedStructs = decodeSingle(
108+
DELEGATION_ARRAY_ABI_TYPES,
109+
encoded,
110+
// return types cannot be inferred from complex ABI types, so we must assert the type
111+
) as DecodedDelegation[];
112+
113+
if (options.out === 'bytes') {
114+
return decodedStructs.map((struct) =>
115+
delegationFromDecodedDelegation(struct, bytesLikeToBytes),
116+
);
117+
} else {
118+
return decodedStructs.map((struct) =>
119+
delegationFromDecodedDelegation(struct, bytesLikeToHex),
120+
);
121+
}
122+
}
123+
124+
/**
125+
* Converts a decoded delegation struct to a delegation object using the provided conversion function.
126+
* @param decodedDelegation - The decoded delegation struct as a tuple.
127+
* @param convertFn - Function to convert BytesLike values to the desired output type.
128+
* @returns A delegation object with all bytes-like values converted using the provided function.
129+
*/
130+
const delegationFromDecodedDelegation = <T extends BytesLike>(
131+
[delegate, delegator, authority, caveats, salt, signature]: DecodedDelegation,
132+
convertFn: (value: BytesLike) => T,
133+
): Delegation<T> => {
134+
return {
135+
delegate: convertFn(delegate),
136+
delegator: convertFn(delegator),
137+
authority: convertFn(authority),
138+
caveats: caveats.map(([enforcer, terms, args]) => ({
139+
enforcer: convertFn(enforcer),
140+
terms: convertFn(terms),
141+
args: convertFn(args),
142+
})),
143+
salt,
144+
signature: convertFn(signature),
145+
};
146+
};
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type { Hex } from './types';
1+
export type { Hex, Delegation, Caveat } from './types';
22

33
export {
44
createValueLteTerms,
@@ -7,3 +7,9 @@ export {
77
createExactCalldataTerms,
88
createNativeTokenStreamingTerms,
99
} from './caveats';
10+
11+
export {
12+
encodeDelegations,
13+
decodeDelegations,
14+
ROOT_AUTHORITY,
15+
} from './delegation';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { type BytesLike, bytesToHex, hexToBytes } from '@metamask/utils';
2+
3+
import type { Hex } from './types';
4+
5+
/**
6+
* The possible return value types for encoding/decoding operations.
7+
*/
8+
export type ResultValue = 'hex' | 'bytes';
9+
10+
/**
11+
* Utility type for function return types based on ResultValue.
12+
*/
13+
export type ResultType<TResultValue extends ResultValue> =
14+
TResultValue extends 'hex' ? Hex : Uint8Array;
15+
16+
/**
17+
* Base options interface for operations that can return hex or bytes.
18+
*/
19+
export type EncodingOptions<TResultValue extends ResultValue> = {
20+
out: TResultValue;
21+
};
22+
23+
/**
24+
* Default options value with proper typing. Use this as your default parameter.
25+
*/
26+
export const defaultOptions = { out: 'hex' } as EncodingOptions<any>;
27+
28+
/**
29+
* Prepares a result by converting between hex and bytes based on options.
30+
* @param result - The value to convert (either Uint8Array or Hex).
31+
* @param options - The options specifying the desired output format.
32+
* @returns The converted value with proper type narrowing.
33+
*/
34+
export function prepareResult<TResultValue extends ResultValue>(
35+
result: Uint8Array | Hex,
36+
options: EncodingOptions<TResultValue>,
37+
): ResultType<TResultValue> {
38+
if (options.out === 'hex') {
39+
const hexValue = typeof result === 'string' ? result : bytesToHex(result);
40+
return hexValue as ResultType<TResultValue>;
41+
}
42+
const bytesValue = result instanceof Uint8Array ? result : hexToBytes(result);
43+
return bytesValue as ResultType<TResultValue>;
44+
}
45+
46+
/**
47+
* Converts a bytes-like value to a hex string.
48+
* @param bytesLike - The bytes-like value to convert.
49+
* @returns The hex string representation of the bytes-like value.
50+
*/
51+
export const bytesLikeToHex = (bytesLike: BytesLike) => {
52+
if (typeof bytesLike === 'string') {
53+
return bytesLike;
54+
}
55+
return bytesToHex(bytesLike);
56+
};
57+
58+
/**
59+
* Converts a bytes-like value to a Uint8Array.
60+
* @param bytesLike - The bytes-like value to convert.
61+
* @returns The Uint8Array representation of the bytes-like value.
62+
*/
63+
export const bytesLikeToBytes = (bytesLike: BytesLike) => {
64+
if (typeof bytesLike === 'string') {
65+
return hexToBytes(bytesLike);
66+
}
67+
return bytesLike;
68+
};
Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,35 @@
1+
import type { BytesLike } from '@metamask/utils';
2+
3+
export type { Hex } from '@metamask/utils';
4+
15
/**
2-
* A hex value - a "0x"-prefixed string.
6+
* Represents a caveat that restricts or conditions a delegation.
7+
*
8+
* @property enforcer - The address of the contract that enforces this caveat's conditions.
9+
* @property terms - The terms or conditions of the caveat encoded as hex data.
10+
* @property args - Additional arguments required by the caveat enforcer, encoded as hex data.
311
*/
4-
export type Hex = `0x${string}`;
12+
export type Caveat<TBytes extends BytesLike = BytesLike> = {
13+
enforcer: TBytes;
14+
terms: TBytes;
15+
args: TBytes;
16+
};
17+
18+
/**
19+
* Represents a delegation that grants permissions from a delegator to a delegate.
20+
*
21+
* @property delegate - The address of the entity receiving the delegation.
22+
* @property delegator - The address of the entity granting the delegation.
23+
* @property authority - The authority under which this delegation is granted. For root delegations, this is ROOT_AUTHORITY.
24+
* @property caveats - An array of restrictions or conditions applied to this delegation.
25+
* @property salt - A unique value to prevent replay attacks and ensure uniqueness of the delegation.
26+
* @property signature - The cryptographic signature validating this delegation.
27+
*/
28+
export type Delegation<TBytes extends BytesLike = BytesLike> = {
29+
delegate: TBytes;
30+
delegator: TBytes;
31+
authority: TBytes;
32+
caveats: Caveat<TBytes>[];
33+
salt: bigint;
34+
signature: TBytes;
35+
};

0 commit comments

Comments
 (0)