UNPKG

viem

Version:

TypeScript Interface for Ethereum

351 lines 17.4 kB
import * as Hex from 'ox/Hex'; import { MultisigConfig, MultisigOperation, SignatureEnvelope, } from 'ox/tempo'; import { getCode } from '../actions/public/getCode.js'; import { getTransaction } from '../actions/public/getTransaction.js'; import { verifyHash } from '../actions/public/verifyHash.js'; import { maxUint256 } from '../constants/number.js'; import { isAddressEqual } from '../utils/address/isAddressEqual.js'; import { extendSchema } from '../utils/chain/defineChain.js'; import { defineTransaction } from '../utils/formatters/transaction.js'; import { defineTransactionReceipt } from '../utils/formatters/transactionReceipt.js'; import { defineTransactionRequest } from '../utils/formatters/transactionRequest.js'; import { getAction } from '../utils/getAction.js'; import { keccak256 } from '../utils/hash/keccak256.js'; import { getMetadata } from './actions/accessKey.js'; import { getConfig } from './actions/multisig.js'; import * as Formatters from './Formatters.js'; import * as Concurrent from './internal/concurrent.js'; import * as Transaction from './Transaction.js'; const maxExpirySecs = 25; // TODO: casting to satisfy viem – viem v3 to have more flexible serializer type. const serializeTransaction = ((transaction, signature) => Transaction.serialize(transaction, signature)); export const chainConfig = { blockTime: 1_000, extendSchema: extendSchema(), formatters: { transaction: defineTransaction({ exclude: ['aaAuthorizationList'], format: Formatters.formatTransaction, }), transactionReceipt: defineTransactionReceipt({ format: Formatters.formatTransactionReceipt, }), transactionRequest: defineTransactionRequest({ format: Formatters.formatTransactionRequest, }), }, prepareTransactionRequest: [ async (r, { client, phase }) => { const request = r; if (request.hash) { if (!request.account || typeof request.account === 'string' || request.account.source !== 'multisig') throw new Error('A local multisig account is required to approve a stored multisig transaction.'); if (!request.owner || typeof request.owner === 'string') throw new Error('A local owner account is required to approve a stored multisig transaction.'); if (request.owner.source !== 'root') throw new Error('A Tempo owner account is required to approve a stored multisig transaction.'); const transaction = await getAction(client, getTransaction, 'getTransaction')({ hash: request.hash }); const operation = 'multisig' in transaction ? transaction.multisig : undefined; if (!operation) throw new Error('Expected a multisig operation transaction.'); if (!isAddressEqual(operation.account, request.account.address)) throw new Error('Multisig operation account does not match the requested account.'); const storedTransaction = Transaction.deserialize(operation.transaction); const hash = MultisigOperation.getHash({ account: operation.account, config: operation.config, transaction: operation.transaction, type: 'transaction', }); if (hash.toLowerCase() !== request.hash.toLowerCase()) throw new Error('Multisig operation hash does not match transaction.'); return { ...storedTransaction, account: request.account, from: operation.account, multisigSimulation: getMultisigSimulation({ config: operation.config, local: request.account, }), owner: request.owner, }; } // FIXME: node estimates gas with secp256k1 dummy sig + null feePayerSignature. // Actual tx has larger keychain/webAuthn sigs + real fee payer sig, costing more intrinsic gas. if (phase === 'afterFillParameters') { // Fee payer signature covers the gas limit, so the relay must set it before signing and Viem must not change it afterward. if (typeof request.gas !== 'undefined' && request.feePayer && !request.feePayerSignature) { if (request.keyAuthorization?.signature.type === 'webAuthn') request.gas = (request.gas ?? 0n) + 20000n; else if (request.account?.source === 'accessKey') request.gas = (request.gas ?? 0n) + 10000n; } return request; } const multisigIdentity = (() => { if (request.account?.source === 'multisig') return { account: request.account.address, config: request.account.config, local: request.account, }; return undefined; })(); if (request.owner && !multisigIdentity) throw new Error('A multisig account is required when an owner is provided.'); if (request.owner && typeof request.owner === 'string') throw new Error('A local owner account is required to approve a multisig transaction.'); if (request.owner && request.owner.source !== 'root') throw new Error('A Tempo owner account is required to approve a multisig transaction.'); const coordinatedMultisig = !!multisigIdentity && !!request.owner && client.transport.multisig === true; if (multisigIdentity) { const { account, local } = multisigIdentity; if (!account) throw new Error('A multisig account address is required with a current config.'); const config = await (async () => { if (multisigIdentity.config) return multisigIdentity.config; if (!coordinatedMultisig) return undefined; const cachedConfig = await getAction(client, getConfig, 'getConfig')({ address: account }); if (!cachedConfig) throw new Error(`No current multisig config is cached for account ${account}. Provide the current config.`); return cachedConfig; })(); if (!config) throw new Error('A multisig config is required to prepare a transaction.'); request.from = account; request.multisigSimulation = getMultisigSimulation({ config, local, }); } // Register concurrency before account preparation performs storage or // network I/O so overlapping requests cannot miss each other. const useExpiringNonce = await (async () => { if (request.nonceKey === 'expiring' || request.nonceKey === maxUint256) return true; if (typeof request.nonceKey !== 'undefined') return false; if (multisigIdentity) return false; if (request.feePayer && typeof request.nonceKey === 'undefined') return true; const account = request.account; const address = typeof account === 'string' ? account : account?.address; if (address && typeof request.nonceKey === 'undefined') return await Concurrent.detect(address.toLowerCase()); return false; })(); if (coordinatedMultisig && typeof request.nonceKey === 'undefined') { // A random nonce lane lets a stored approval ceremony remain valid // indefinitely without blocking other pending operations. request.nonceKey = Hex.toBigInt(Hex.random(31)) + 1n; request.nonce = 0; } else if (useExpiringNonce) { request.nonceKey = maxUint256; request.nonce = 0; if (typeof request.validAfter === 'undefined') request.validAfter = randomValidAfter(); if (typeof request.validBefore === 'undefined') request.validBefore = Math.floor(Date.now() / 1000) + maxExpirySecs; } else if (typeof request.nonceKey !== 'undefined') { request.nonce = typeof request.nonce === 'number' ? request.nonce : 0; } if (!request.keyAuthorization && request.account?.source === 'accessKey') { const keyAuthorizationManager = request.account.keyAuthorizationManager; if (keyAuthorizationManager) { const chainId = request.chainId ?? request.chain?.id; if (typeof chainId !== 'undefined') { const address = request.account.address; const accessKey = request.account.accessKeyAddress; const key = { address, accessKey, chainId }; const keyAuthorization = await keyAuthorizationManager.get(key); if (keyAuthorization) { const now = BigInt(Math.floor(Date.now() / 1000)); if (keyAuthorization.expiry != null && BigInt(keyAuthorization.expiry) <= now) { await keyAuthorizationManager.remove(key); } else { const metadata = await getAction(client, getMetadata, 'getMetadata')({ account: address, accessKey }); if (isAddressEqual(metadata.address, accessKey) && !metadata.isRevoked && metadata.expiry > now) await keyAuthorizationManager.remove(key); else request.keyAuthorization = keyAuthorization; } } } } } if (!request.feeToken && request.chain?.feeToken) request.feeToken = request.chain.feeToken; return request; }, { runAt: ['beforeFillTransaction', 'afterFillParameters'] }, ], serializers: { transaction: serializeTransaction, async transactionEnvelope({ serializedTransaction, transaction }) { const request = transaction; if (!request.multisigSimulation) return serializedTransaction; try { SignatureEnvelope.deserialize(serializedTransaction); } catch { const transaction = Transaction.deserialize(serializedTransaction); if (transaction.signature?.type === 'multisig') return serializedTransaction; throw new Error('A Tempo owner account is required to approve a multisig transaction.'); } return await serializeTransaction({ ...request, signatures: [...(request.signatures ?? []), serializedTransaction], }); }, }, async verifyHash(client, parameters) { const { address, hash, signature, mode } = parameters; const envelope = (() => { if (typeof signature !== 'string') return; try { return SignatureEnvelope.deserialize(signature); } catch { return undefined; } })(); // `verifyHash` supports "signature envelopes" (a Tempo proposal) to natively verify arbitrary // envelope-compatible (WebAuthn, P256, etc.) signatures. if (envelope) { // Access key (keychain) signature verification: check the key is // authorized, not expired, and not revoked on the AccountKeychain. if (envelope?.type === 'keychain' && mode === 'allowAccessKey') { // For v2 keychain envelopes, the inner signature signs // keccak256(0x04 || hash || userAddress). const innerPayload = envelope.version === 'v2' ? keccak256(Hex.concat('0x04', hash, address)) : hash; const accessKeyAddress = (() => { try { return SignatureEnvelope.extractAddress({ payload: innerPayload, signature: envelope.inner, }); } catch { return undefined; } })(); if (!accessKeyAddress) return false; const keyInfo = await getMetadata(client, { account: address, accessKey: accessKeyAddress, blockHash: parameters.blockHash, blockNumber: parameters.blockNumber, blockTag: parameters.blockTag, requireCanonical: parameters.requireCanonical, }); if (keyInfo.isRevoked) return false; if (keyInfo.expiry <= BigInt(Math.floor(Date.now() / 1000))) return false; return SignatureEnvelope.verify(envelope.inner, { address: accessKeyAddress, payload: innerPayload, }); } // Stateless, non-keychain signature envelopes (P256, WebAuthn) can be // verified directly without a network request. if (envelope.type === 'p256' || envelope.type === 'webAuthn') { const code = await getCode(client, { address, blockHash: parameters.blockHash, blockNumber: parameters.blockNumber, blockTag: parameters.blockTag, requireCanonical: parameters.requireCanonical, }); // Check if EOA, if not, we want to go down the ERC-1271 flow. if ( // not a contract (EOA) !code || // default delegation (tempo EOA) code === '0xef01007702c00000000000000000000000000000000000') return SignatureEnvelope.verify(envelope, { address, payload: hash, }); } } return await getAction(client, verifyHash, 'verifyHash')({ ...parameters, chain: null }); }, }; /** Builds a bounded multisig spec for gas simulation. */ function getMultisigSimulation(options) { const { config, local } = options; return { approvals: selectOwners(config).map((owner) => { const localOwner = local?.owners.find((account) => isAddressEqual(account.address, owner.owner)); return { ...getSimulationKey(localOwner), owner: owner.owner, }; }), config, }; } /** Returns signature metadata for conservative gas estimation. */ function getSimulationKey(account) { const keyType = (() => { if (account && typeof account === 'object' && 'keyType' in account && typeof account.keyType === 'string') return account.keyType; return undefined; })(); if (!keyType || keyType === 'webAuthn') return { keyData: '0x0578', keyType: 'webAuthn' }; if (keyType === 'p256' || keyType === 'secp256k1') return { keyType }; return { keyData: '0x0578', keyType: 'webAuthn' }; } /** Returns random past seconds to distinguish otherwise-identical expiring transactions. */ function randomValidAfter() { const now = BigInt(Math.floor(Date.now() / 1_000)); const latest = now - 60n; if (latest <= 0n) return 0; return Number(BigInt(Hex.random(8)) % latest); } /** Selects a deterministic minimum-cardinality owner quorum. */ function selectOwners(config) { const owners = [...config.owners].sort((a, b) => Number(b.weight) - Number(a.weight) || a.owner.toLowerCase().localeCompare(b.owner.toLowerCase())); const selected = []; let weight = 0; for (const owner of owners.slice(0, MultisigConfig.maxSignatures)) { if (weight >= Number(config.threshold)) break; selected.push(owner); weight += Number(owner.weight); } return selected.sort((a, b) => a.owner.toLowerCase().localeCompare(b.owner.toLowerCase())); } //# sourceMappingURL=chainConfig.js.map