ox
Version:
Ethereum Standard Library
1,232 lines (1,174 loc) • 38.9 kB
text/typescript
import * as AbiItem from '../core/AbiItem.js'
import type * as Address from '../core/Address.js'
import type * as Errors from '../core/Errors.js'
import * as Hash from '../core/Hash.js'
import * as Hex from '../core/Hex.js'
import * as Quantity from '../core/internal/quantity.js'
import type { Compute, OneOf, UnionPartialBy } from '../core/internal/types.js'
import * as Rlp from '../core/Rlp.js'
import * as SignatureEnvelope from './SignatureEnvelope.js'
/**
* Key authorization for provisioning access keys.
*
* Access keys allow a root key (e.g., a passkey) to delegate transaction signing to secondary
* keys with customizable permissions including expiry timestamps and per-TIP-20 token spending
* limits. This enables a user to sign transactions without repeated passkey prompts.
*
* The root key signs a `KeyAuthorization` to grant an access key permission to sign transactions
* on its behalf. The authorization is attached to a transaction (which can be signed by the access
* key itself), and the protocol validates the authorization before storing the key in the
* AccountKeychain precompile.
*
* Key authorization fields:
* - `address`: Address derived from the access key's public key (the "key ID")
* - `chainId`: Chain ID for replay protection (0 = valid on any chain)
* - `expiry`: Unix timestamp when the key expires (undefined = never expires)
* - `limits`: Per-TIP-20 token spending limits (only applies to `transfer()` and `approve()` calls)
* - `type`: Key type (`secp256k1`, `p256`, or `webAuthn`)
*
* [Access Keys Specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#access-keys)
*/
export type KeyAuthorization<
signed extends boolean = boolean,
bigintType = bigint,
numberType = number,
> = {
/** Address derived from the public key of the key type. */
address: Address.Address
/** Chain ID for replay protection. */
chainId: bigintType
/** Unix timestamp when key expires (undefined = never expires). */
expiry?: numberType | null | undefined
/** TIP20 spending limits for this key. */
limits?: readonly TokenLimit<bigintType, numberType>[] | undefined
/**
* Call scopes restricting which contracts/selectors this key can call.
*
* - `undefined` = unrestricted key (any call allowed)
* - `[]` = scoped mode with no calls allowed
* - `[...]` = only listed contract+selector combinations allowed
*/
scopes?: readonly Scope[] | undefined
/** Key type. (secp256k1, P256, WebAuthn). */
type: SignatureEnvelope.Type
/**
* Optional 32-byte witness bound into the signing hash.
*
* Applications use this to bind a single signature to an arbitrary offchain
* context (e.g. a server-issued challenge), or as a revocation handle that
* can be burned onchain to invalidate the authorization before submission.
*
* [TIP-1053 Specification](https://tips.sh/1053)
*/
witness?: Hex.Hex | undefined
} & OneOf<
// TIP-1049 admin access keys: `account` and `isAdmin` are paired — either
// both are specified or neither. The `account` binding scopes the signing
// hash to a specific account, and `isAdmin: true` provisions an admin
// access key with unrestricted keychain mutator privileges.
//
// [TIP-1049 Specification](https://tips.sh/1049)
| {
/** Account address this authorization is bound to. */
account: Address.Address
/** Whether this authorization provisions an admin access key. */
isAdmin: boolean
}
| {}
> &
(signed extends true
? { signature: SignatureEnvelope.Primitive<numberType> }
: {
signature?: SignatureEnvelope.Primitive<numberType> | undefined
})
/** Input type for a Key Authorization. */
export type Input = KeyAuthorization<false, bigint, number>
/** RPC representation matching the node's wire format. */
export type Rpc = {
/** Optional account address binding (TIP-1049). */
account?: Address.Address | null | undefined
/** Allowed call scopes (node field: `allowedCalls`). */
allowedCalls?: readonly RpcCallScope[] | null | undefined
/** Chain ID (hex quantity). */
chainId: Hex.Hex
/** Expiry timestamp (hex quantity or null). */
expiry: Hex.Hex | null | undefined
/** Whether this authorization provisions an admin access key (TIP-1049). */
isAdmin?: boolean | null | undefined
/** Key identifier. */
keyId: Address.Address
/** Key type. */
keyType: SignatureEnvelope.Type
/** Token spending limits. */
limits?: readonly RpcTokenLimit[] | null | undefined
/** Primitive signature authorizing this key. */
signature: SignatureEnvelope.PrimitiveRpc
/** Optional 32-byte witness (hex). */
witness?: Hex.Hex | null | undefined
}
/** RPC representation of a token limit (matches node's `TokenLimit` serde). */
export type RpcTokenLimit = {
token: Address.Address
limit: Hex.Hex
period?: Hex.Hex | null | undefined
}
/** RPC representation of a call scope (matches node's `CallScope` serde). */
export type RpcCallScope = {
target: Address.Address
selectorRules?: readonly RpcSelectorRule[] | null | undefined
}
/** RPC representation of a selector rule (matches node's `SelectorRule` serde). */
export type RpcSelectorRule = {
selector: Hex.Hex
recipients?: readonly Address.Address[] | null | undefined
}
/** Signed representation of a Key Authorization. */
export type Signed<bigintType = bigint, numberType = number> = KeyAuthorization<
true,
bigintType,
numberType
>
type PrimitiveSignatureValue =
| UnionPartialBy<SignatureEnvelope.Primitive, 'prehash' | 'type'>
| SignatureEnvelope.Secp256k1Flat
| SignatureEnvelope.Serialized
type BaseTuple = readonly [
chainId: Hex.Hex,
keyType: Hex.Hex,
keyId: Address.Address,
]
type TokenLimitTuple =
| readonly [token: Address.Address, limit: Hex.Hex]
| readonly [token: Address.Address, limit: Hex.Hex, period: Hex.Hex]
type SelectorRuleTuple = readonly [
selector: Hex.Hex,
recipients: readonly Address.Address[],
]
type CallScopeTuple = readonly [
target: Address.Address,
selectorRules: readonly SelectorRuleTuple[],
]
type AuthorizationTuple =
| BaseTuple
| readonly [...BaseTuple, expiry: Hex.Hex]
| readonly [...BaseTuple, expiry: Hex.Hex, limits: readonly TokenLimitTuple[]]
| readonly [
...BaseTuple,
expiry: Hex.Hex,
limits: readonly TokenLimitTuple[],
calls: readonly CallScopeTuple[],
]
| readonly [
...BaseTuple,
expiry: Hex.Hex,
limits: readonly TokenLimitTuple[],
calls: readonly CallScopeTuple[],
witness: Hex.Hex,
]
| readonly [
...BaseTuple,
expiry: Hex.Hex,
limits: readonly TokenLimitTuple[],
calls: readonly CallScopeTuple[],
witness: Hex.Hex,
isAdmin: Hex.Hex,
]
| readonly [
...BaseTuple,
expiry: Hex.Hex,
limits: readonly TokenLimitTuple[],
calls: readonly CallScopeTuple[],
witness: Hex.Hex,
isAdmin: Hex.Hex,
account: Address.Address,
]
/** Tuple representation of a Key Authorization. */
export type Tuple<signed extends boolean = boolean> = signed extends true
? readonly [authorization: AuthorizationTuple, signature: Hex.Hex]
: readonly [authorization: AuthorizationTuple]
/**
* Call scope entry restricting which contract, selector, and recipients an access key can use.
*
* Multiple entries with the same `address` are grouped by target on the wire.
*
* - `{ address }` = any selector on this contract
* - `{ address, selector }` = specific selector
* - `{ address, selector, recipients }` = selector + recipient constraint
*
* [TIP-1011 Specification](https://docs.tempo.xyz/protocol/transactions/tip-1011)
*/
export type Scope = {
/** Target contract address. */
address: Address.Address
/**
* 4-byte function selector, or a human-readable ABI signature
* (e.g. `'transfer(address,uint256)'` or `'function transfer(address,uint256)'`).
*
* Signatures are encoded into a 4-byte selector automatically.
* Omit to allow any selector on this contract.
*/
selector?: Hex.Hex | string | undefined
/**
* Recipient allowlist for this selector (first ABI `address` argument).
*
* - `undefined` or `[]` = any recipient allowed
* - `[...]` = only listed recipients allowed
*
* Only valid for constrained selectors: `transfer`, `approve`, `transferWithMemo`.
*/
recipients?: readonly Address.Address[] | undefined
}
/**
* Token spending limit for access keys.
*
* Defines a per-TIP-20 token spending limit for an access key. Limits deplete as tokens
* are spent and can be updated by the root key via `updateSpendingLimit()`.
*
* [Access Keys Specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#access-keys)
*/
export type TokenLimit<bigintType = bigint, numberType = number> = {
/** Address of the TIP-20 token. */
token: Address.Address
/** Maximum spending amount for this token (enforced over the key's lifetime, or per period if `period` \> 0). */
limit: bigintType
/**
* Period duration in seconds for recurring spending limits.
*
* - `0` or `undefined` = one-time limit
* - `\> 0` = periodic limit that resets every `period` seconds
*/
period?: numberType | undefined
}
/**
* Converts a Key Authorization object into a typed {@link ox#KeyAuthorization.KeyAuthorization}.
*
* Use this to create an unsigned key authorization, then sign it with the root key using
* {@link ox#KeyAuthorization.(getSignPayload:function)} and attach the signature. The signed authorization
* can be included in a {@link ox#TxEnvelopeTempo.TxEnvelopeTempo} via the
* `keyAuthorization` field to provision the access key on-chain.
*
* [Access Keys Specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#access-keys)
*
* @example
* ### Secp256k1 Key
*
* Standard Ethereum ECDSA key using the secp256k1 curve.
*
* ```ts twoslash
* import { Address, Secp256k1, Value } from 'ox'
* import { KeyAuthorization } from 'ox/tempo'
*
* const privateKey = Secp256k1.randomPrivateKey()
* const address = Address.fromPublicKey(
* Secp256k1.getPublicKey({ privateKey })
* )
*
* const authorization = KeyAuthorization.from({
* address,
* chainId: 4217n,
* expiry: 1234567890,
* type: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
* ```
*
* @example
* ### WebCryptoP256 Key
*
* ```ts twoslash
* import { Address, WebCryptoP256, Value } from 'ox'
* import { KeyAuthorization } from 'ox/tempo'
*
* const keyPair = await WebCryptoP256.createKeyPair()
* const address = Address.fromPublicKey(keyPair.publicKey)
*
* const authorization = KeyAuthorization.from({
* address,
* chainId: 4217n,
* expiry: 1234567890,
* type: 'p256',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
* ```
*
* @example
* ### Attaching Signatures (Secp256k1)
*
* Attach a signature to a Key Authorization using a Secp256k1 private key to
* authorize another Secp256k1 key on the account.
*
* ```ts twoslash
* import { Address, Secp256k1, Value } from 'ox'
* import { KeyAuthorization } from 'ox/tempo'
*
* const privateKey = '0x...'
* const address = Address.fromPublicKey(
* Secp256k1.getPublicKey({ privateKey })
* )
*
* const authorization = KeyAuthorization.from({
* address,
* chainId: 4217n,
* expiry: 1234567890,
* type: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
*
* const rootPrivateKey = '0x...'
* const signature = Secp256k1.sign({
* payload: KeyAuthorization.getSignPayload(authorization),
* privateKey: rootPrivateKey
* })
*
* const authorization_signed = KeyAuthorization.from(
* authorization,
* { signature }
* )
* ```
*
* @example
* ### Attaching Signatures (WebAuthn)
*
* Attach a signature to a Key Authorization using a WebAuthn credential to
* authorize a new WebCryptoP256 key on the account.
*
* ```ts twoslash
* // @noErrors
* import {
* Address,
* Value,
* WebCryptoP256,
* WebAuthnP256
* } from 'ox'
* import {
* KeyAuthorization,
* SignatureEnvelope
* } from 'ox/tempo'
*
* const keyPair = await WebCryptoP256.createKeyPair()
* const address = Address.fromPublicKey(keyPair.publicKey)
*
* const authorization = KeyAuthorization.from({
* address,
* chainId: 4217n,
* expiry: 1234567890,
* type: 'p256',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
*
* const credential = await WebAuthnP256.createCredential({
* name: 'Example'
* })
*
* const { metadata, signature } = await WebAuthnP256.sign({
* challenge: KeyAuthorization.getSignPayload(authorization),
* credentialId: credential.id
* })
*
* const signatureEnvelope = SignatureEnvelope.from({
* // [!code focus]
* signature, // [!code focus]
* publicKey: credential.publicKey, // [!code focus]
* metadata // [!code focus]
* })
* const authorization_signed = KeyAuthorization.from(
* authorization,
* { signature: signatureEnvelope } // [!code focus]
* )
* ```
*
* @param authorization - A Key Authorization tuple in object format.
* @param options - Key Authorization options.
* @returns The {@link ox#KeyAuthorization.KeyAuthorization}.
*/
export function from<
const authorization extends Input | Rpc,
const signature extends PrimitiveSignatureValue | undefined = undefined,
>(
authorization: authorization | KeyAuthorization,
options: from.Options<signature> = {},
): from.ReturnType<authorization, signature> {
if ('keyId' in authorization) return fromRpc(authorization as Rpc) as never
const auth = authorization as KeyAuthorization & {
limits?: readonly { token: Address.Address; limit: bigint }[]
scopes?: readonly {
address: Address.Address
selector?: Hex.Hex | string
recipients?: readonly Address.Address[]
}[]
}
if (auth.witness !== undefined) assertWitness(auth.witness)
if (auth.signature) assertSignature(auth.signature)
const resolved = {
...auth,
...(auth.scopes
? {
scopes: auth.scopes.map((scope) => ({
...scope,
selector: resolveSelector(scope.selector),
})),
}
: {}),
}
if (options.signature) {
const signature = SignatureEnvelope.from(
options.signature,
) as SignatureEnvelope.SignatureEnvelope
assertSignature(signature)
return {
...resolved,
signature,
} as never
}
return resolved as never
}
export declare namespace from {
type Options<
signature extends PrimitiveSignatureValue | undefined =
| PrimitiveSignatureValue
| undefined,
> = {
/** The primitive signature to attach to the Key Authorization. */
signature?: signature | SignatureEnvelope.Primitive | undefined
}
type ReturnType<
authorization extends KeyAuthorization | Input | Rpc = KeyAuthorization,
signature extends PrimitiveSignatureValue | undefined =
| PrimitiveSignatureValue
| undefined,
> = Compute<
authorization extends Rpc
? Signed
: authorization &
(signature extends PrimitiveSignatureValue
? {
signature: Extract<
SignatureEnvelope.from.ReturnValue<signature>,
SignatureEnvelope.Primitive
>
}
: {})
>
type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
}
/**
* Converts an {@link ox#AuthorizationTempo.Rpc} to an {@link ox#AuthorizationTempo.AuthorizationTempo}.
*
* @example
* ```ts twoslash
* import { KeyAuthorization } from 'ox/tempo'
*
* const keyAuthorization = KeyAuthorization.fromRpc({
* chainId: '0x1079',
* expiry: '0x174876e800',
* keyId: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* keyType: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: '0xf4240'
* }
* ],
* signature: {
* type: 'secp256k1',
* r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d',
* s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540',
* yParity: '0x0'
* }
* })
* ```
*
* @param authorization - The RPC-formatted Key Authorization.
* @returns A signed {@link ox#AuthorizationTempo.AuthorizationTempo}.
*/
export function fromRpc(authorization: Rpc): Signed {
const { allowedCalls, chainId, keyId, expiry, limits, keyType } =
authorization
const witness = authorization.witness ?? undefined
const isAdmin = authorization.isAdmin ?? undefined
const account = authorization.account ?? undefined
const signature = SignatureEnvelope.fromRpc(authorization.signature)
assertSignature(signature)
if (witness !== undefined) assertWitness(witness)
// Unflatten nested allowedCalls into flat scopes
const scopes = allowedCalls
? allowedCalls.flatMap((callScope) => {
if (!callScope.selectorRules || callScope.selectorRules.length === 0)
return [{ address: callScope.target }] as Scope[]
return callScope.selectorRules.map(
(rule): Scope => ({
address: callScope.target,
selector: normalizeSelector(rule.selector),
...(rule.recipients && rule.recipients.length > 0
? { recipients: rule.recipients }
: {}),
}),
)
})
: undefined
// TIP-1049 admin fields are paired: emit both or neither; orphan wire
// fields are dropped. Separate conditional spreads break `Signed`
// assignability without `exactOptionalPropertyTypes` (#290).
const adminPair =
account !== undefined && isAdmin ? { account, isAdmin: true as const } : {}
return {
address: keyId,
chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
...(expiry != null ? { expiry: Number(expiry) } : {}),
limits: limits?.map((limit) => ({
token: limit.token,
limit: BigInt(limit.limit),
...(limit.period && hexToNumber(limit.period) > 0
? { period: hexToNumber(limit.period) }
: {}),
})),
...(scopes ? { scopes } : {}),
signature,
type: keyType,
...(witness !== undefined ? { witness } : {}),
...adminPair,
}
}
export declare namespace fromRpc {
type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
}
/**
* Converts an {@link ox#KeyAuthorization.Tuple} to an {@link ox#KeyAuthorization.KeyAuthorization}.
*
* @example
* ```ts twoslash
* import { KeyAuthorization } from 'ox/tempo'
*
* const authorization = KeyAuthorization.fromTuple([
* [
* '0x',
* '0x00',
* '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* '0x174876e800',
* [
* [
* '0x20c0000000000000000000000000000000000001',
* '0xf4240'
* ]
* ]
* ],
* '0x01a068a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b907e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064'
* ])
* ```
*
* @example
* Unsigned Key Authorization tuple (no signature):
*
* ```ts twoslash
* import { KeyAuthorization } from 'ox/tempo'
*
* const authorization = KeyAuthorization.fromTuple([
* [
* '0x',
* '0x00',
* '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* '0x174876e800',
* [
* [
* '0x20c0000000000000000000000000000000000001',
* '0xf4240'
* ]
* ]
* ]
* ])
* ```
*
* @param tuple - The Key Authorization tuple.
* @returns The {@link ox#KeyAuthorization.KeyAuthorization}.
*/
export function fromTuple<const tuple extends Tuple>(
tuple: tuple,
): fromTuple.ReturnType<tuple> {
const [authorization, signatureSerialized] = tuple
const [chainId, keyType_hex, keyId, ...trailing] =
authorization as unknown as [
Hex.Hex,
Hex.Hex,
Address.Address,
...unknown[],
]
const keyType = (() => {
switch (keyType_hex) {
case '0x':
case '0x00':
return 'secp256k1'
case '0x01':
return 'p256'
case '0x02':
return 'webAuthn'
default:
throw new Error(`Invalid key type: ${keyType_hex}`)
}
})()
// Trailing optional fields in wire order. Each entry pulls one slot off the
// trailing array and decodes it (treating absent or RLP-null placeholders as
// missing). To add a new optional trailing field, append a single entry.
const [rawExpiry, rawLimits, rawScopes, rawWitness, rawIsAdmin, rawAccount] =
trailing
const expiry = isAbsent(rawExpiry)
? undefined
: hexToNumber(rawExpiry as Hex.Hex) || undefined
const limits =
Array.isArray(rawLimits) && rawLimits.length > 0
? rawLimits.map((limitTuple: any) => {
const [token, limit, period] = limitTuple
return {
token,
limit: hexToBigint(limit),
...(period !== undefined ? { period: hexToNumber(period) } : {}),
}
})
: undefined
const scopes = Array.isArray(rawScopes)
? rawScopes.flatMap((scopeTuple: any) => {
const [address, selectorRules] = scopeTuple
// If no selector rules, this is an address-only scope.
if (!Array.isArray(selectorRules) || selectorRules.length === 0)
return [{ address }]
// Flatten each selector rule into a separate scope entry.
return selectorRules.map((ruleTuple: any) => {
const [selector, recipients] = ruleTuple
return {
address,
selector,
...(Array.isArray(recipients) && recipients.length > 0
? { recipients }
: {}),
}
})
})
: undefined
const witness = isAbsent(rawWitness) ? undefined : (rawWitness as Hex.Hex)
if (witness !== undefined) assertWitness(witness)
const isAdmin = (() => {
if (isAbsent(rawIsAdmin)) return undefined
// TIP-1049: the admin marker is strictly `0x01`. Any other value is a
// protocol-level decode error on the node, so reject it here too.
if (rawIsAdmin !== '0x01')
throw new InvalidAdminMarkerError(rawIsAdmin as Hex.Hex)
return true
})()
const account = isAbsent(rawAccount)
? undefined
: (rawAccount as Address.Address)
// TIP-1049 admin fields are paired: only emit both when both are present on
// the wire. Wire shapes carrying only one are tolerated for forward-compat
// but the orphan field is dropped (since the public API requires both).
const adminPair =
account !== undefined && isAdmin ? { account, isAdmin: true as const } : {}
const args: KeyAuthorization = {
address: keyId,
chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
type: keyType,
...(expiry !== undefined ? { expiry } : {}),
...(limits !== undefined ? { limits } : {}),
...(scopes !== undefined ? { scopes } : {}),
...(witness !== undefined ? { witness } : {}),
...adminPair,
}
if (signatureSerialized) {
const signature = SignatureEnvelope.deserialize(signatureSerialized)
assertSignature(signature)
args.signature = signature
}
return from(args) as never
}
export declare namespace fromTuple {
type ReturnType<authorization extends Tuple = Tuple> = Compute<
KeyAuthorization<authorization extends Tuple<true> ? true : false>
>
type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
}
/**
* Computes the sign payload for an {@link ox#KeyAuthorization.KeyAuthorization}.
*
* The root key must sign this payload to authorize the access key. The resulting signature
* is attached to the key authorization via {@link ox#KeyAuthorization.(from:function)} with the
* `signature` option.
*
* [Access Keys Specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#access-keys)
*
* @example
* ```ts twoslash
* import { Address, Secp256k1, Value } from 'ox'
* import { KeyAuthorization } from 'ox/tempo'
*
* const privateKey = '0x...'
* const address = Address.fromPublicKey(
* Secp256k1.getPublicKey({ privateKey })
* )
*
* const authorization = KeyAuthorization.from({
* address,
* chainId: 4217n,
* expiry: 1234567890,
* type: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
*
* const payload =
* KeyAuthorization.getSignPayload(authorization) // [!code focus]
* ```
*
* @param authorization - The {@link ox#KeyAuthorization.KeyAuthorization}.
* @returns The sign payload.
*/
export function getSignPayload(authorization: KeyAuthorization): Hex.Hex {
return hash(authorization)
}
export declare namespace getSignPayload {
type ErrorType = hash.ErrorType | Errors.GlobalErrorType
}
/**
* Deserializes an RLP-encoded {@link ox#KeyAuthorization.KeyAuthorization}.
*
* @example
* ```ts twoslash
* import { KeyAuthorization } from 'ox/tempo'
* import { Value } from 'ox'
*
* const authorization = KeyAuthorization.from({
* address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* chainId: 4217n,
* expiry: 1234567890,
* type: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
*
* const serialized = KeyAuthorization.serialize(authorization)
* const deserialized =
* KeyAuthorization.deserialize(serialized) // [!code focus]
* ```
*
* @param serialized - The RLP-encoded Key Authorization.
* @returns The {@link ox#KeyAuthorization.KeyAuthorization}.
*/
export function deserialize(serialized: Hex.Hex): KeyAuthorization {
const tuple = Rlp.toHex(serialized) as unknown as Tuple
return fromTuple(tuple)
}
export declare namespace deserialize {
type ErrorType =
| Rlp.toHex.ErrorType
| fromTuple.ErrorType
| Errors.GlobalErrorType
}
/**
* Computes the hash for an {@link ox#KeyAuthorization.KeyAuthorization}.
*
* @example
* ```ts twoslash
* import { KeyAuthorization } from 'ox/tempo'
* import { Value } from 'ox'
*
* const authorization = KeyAuthorization.from({
* address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* chainId: 4217n,
* expiry: 1234567890,
* type: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
*
* const hash = KeyAuthorization.hash(authorization) // [!code focus]
* ```
*
* @param authorization - The {@link ox#KeyAuthorization.KeyAuthorization}.
* @returns The hash.
*/
export function hash(authorization: KeyAuthorization): Hex.Hex {
const [authorizationTuple] = toTuple(authorization)
const serialized = Rlp.fromHex(authorizationTuple)
return Hash.keccak256(serialized)
}
export declare namespace hash {
type ErrorType =
| toTuple.ErrorType
| Hash.keccak256.ErrorType
| Hex.concat.ErrorType
| Rlp.fromHex.ErrorType
| Errors.GlobalErrorType
}
/**
* Serializes a {@link ox#KeyAuthorization.KeyAuthorization} to RLP-encoded hex.
*
* @example
* ```ts twoslash
* import { KeyAuthorization } from 'ox/tempo'
* import { Value } from 'ox'
*
* const authorization = KeyAuthorization.from({
* address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* chainId: 4217n,
* expiry: 1234567890,
* type: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
*
* const serialized = KeyAuthorization.serialize(authorization) // [!code focus]
* ```
*
* @param authorization - The {@link ox#KeyAuthorization.KeyAuthorization}.
* @returns The RLP-encoded Key Authorization.
*/
export function serialize(authorization: KeyAuthorization): Hex.Hex {
const tuple = toTuple(authorization)
return Rlp.fromHex(tuple as any)
}
export declare namespace serialize {
type ErrorType =
| toTuple.ErrorType
| Rlp.fromHex.ErrorType
| Errors.GlobalErrorType
}
/**
* Converts an {@link ox#KeyAuthorization.KeyAuthorization} to an {@link ox#KeyAuthorization.Rpc}.
*
* @example
* ```ts twoslash
* import { KeyAuthorization } from 'ox/tempo'
* import { Value } from 'ox'
*
* const authorization = KeyAuthorization.toRpc({
* address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* chainId: 4217n,
* expiry: 1234567890,
* type: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ],
* signature: {
* type: 'secp256k1',
* signature: {
* r: '0x635dc2033e60185bb36709c29c75d64ea51dfbd91c32ef4be198e4ceb169fb4d',
* s: '0x50c2667ac4c771072746acfdcf1f1483336dcca8bd2df47cd83175dbe60f0540',
* yParity: 0
* }
* }
* })
* ```
*
* @param authorization - A Key Authorization.
* @returns An RPC-formatted Key Authorization.
*/
export function toRpc(authorization: toRpc.Input): Rpc {
const {
address,
scopes,
chainId,
expiry,
limits,
type,
signature,
witness,
isAdmin,
account,
} = authorization
assertSignature(signature)
if (witness !== undefined) assertWitness(witness)
// Group flat scopes by address into nested allowedCalls wire format
const allowedCalls = (() => {
if (!scopes) return undefined
const grouped = new Map<string, RpcSelectorRule[]>()
for (const scope of scopes) {
const key = scope.address as string
if (!grouped.has(key)) grouped.set(key, [])
if (scope.selector) {
grouped.get(key)!.push({
selector: resolveSelector(scope.selector)!,
...(scope.recipients && scope.recipients.length > 0
? { recipients: scope.recipients }
: {}),
})
}
}
return [...grouped.entries()].map(
([target, selectorRules]): RpcCallScope => ({
target: target as Address.Address,
...(selectorRules.length > 0 ? { selectorRules } : {}),
}),
)
})()
const chainIdHex = Quantity.fromNumberish(chainId)
return {
chainId: chainIdHex === '0x0' ? '0x' : chainIdHex,
expiry: expiry != null ? Quantity.fromNumberish(expiry) : null,
keyId: address,
keyType: type,
limits: limits?.map(({ token, limit, period }) => ({
token,
limit: Quantity.fromNumberish(limit),
...(period ? { period: Quantity.fromNumberish(period) } : {}),
})),
signature: SignatureEnvelope.toRpc(
signature,
) as SignatureEnvelope.PrimitiveRpc,
...(allowedCalls ? { allowedCalls } : {}),
...(witness !== undefined ? { witness } : {}),
...(isAdmin ? { isAdmin: true } : {}),
...(account !== undefined ? { account } : {}),
}
}
export declare namespace toRpc {
/** Numberish input accepted by {@link ox#KeyAuthorization.(toRpc:function)}. */
type Input = Signed<Hex.Hex | bigint | number, Hex.Hex | number>
type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
}
/**
* Converts an {@link ox#KeyAuthorization.KeyAuthorization} to an {@link ox#KeyAuthorization.Tuple}.
*
* @example
* ```ts twoslash
* import { KeyAuthorization } from 'ox/tempo'
* import { Value } from 'ox'
*
* const authorization = KeyAuthorization.from({
* address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* chainId: 4217n,
* expiry: 1234567890,
* type: 'secp256k1',
* limits: [
* {
* token: '0x20c0000000000000000000000000000000000001',
* limit: Value.from('10', 6)
* }
* ]
* })
*
* const tuple = KeyAuthorization.toTuple(authorization) // [!code focus]
* // @log: [
* // @log: '0x174876e800',
* // @log: [['0x20c0000000000000000000000000000000000001', '0xf4240']],
* // @log: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
* // @log: 'secp256k1',
* // @log: ]
* ```
*
* @param authorization - The {@link ox#KeyAuthorization.KeyAuthorization}.
* @returns A Tempo Key Authorization tuple.
*/
export function toTuple<const authorization extends KeyAuthorization>(
authorization: authorization,
): toTuple.ReturnType<authorization> {
const {
address,
chainId,
scopes,
expiry,
limits,
witness,
isAdmin,
account,
} = authorization
if (witness !== undefined) assertWitness(witness)
const signature = (() => {
if (!authorization.signature) return undefined
assertSignature(authorization.signature)
return SignatureEnvelope.serialize(authorization.signature)
})()
const type = (() => {
switch (authorization.type) {
case 'secp256k1':
return '0x'
case 'p256':
return '0x01'
case 'webAuthn':
return '0x02'
default:
throw new Error(`Invalid key type: ${authorization.type}`)
}
})()
const limitsValue = limits?.map((limit) => {
const tuple: any[] = [limit.token, bigintToHex(limit.limit)]
// Canonical: omit period when 0 (one-time limit)
if (limit.period && limit.period > 0) tuple.push(numberToHex(limit.period))
return tuple
})
// Group flat scopes by address for wire format
const callsValue = (() => {
if (!scopes) return undefined
const grouped = new Map<
string,
[Hex.Hex, (readonly Address.Address[])[]][]
>()
for (const scope of scopes) {
const key = scope.address as string
if (!grouped.has(key)) grouped.set(key, [])
if (scope.selector) {
grouped
.get(key)!
.push([
resolveSelector(scope.selector)!,
(scope.recipients ??
[]) as unknown as (readonly Address.Address[])[],
])
}
}
return [...grouped.entries()].map(([address, selectorRules]) => [
address,
selectorRules.map(([selector, recipients]) => [selector, recipients]),
])
})()
// Optional trailing fields in wire order. Each entry's `placeholder` is
// emitted when this field is skipped but a later field is present.
//
// Placeholder convention:
// - `'0x'` (RLP null) is the canonical placeholder for fields added at or
// after TIP-1053. The node decodes it as `None` (unrestricted).
// - `limits` keeps `[]` as its skipped placeholder for pre-TIP-1053 wire
// shapes — preserving byte-for-byte equivalence with signed payloads
// produced before TIP-1053 was added. When any TIP-1053+ field is
// present, the canonical `'0x'` placeholder is used instead.
//
// To add a new optional trailing field (e.g. from a future TIP): append a
// single entry to this list with `placeholder: '0x'`.
const hasTip1053Plus =
witness !== undefined || isAdmin || account !== undefined
const optionals: readonly { value: unknown; placeholder: unknown }[] = [
{
value:
expiry !== null && expiry !== undefined && expiry !== 0
? numberToHex(expiry)
: undefined,
placeholder: '0x',
},
{
value: limitsValue,
placeholder: hasTip1053Plus ? '0x' : [],
},
{ value: callsValue, placeholder: '0x' },
{ value: witness, placeholder: '0x' },
// TIP-1049: admin marker. Present = `0x01` (RLP integer 1); absent
// skipped or omitted. Any other value is a hard decode error on the node.
{ value: isAdmin ? '0x01' : undefined, placeholder: '0x' },
// TIP-1049: optional account binding. Last field — never a placeholder.
{ value: account, placeholder: '0x' },
]
let lastPresent = -1
for (let i = optionals.length - 1; i >= 0; i--)
if (optionals[i]!.value !== undefined) {
lastPresent = i
break
}
const trailing = optionals
.slice(0, lastPresent + 1)
.map(({ value, placeholder }) => value ?? placeholder)
const authorizationTuple = [bigintToHex(chainId), type, address, ...trailing]
return [authorizationTuple, ...(signature ? [signature] : [])] as never
}
export declare namespace toTuple {
type ReturnType<authorization extends KeyAuthorization = KeyAuthorization> =
Compute<Tuple<authorization extends KeyAuthorization<true> ? true : false>>
type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
}
function bigintToHex(value: bigint): Hex.Hex {
return value === 0n ? '0x' : Hex.fromNumber(value)
}
function numberToHex(value: number): Hex.Hex {
return value === 0 ? '0x' : Hex.fromNumber(value)
}
function hexToBigint(hex: Hex.Hex): bigint {
return hex === '0x' ? 0n : BigInt(hex)
}
function hexToNumber(hex: Hex.Hex): number {
return hex === '0x' ? 0 : Hex.toNumber(hex)
}
function normalizeSelector(selector: Hex.Hex | number[]): Hex.Hex {
if (typeof selector === 'string') return selector
if (Array.isArray(selector))
return Hex.fromBytes(new Uint8Array(selector)) as Hex.Hex
return selector
}
function resolveSelector(
selector: Hex.Hex | string | undefined,
): Hex.Hex | undefined {
if (!selector) return undefined
if (selector.startsWith('0x')) return selector as Hex.Hex
return AbiItem.getSelector(selector)
}
function assertWitness(witness: Hex.Hex): void {
if (Hex.size(witness) !== 32) throw new InvalidWitnessSizeError(witness)
}
function assertSignature<numberType>(
signature: SignatureEnvelope.SignatureEnvelope<numberType>,
): asserts signature is SignatureEnvelope.Primitive<numberType> {
if (signature.type === 'keychain' || signature.type === 'multisig')
throw new InvalidSignatureTypeError(signature.type)
}
function isAbsent(value: unknown): boolean {
return value === undefined || value === '0x'
}
/** Thrown when a `witness` field is not exactly 32 bytes. */
export class InvalidWitnessSizeError extends Error {
override readonly name = 'KeyAuthorization.InvalidWitnessSizeError'
constructor(witness: Hex.Hex) {
super(
`Witness \`${witness}\` must be exactly 32 bytes (got ${Hex.size(witness)} bytes).`,
)
}
}
/** Thrown when a TIP-1049 admin marker has any value other than `0x01`. */
export class InvalidAdminMarkerError extends Error {
override readonly name = 'KeyAuthorization.InvalidAdminMarkerError'
constructor(marker: Hex.Hex) {
super(
`Admin marker \`${marker}\` is invalid; expected \`0x01\` (TIP-1049).`,
)
}
}
/** Thrown when a key authorization contains a non-primitive signature. */
export class InvalidSignatureTypeError extends Error {
override readonly name = 'KeyAuthorization.InvalidSignatureTypeError'
constructor(type: SignatureEnvelope.SignatureEnvelope['type']) {
super(
`Signature type \`${type}\` is invalid for key authorizations; expected \`secp256k1\`, \`p256\`, or \`webAuthn\`.`,
)
}
}