ox
Version:
663 lines • 22 kB
JavaScript
import * as AbiItem from '../core/AbiItem.js';
import * as Hash from '../core/Hash.js';
import * as Hex from '../core/Hex.js';
import * as Rlp from '../core/Rlp.js';
import * as SignatureEnvelope from './SignatureEnvelope.js';
import * as TempoAddress from './TempoAddress.js';
/**
* 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(authorization, options = {}) {
if ('keyId' in authorization)
return fromRpc(authorization);
const auth = authorization;
const resolved = {
...auth,
address: TempoAddress.resolve(auth.address),
...(auth.limits
? {
limits: auth.limits.map((l) => ({
...l,
token: TempoAddress.resolve(l.token),
})),
}
: {}),
...(auth.scopes
? {
scopes: auth.scopes.map((scope) => ({
...scope,
address: TempoAddress.resolve(scope.address),
selector: resolveSelector(scope.selector),
...(scope.recipients
? {
recipients: scope.recipients.map((r) => TempoAddress.resolve(r)),
}
: {}),
})),
}
: {}),
};
if (options.signature)
return {
...resolved,
signature: SignatureEnvelope.from(options.signature),
};
return resolved;
}
/**
* 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) {
const { allowedCalls, chainId, keyId, expiry, limits, keyType } = authorization;
const signature = SignatureEnvelope.fromRpc(authorization.signature);
// Unflatten nested allowedCalls into flat scopes
const scopes = allowedCalls
? allowedCalls.flatMap((callScope) => {
if (!callScope.selectorRules || callScope.selectorRules.length === 0)
return [{ address: callScope.target }];
return callScope.selectorRules.map((rule) => ({
address: callScope.target,
selector: normalizeSelector(rule.selector),
...(rule.recipients && rule.recipients.length > 0
? { recipients: rule.recipients }
: {}),
}));
})
: undefined;
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,
};
}
/**
* 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(tuple) {
const [authorization, signatureSerialized] = tuple;
const [chainId, keyType_hex, keyId, expiry, limits, scopes] = authorization;
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}`);
}
})();
const args = {
address: keyId,
expiry: typeof expiry !== 'undefined'
? hexToNumber(expiry) || undefined
: undefined,
type: keyType,
chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
...(typeof expiry !== 'undefined'
? { expiry: hexToNumber(expiry) || undefined }
: {}),
...(typeof limits !== 'undefined' &&
Array.isArray(limits) &&
limits.length > 0
? {
limits: limits.map((limitTuple) => {
const [token, limit, period] = limitTuple;
return {
token,
limit: hexToBigint(limit),
...(typeof period !== 'undefined'
? { period: hexToNumber(period) }
: {}),
};
}),
}
: {}),
...(typeof scopes !== 'undefined' && Array.isArray(scopes)
? {
scopes: scopes.flatMap((scopeTuple) => {
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) => {
const [selector, recipients] = ruleTuple;
return {
address,
selector,
...(Array.isArray(recipients) && recipients.length > 0
? { recipients }
: {}),
};
});
}),
}
: {}),
};
if (signatureSerialized)
args.signature = SignatureEnvelope.deserialize(signatureSerialized);
return from(args);
}
/**
* 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) {
return hash(authorization);
}
/**
* 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) {
const tuple = Rlp.toHex(serialized);
return fromTuple(tuple);
}
/**
* 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) {
const [authorizationTuple] = toTuple(authorization);
const serialized = Rlp.fromHex(authorizationTuple);
return Hash.keccak256(serialized);
}
/**
* 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) {
const tuple = toTuple(authorization);
return Rlp.fromHex(tuple);
}
/**
* 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: 44944627813007772897391531230081695102703289123332187696115181104739239197517n,
* s: 36528503505192438307355164441104001310566505351980369085208178712678799181120n,
* yParity: 0,
* },
* },
* })
* ```
*
* @param authorization - A Key Authorization.
* @returns An RPC-formatted Key Authorization.
*/
export function toRpc(authorization) {
const { address, scopes, chainId, expiry, limits, type, signature } = authorization;
// Group flat scopes by address into nested allowedCalls wire format
const allowedCalls = (() => {
if (!scopes)
return undefined;
const grouped = new Map();
for (const scope of scopes) {
const key = scope.address;
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]) => ({
target: target,
...(selectorRules.length > 0 ? { selectorRules } : {}),
}));
})();
return {
chainId: chainId === 0n ? '0x' : Hex.fromNumber(chainId),
expiry: typeof expiry === 'number' ? Hex.fromNumber(expiry) : null,
keyId: TempoAddress.resolve(address),
keyType: type,
limits: limits?.map(({ token, limit, period }) => ({
token,
limit: Hex.fromNumber(limit),
...(period ? { period: numberToHex(period) } : {}),
})),
signature: SignatureEnvelope.toRpc(signature),
...(allowedCalls ? { allowedCalls } : {}),
};
}
/**
* 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(authorization) {
const { address, chainId, scopes, expiry, limits } = authorization;
const signature = authorization.signature
? SignatureEnvelope.serialize(authorization.signature)
: undefined;
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 = [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();
for (const scope of scopes) {
const key = scope.address;
if (!grouped.has(key))
grouped.set(key, []);
if (scope.selector) {
grouped
.get(key)
.push([
resolveSelector(scope.selector),
(scope.recipients ??
[]),
]);
}
}
return [...grouped.entries()].map(([address, selectorRules]) => [
address,
selectorRules.map(([selector, recipients]) => [selector, recipients]),
]);
})();
const authorizationTuple = [
bigintToHex(chainId),
type,
address,
// expiry is required in the tuple when limits or scopes are present
// expiry=0 is treated the same as undefined (never expires)
(expiry !== null && expiry !== undefined && expiry !== 0) ||
limitsValue ||
callsValue
? numberToHex(expiry ?? 0)
: undefined,
// limits is required in the tuple when scopes are present
limitsValue || callsValue ? (limitsValue ?? []) : undefined,
callsValue,
].filter((x) => typeof x !== 'undefined');
return [authorizationTuple, ...(signature ? [signature] : [])];
}
function bigintToHex(value) {
return value === 0n ? '0x' : Hex.fromNumber(value);
}
function numberToHex(value) {
return value === 0 ? '0x' : Hex.fromNumber(value);
}
function hexToBigint(hex) {
return hex === '0x' ? 0n : BigInt(hex);
}
function hexToNumber(hex) {
return hex === '0x' ? 0 : Hex.toNumber(hex);
}
function normalizeSelector(selector) {
if (typeof selector === 'string')
return selector;
if (Array.isArray(selector))
return Hex.fromBytes(new Uint8Array(selector));
return selector;
}
function resolveSelector(selector) {
if (!selector)
return undefined;
if (selector.startsWith('0x'))
return selector;
return AbiItem.getSelector(selector);
}
//# sourceMappingURL=KeyAuthorization.js.map