Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions packages/delegation-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,23 +240,48 @@ import { ROOT_AUTHORITY } from '@metamask/delegation-core';
console.log(ROOT_AUTHORITY); // Root authority identifier
```

#### `hashDelegation(delegation)`

Computes a hash for a given delegation object.

**Parameters:**
- `delegation: DelegationStruct` - The delegation object to hash

**Returns:** `Hex` - The hash of the delegation

**Example:**
```typescript
import { hashDelegation } from '@metamask/delegation-core';

const delegation = {
delegate: '0x742d35Cc6634C0532925a3b8d40EC49b0e8BaA5e',
delegator: '0x...',
authority: '0x...',
caveats: [],
salt: 0n,
signature: '0x...'
};

const hash = hashDelegation(delegation);
```

## Type Definitions

### Core Types

```typescript
export type Hex = `0x${string}`;

export type Delegation = {
export type DelegationStruct = {
delegate: string;
delegator: string;
authority: string;
caveats: Caveat[];
caveats: CaveatStruct[];
salt: bigint;
signature: string;
};

export type Caveat = {
export type CaveatStruct = {
enforcer: string;
terms: string;
};
Expand Down
3 changes: 2 additions & 1 deletion packages/delegation-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
},
"dependencies": {
"@metamask/abi-utils": "^3.0.0",
"@metamask/utils": "^11.4.0"
"@metamask/utils": "^11.4.0",
"@noble/hashes": "^1.8.0"
}
}
130 changes: 120 additions & 10 deletions packages/delegation-core/src/delegation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { encodeSingle, decodeSingle } from '@metamask/abi-utils';
import { type BytesLike } from '@metamask/utils';
import { encode, encodeSingle, decodeSingle } from '@metamask/abi-utils';
import { hexToBytes, type BytesLike } from '@metamask/utils';
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';

import {
bytesLikeToBytes,
Expand All @@ -9,14 +10,35 @@ import {
type EncodingOptions,
type ResultValue,
} from './returns';
import type { Delegation, Hex } from './types';
import type { CaveatStruct, DelegationStruct, Hex } from './types';

/**
* When designated as the delegate address in a delegation, this allows any beneficiary to redeem the delegation.
*/
export const ANY_BENEFICIARY = '0x0000000000000000000000000000000000000a11';

/**
* To be used on a delegation as the root authority.
*/
export const ROOT_AUTHORITY =
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';

/**
* The typehash for a delegation, used when generating a delegation hash.
*
* keccak('Delegation(address delegate,address delegator,bytes32 authority,Caveat[] caveats,uint256 salt)Caveat(address enforcer,bytes terms)')
*/
export const DELEGATION_TYPEHASH =
'0x88c1d2ecf185adf710588203a5f263f0ff61be0d33da39792cde19ba9aa4331e';

/**
* The typehash for a caveat, used when generating a caveat hash.
*
* keccak('Caveat(address enforcer,bytes terms)')
*/
export const CAVEAT_TYPEHASH =
'0x80ad7e1b04ee6d994a125f4714ca0720908bd80ed16063ec8aee4b88e9253e2d';

/**
* The ABI types for an array of delegations.
*/
Expand All @@ -30,11 +52,11 @@ const DELEGATION_ARRAY_ABI_TYPES =
* @returns The encoded delegations as a hex string (default) or Uint8Array.
*/
export function encodeDelegations(
delegations: Delegation[],
delegations: DelegationStruct[],
options?: EncodingOptions<'hex'>,
): Hex;
export function encodeDelegations(
delegations: Delegation[],
delegations: DelegationStruct[],
options: EncodingOptions<'bytes'>,
): Uint8Array;
/**
Expand All @@ -44,7 +66,7 @@ export function encodeDelegations(
* @returns The encoded delegations as a hex string (default) or Uint8Array.
*/
export function encodeDelegations(
delegations: Delegation[],
delegations: DelegationStruct[],
options: EncodingOptions<ResultValue> = defaultOptions,
): Hex | Uint8Array {
let result: Uint8Array;
Expand Down Expand Up @@ -83,7 +105,7 @@ export function encodeDelegations(
const delegationFromDecodedDelegation = <TEncoding extends BytesLike>(
decodedDelegation: DecodedDelegation,
convertFn: (value: BytesLike) => TEncoding,
): Delegation<TEncoding> => {
): DelegationStruct<TEncoding> => {
const [delegate, delegator, authority, caveats, salt, signature] =
decodedDelegation;

Expand Down Expand Up @@ -124,11 +146,11 @@ type DecodedDelegation = [
export function decodeDelegations(
encoded: BytesLike,
options?: EncodingOptions<'hex'>,
): Delegation<Hex>[];
): DelegationStruct<Hex>[];
export function decodeDelegations(
encoded: BytesLike,
options: EncodingOptions<'bytes'>,
): Delegation<Uint8Array>[];
): DelegationStruct<Uint8Array>[];
/**
* Decodes an encoded permission context back into an array of delegations.
* @param encoded - The encoded delegations as a hex string or Uint8Array.
Expand All @@ -138,7 +160,7 @@ export function decodeDelegations(
export function decodeDelegations(
encoded: BytesLike,
options: EncodingOptions<ResultValue> = defaultOptions,
): Delegation<Hex>[] | Delegation<Uint8Array>[] {
): DelegationStruct<Hex>[] | DelegationStruct<Uint8Array>[] {
// it's possible to short circuit for empty delegations, but due to the
// complexity of the input type, and the relative infrequency of empty delegations,
// it's not worthwhile.
Expand All @@ -158,3 +180,91 @@ export function decodeDelegations(
delegationFromDecodedDelegation(struct, bytesLikeToHex),
);
}

/**
* Calculates the hash of a delegation for signing purposes.
* The hash is computed by encoding the delegation parameters with the delegation typehash
* and then applying keccak256 hashing.
*
* @param delegation - The delegation to hash.
* @param options - Encoding options. Defaults to { out: 'hex' }.
* @returns The keccak256 hash of the encoded delegation as a hex string (default) or Uint8Array.
*/
export function hashDelegation(
delegation: DelegationStruct,
options?: EncodingOptions<'hex'>,
): Hex;
export function hashDelegation(
delegation: DelegationStruct,
options: EncodingOptions<'bytes'>,
): Uint8Array;
/**
* Calculates the hash of a delegation for signing purposes.
* The hash is computed by encoding the delegation parameters with the delegation typehash
* and then applying keccak256 hashing.
*
* @param delegation - The delegation to hash.
* @param options - Encoding options. Defaults to { out: 'hex' }.
* @returns The keccak256 hash of the encoded delegation as a hex string (default) or Uint8Array.
*/
export function hashDelegation(
delegation: DelegationStruct,
options: EncodingOptions<ResultValue> = defaultOptions,
): Hex | Uint8Array {
const encoded = encode(
['bytes32', 'address', 'address', 'bytes32', 'bytes32', 'uint256'],
[
DELEGATION_TYPEHASH,
delegation.delegate,
delegation.delegator,
delegation.authority,
getCaveatsArrayHash(delegation.caveats),
delegation.salt,
],
);
const hash = keccak256(encoded);
return prepareResult(hash, options);
}

/**
* Calculates the hash of an array of caveats. The caveats are individually abi
* encoded and hashed, and concatenated. The resulting byte array is then
* hashed to produce the CaveatsArrayHash.
*
* @param caveats - The array of caveats to hash.
* @returns The keccak256 hash of the encoded caveat array.
*/
function getCaveatsArrayHash(caveats: CaveatStruct[]): Uint8Array {
const byteLength = 32 * caveats.length;
const encoded = new Uint8Array(byteLength);

for (let i = 0; i < caveats.length; i++) {
const caveat = caveats[i];
if (!caveat) {
throw new Error(`Caveat was undefined at index ${i}`);
}
const caveatHash = getCaveatHash(caveat);
encoded.set(caveatHash, i * 32);
}

return keccak256(encoded);
}

/**
* Calculates the hash of a single caveat.
* @param caveat - The caveat to hash.
* @returns The keccak256 hash of the encoded caveat.
*/
function getCaveatHash(caveat: CaveatStruct): Uint8Array {
const termsBytes =
typeof caveat.terms === 'string' ? hexToBytes(caveat.terms) : caveat.terms;

const termsHash = keccak256(termsBytes);

const encoded = encode(
['bytes32', 'address', 'bytes32'],
[CAVEAT_TYPEHASH, caveat.enforcer, termsHash],
);
const hash = keccak256(encoded);
return hash;
}
10 changes: 9 additions & 1 deletion packages/delegation-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export type { Hex, Delegation, Caveat } from './types';
export type {
Hex,
DelegationStruct as Delegation,
CaveatStruct as Caveat,
} from './types';

export {
createValueLteTerms,
Expand All @@ -12,5 +16,9 @@ export {
export {
encodeDelegations,
decodeDelegations,
hashDelegation,
ROOT_AUTHORITY,
ANY_BENEFICIARY,
DELEGATION_TYPEHASH,
CAVEAT_TYPEHASH,
} from './delegation';
6 changes: 3 additions & 3 deletions packages/delegation-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type { Hex } from '@metamask/utils';
* @property terms - The terms or conditions of the caveat encoded as hex data.
* @property args - Additional arguments required by the caveat enforcer, encoded as hex data.
*/
export type Caveat<TBytes extends BytesLike = BytesLike> = {
export type CaveatStruct<TBytes extends BytesLike = BytesLike> = {
enforcer: TBytes;
terms: TBytes;
args: TBytes;
Expand All @@ -25,11 +25,11 @@ export type Caveat<TBytes extends BytesLike = BytesLike> = {
* @property salt - A unique value to prevent replay attacks and ensure uniqueness of the delegation.
* @property signature - The cryptographic signature validating this delegation.
*/
export type Delegation<TBytes extends BytesLike = BytesLike> = {
export type DelegationStruct<TBytes extends BytesLike = BytesLike> = {
delegate: TBytes;
delegator: TBytes;
authority: TBytes;
caveats: Caveat<TBytes>[];
caveats: CaveatStruct<TBytes>[];
salt: bigint;
signature: TBytes;
};
Loading
Loading