viem
Version:
1,030 lines • 43.1 kB
JavaScript
import * as Address_ from 'ox/Address';
import * as Hash from 'ox/Hash';
import * as Hex from 'ox/Hex';
import * as RpcResponse from 'ox/RpcResponse';
import { KeyAuthorization, MultisigConfig, MultisigOperation, Transaction as ox_Transaction, SignatureEnvelope, TxEnvelopeTempo, } from 'ox/tempo';
import { getBlockNumber } from '../actions/public/getBlockNumber.js';
import { createClient } from '../clients/createClient.js';
import { custom } from '../clients/transports/custom.js';
import { decodeFunctionData } from '../utils/abi/decodeFunctionData.js';
import { isAddressEqual } from '../utils/address/isAddressEqual.js';
import * as Abis from './Abis.js';
import * as Addresses from './Addresses.js';
import { getConfigCommitment } from './actions/multisig.js';
import * as ConfigStore from './multisig/Config.js';
import * as OperationStore from './multisig/Operation.js';
import * as Transaction from './Transaction.js';
const submissionTtl = 30_000;
const pollingInterval = 100;
/**
* Creates an RPC request handler that coordinates native multisig approvals.
*
* @param next - Downstream RPC request handler.
* @param parameters - Handler parameters.
* @returns The multisig-aware RPC request handler.
*/
export function handleRequest(next, parameters) {
if (!parameters.store.compareAndSet)
throw new RpcResponse.InvalidParamsError({
message: 'Multisig coordination requires a store with atomic `compareAndSet`.',
});
return async (request, requestOptions_) => {
const requestOptions = await resolveRequestOptions({
request,
requestOptions: requestOptions_,
store: parameters.store,
});
const client = createClient({
transport: custom({
request: ({ method, params }, options) => next({ method, params }, { ...requestOptions, ...options }),
}),
});
if (request.method === 'multisig_getConfig') {
const value = request.params?.[0];
const address = value && typeof value === 'object' && 'address' in value
? value.address
: undefined;
if (typeof address !== 'string' ||
!Address_.validate(address) ||
Hex.toBigInt(address) === 0n)
throw new RpcResponse.InvalidParamsError({
message: 'Expected a multisig account address.',
});
const blockNumber = await getBlockNumber(client, { cacheTime: 0 });
const commitment = await getConfigCommitment(client, {
account: address,
blockNumber,
});
const config = await ConfigStore.read(parameters.store, {
address,
commitment,
});
if (!config)
return null;
return MultisigConfig.toRpc(config);
}
if (request.method === 'multisig_getOperation') {
const hash = request.params?.[0];
if (typeof hash !== 'string' || !Hash.validate(hash))
throw new RpcResponse.InvalidParamsError({
message: 'Expected a multisig operation hash.',
});
const operation = await OperationStore.read(parameters.store, hash);
return operation ? MultisigOperation.toRpc(operation) : null;
}
if (request.method === 'multisig_approveKeyAuthorization')
return await approveKeyAuthorization({
client,
request,
store: parameters.store,
});
if (request.method === 'eth_getTransactionByHash' ||
request.method === 'eth_getTransactionReceipt') {
const hash = request.params?.[0];
if (typeof hash !== 'string' || !Hash.validate(hash))
return await next(request, requestOptions);
const operation = await OperationStore.read(parameters.store, hash);
if (!operation || operation.type !== 'transaction')
return await next(request, requestOptions);
if (request.method === 'eth_getTransactionReceipt') {
if (operation.status === 'pending')
return null;
const transactionHash = await getSubmittedTransactionHash(parameters.store, operation);
if (!transactionHash)
return null;
const receipt = await next({
...request,
params: [transactionHash],
}, requestOptions);
if (!receipt || typeof receipt !== 'object')
return receipt;
const success = operation.status === 'submitting'
? await completeSubmission(parameters.store, operation, transactionHash)
: operation;
return {
...receipt,
multisig: MultisigOperation.toRpc(success),
};
}
if (operation.status === 'pending')
return await toTransaction(client, operation);
const transactionHash = await getSubmittedTransactionHash(parameters.store, operation);
if (!transactionHash)
return await toTransaction(client, operation);
const transaction = await next({
...request,
params: [transactionHash],
}, requestOptions);
if (!transaction || typeof transaction !== 'object')
return await toTransaction(client, operation);
const success = operation.status === 'submitting'
? await completeSubmission(parameters.store, operation, transactionHash)
: operation;
return {
...transaction,
multisig: MultisigOperation.toRpc(success),
};
}
if (request.method !== 'eth_sendRawTransaction' &&
request.method !== 'eth_sendRawTransactionSync' &&
request.method !== 'multisig_approveRawTransaction' &&
request.method !== 'multisig_approveRawTransactionSync')
return await next(request, requestOptions);
const standard = request.method === 'eth_sendRawTransaction' ||
request.method === 'eth_sendRawTransactionSync';
const serialized = request.params?.[0];
if (!isSerializedTempoTransaction(serialized)) {
if (standard)
return await next(request, requestOptions);
throw new RpcResponse.InvalidParamsError({
message: 'Expected a serialized Tempo multisig transaction.',
});
}
return await submit({
client,
method: request.method,
next,
request,
requestOptions,
serialized,
store: parameters.store,
});
};
}
/** Collects approvals and submits a transaction after quorum. @internal */
// biome-ignore lint/correctness/noUnusedVariables: _
async function submit(options) {
const transaction = (() => {
try {
return deserialize(options.serialized);
}
catch (error) {
if (options.method !== 'eth_sendRawTransaction' &&
options.method !== 'eth_sendRawTransactionSync')
throw error;
return undefined;
}
})();
const signature = transaction?.signature;
if (!transaction || signature?.type !== 'multisig') {
if (options.method === 'eth_sendRawTransaction' ||
options.method === 'eth_sendRawTransactionSync')
return await options.next(options.request, options.requestOptions);
throw new RpcResponse.InvalidParamsError({
message: 'Expected a multisig transaction signature.',
});
}
const { signature: _, ...unsigned } = transaction;
const envelope = TxEnvelopeTempo.from(unsigned);
const serializedUnsigned = serializeUnsigned(envelope, options.serialized.startsWith(TxEnvelopeTempo.feePayerMagic));
const blockNumber = await getBlockNumber(options.client, { cacheTime: 0 });
const config = MultisigConfig.from(signature.config);
const configCommitment = await validateConfig({
account: signature.account,
blockNumber,
client: options.client,
config,
});
const operationHash = MultisigOperation.getHash({
account: signature.account,
config,
transaction: serializedUnsigned,
type: 'transaction',
});
const incoming = signature.signatures.map((signature) => SignatureEnvelope.serialize(signature));
if (incoming.length === 0)
throw new RpcResponse.InvalidParamsError({
message: 'A multisig approval envelope must include a signature.',
});
const now = Date.now();
const expiredSubmissionIds = new Set();
const operation = await OperationStore.update(options.store, operationHash, async (existing) => {
if (existing && existing.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (existing?.status === 'success')
return existing;
if (existing?.status === 'submitting' && existing.expiresAt > now)
return existing;
if (existing?.status === 'submitting')
expiredSubmissionIds.add(existing.submissionId);
const existingApprovals = existing
? await selectApprovals({
account: signature.account,
config,
hash: operationHash,
approvals: existing.approvals,
})
: undefined;
const approvals = await selectApprovals({
account: signature.account,
config,
hash: operationHash,
approvals: [...(existingApprovals?.approvals ?? []), ...incoming],
});
return MultisigOperation.from({
account: signature.account,
approvals: approvals.approvals,
config,
createdAt: existing?.createdAt ?? now,
hash: operationHash,
signatureCount: approvals.signatureCount,
status: 'pending',
threshold: approvals.threshold,
transaction: mergeTransaction(existing?.transaction, serializedUnsigned),
type: 'transaction',
updatedAt: now,
weight: approvals.weight,
});
});
for (const submissionId of expiredSubmissionIds) {
if (operation.status === 'submitting' &&
operation.submissionId?.toLowerCase() === submissionId.toLowerCase())
continue;
await removeSettledSubmission(options.store, operationHash, submissionId);
}
await ConfigStore.write(options.store, {
address: signature.account,
commitment: configCommitment,
config,
});
await cacheNextConfigs({
account: signature.account,
config,
store: options.store,
transaction,
});
if (operation.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (operation.status === 'success')
return await submittedResult(options, operation);
if (operation.status === 'submitting' && operation.expiresAt > Date.now())
return await submittingResult(options, operation);
if (operation.weight < operation.threshold)
return pendingResult(options.method, operation);
const submissionId = Hex.random(32);
const timeout = options.request.params?.[1];
const synchronous = options.method === 'eth_sendRawTransactionSync' ||
options.method === 'multisig_approveRawTransactionSync';
const leaseTtl = synchronous &&
typeof timeout === 'number' &&
Number.isSafeInteger(timeout) &&
timeout >= 0
? timeout + submissionTtl
: submissionTtl;
const claim = await OperationStore.update(options.store, operationHash, (current) => {
if (!current || current.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (current.status === 'success')
return current;
if (current.status === 'submitting' && current.expiresAt > Date.now())
return current;
if (current.weight < current.threshold)
return current;
const now = Date.now();
return MultisigOperation.from({
...current,
expiresAt: Math.min(now + leaseTtl, Number.MAX_SAFE_INTEGER),
status: 'submitting',
submissionId,
updatedAt: now,
});
});
if (claim.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (claim.status === 'success')
return await submittedResult(options, claim);
if (claim.status === 'pending')
return pendingResult(options.method, claim);
if (claim.submissionId !== submissionId)
return await submittingResult(options, claim);
const { final, transactionHash: initialTransactionHash } = await (async () => {
try {
const blockNumber = await getBlockNumber(options.client, {
cacheTime: 0,
});
await validateConfig({
account: claim.account,
blockNumber,
client: options.client,
config: claim.config,
});
const finalApprovals = await selectApprovals({
account: claim.account,
config: claim.config,
hash: claim.hash,
approvals: claim.approvals,
});
const final = MultisigOperation.serializeTransaction(claim, {
approvals: finalApprovals.selectedApprovals,
});
const transactionHash = TxEnvelopeTempo.hash(TxEnvelopeTempo.deserialize(final));
await OperationStore.writeSubmission(options.store, operationHash, submissionId, final);
return { final, transactionHash };
}
catch (error) {
await releaseSubmission(options.store, operationHash, submissionId);
throw error;
}
})();
let transactionHash = initialTransactionHash;
let result;
const stopLease = maintainSubmissionLease(options.store, operationHash, submissionId, leaseTtl);
let leaseStopped = false;
const stopLeaseOnce = async () => {
if (leaseStopped)
return;
leaseStopped = true;
await stopLease();
};
try {
try {
result = await options.next({
method: options.method === 'eth_sendRawTransaction' ||
options.method === 'multisig_approveRawTransaction'
? 'eth_sendRawTransaction'
: 'eth_sendRawTransactionSync',
params: [final, ...(options.request.params?.slice(1) ?? [])],
}, options.requestOptions);
const returnedHash = getTransactionHash(result);
if (returnedHash.toLowerCase() !== transactionHash.toLowerCase())
transactionHash = returnedHash;
}
catch (error) {
const submittedHash = await OperationStore.readSubmission(options.store, claim, submissionId);
const transaction = await (async () => {
if (!submittedHash)
return null;
try {
return await options.next({
method: 'eth_getTransactionByHash',
params: [submittedHash],
});
}
catch {
return null;
}
})();
await stopLeaseOnce();
if (!submittedHash || !transaction) {
await releaseSubmission(options.store, operationHash, submissionId);
throw error;
}
transactionHash = submittedHash;
}
}
finally {
await stopLeaseOnce();
}
const success = await OperationStore.update(options.store, operationHash, (current) => {
if (!current || current.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (current.status === 'success') {
if (current.transactionHash?.toLowerCase() !==
transactionHash.toLowerCase())
throw new OperationStore.InvalidStoreValueError();
return current;
}
if (current.status !== 'submitting' ||
current.submissionId !== submissionId)
throw new OperationStore.InvalidStoreValueError();
const { expiresAt: _, submissionId: __, ...operation } = current;
return MultisigOperation.from({
...operation,
status: 'success',
transactionHash,
updatedAt: Date.now(),
});
});
if (success.type !== 'transaction' || success.status !== 'success')
throw new OperationStore.InvalidStoreValueError();
await removeSettledSubmission(options.store, operationHash, submissionId);
if (options.method === 'eth_sendRawTransactionSync' &&
result &&
typeof result === 'object')
return { ...result, multisig: MultisigOperation.toRpc(success) };
return await submittedResult(options, success);
}
/** Collects approvals for a multisig key authorization. @internal */
// biome-ignore lint/correctness/noUnusedVariables: declaration merge
async function approveKeyAuthorization(options) {
const value = options.request.params?.[0];
if (!value || typeof value !== 'object')
throw new RpcResponse.InvalidParamsError({
message: 'Expected a multisig key authorization approval.',
});
const approval = await (async () => {
if ('keyAuthorization' in value) {
const authorization = (() => {
try {
return KeyAuthorization.fromRpc(value.keyAuthorization);
}
catch {
throw new RpcResponse.InvalidParamsError({
message: 'Invalid multisig key authorization.',
});
}
})();
const signature = authorization.signature;
if (signature?.type !== 'multisig')
throw new RpcResponse.InvalidParamsError({
message: 'Expected a multisig key authorization signature.',
});
if (!authorization.account ||
!isAddressEqual(authorization.account, signature.account))
throw new RpcResponse.InvalidParamsError({
message: 'Multisig key authorization account does not match its signature.',
});
const { signature: _, ...unsigned } = authorization;
const keyAuthorization = KeyAuthorization.serialize(KeyAuthorization.from(unsigned));
const config = MultisigConfig.from(signature.config);
const hash = MultisigOperation.getHash({
account: signature.account,
config,
keyAuthorization,
type: 'keyAuthorization',
});
return {
account: signature.account,
approvals: signature.signatures.map((signature) => SignatureEnvelope.serialize(signature)),
config,
hash,
keyAuthorization,
};
}
if ('hash' in value && 'signature' in value) {
if (typeof value.hash !== 'string' || !Hash.validate(value.hash))
throw new RpcResponse.InvalidParamsError({
message: 'Expected a multisig operation hash.',
});
const operation = await OperationStore.read(options.store, value.hash);
if (!operation || operation.type !== 'keyAuthorization')
throw new RpcResponse.InvalidParamsError({
message: 'Multisig key authorization operation was not found.',
});
if (operation.status === 'success')
return { operation };
const signature = (() => {
try {
return SignatureEnvelope.serialize(SignatureEnvelope.from(value.signature));
}
catch {
throw new RpcResponse.InvalidParamsError({
message: 'Invalid multisig owner signature.',
});
}
})();
return {
account: operation.account,
approvals: [signature],
config: operation.config,
hash: operation.hash,
keyAuthorization: operation.keyAuthorization,
};
}
throw new RpcResponse.InvalidParamsError({
message: 'Expected a multisig key authorization approval.',
});
})();
if ('operation' in approval)
return MultisigOperation.toRpc(approval.operation);
if (approval.approvals.length === 0)
throw new RpcResponse.InvalidParamsError({
message: 'A multisig approval envelope must include a signature.',
});
const blockNumber = await getBlockNumber(options.client, { cacheTime: 0 });
const commitment = await validateConfig({
account: approval.account,
blockNumber,
client: options.client,
config: approval.config,
});
const now = Date.now();
const operation = await OperationStore.update(options.store, approval.hash, async (existing) => {
if (existing && existing.type !== 'keyAuthorization')
throw new OperationStore.InvalidStoreValueError();
if (existing?.status === 'success')
return existing;
if (existing &&
(existing.account.toLowerCase() !== approval.account.toLowerCase() ||
MultisigConfig.getCommitment(existing.config).toLowerCase() !==
MultisigConfig.getCommitment(approval.config).toLowerCase() ||
existing.keyAuthorization.toLowerCase() !==
approval.keyAuthorization.toLowerCase()))
throw new OperationStore.InvalidStoreValueError();
const existingApprovals = existing
? await selectApprovals({
account: approval.account,
approvals: existing.approvals,
config: approval.config,
hash: approval.hash,
})
: undefined;
const approvals = await selectApprovals({
account: approval.account,
approvals: [
...(existingApprovals?.approvals ?? []),
...approval.approvals,
],
config: approval.config,
hash: approval.hash,
});
if (existing &&
approvals.approvals.length === existing.approvals.length &&
approvals.approvals.every((approval, index) => approval === existing.approvals[index]))
return existing;
const pending = {
account: approval.account,
approvals: approvals.approvals,
config: approval.config,
createdAt: existing?.createdAt ?? now,
hash: approval.hash,
keyAuthorization: approval.keyAuthorization,
signatureCount: approvals.signatureCount,
threshold: approvals.threshold,
type: 'keyAuthorization',
updatedAt: now,
weight: approvals.weight,
};
if (approvals.weight < approvals.threshold)
return MultisigOperation.from({ ...pending, status: 'pending' });
return MultisigOperation.from({
...pending,
keyAuthorization: MultisigOperation.serializeKeyAuthorization(approval.keyAuthorization, {
account: approval.account,
approvals: approvals.selectedApprovals,
config: approval.config,
}),
status: 'success',
});
});
await ConfigStore.write(options.store, {
address: approval.account,
commitment,
config: approval.config,
});
if (operation.type !== 'keyAuthorization')
throw new OperationStore.InvalidStoreValueError();
return MultisigOperation.toRpc(operation);
}
/** Deserializes a Tempo transaction or throws an RPC parameter error. */
function deserialize(serialized) {
try {
return Transaction.deserialize(serialized);
}
catch {
throw new RpcResponse.InvalidParamsError({
message: 'Invalid serialized Tempo transaction.',
});
}
}
/** Validates a config against an observed chain commitment. */
function assertConfig(options) {
const { account, commitment, config } = options;
if (config.version === 0n &&
MultisigConfig.getAddress(config, {
factory: Addresses.nativeMultisigFactory,
}).toLowerCase() !== account.toLowerCase())
throw new RpcResponse.InvalidParamsError({
message: 'Initial multisig config does not match the multisig account.',
});
if (config.version === 0n && Hex.toBigInt(commitment) === 0n)
return;
if (commitment.toLowerCase() !==
MultisigConfig.getCommitment(config).toLowerCase())
throw new RpcResponse.InvalidParamsError({
message: `Multisig config does not match account ${account}.`,
});
}
/** Reads and validates a config commitment at one block. */
async function validateConfig(options) {
const { account, blockNumber, client, config } = options;
const commitment = await getConfigCommitment(client, {
account,
blockNumber,
});
assertConfig({ account, commitment, config });
return commitment;
}
/** Caches candidate configs from valid native update calls. */
// biome-ignore lint/correctness/noUnusedVariables: declaration merge
async function cacheNextConfigs(options) {
let currentConfig = options.config;
for (const call of options.transaction.calls) {
if (!call.to ||
!isAddressEqual(call.to, Addresses.nativeMultisig) ||
!call.data)
continue;
const decoded = (() => {
try {
return decodeFunctionData({ abi: Abis.nativeMultisig, data: call.data });
}
catch {
return undefined;
}
})();
if (decoded?.functionName !== 'updateConfig')
continue;
const [current, threshold, owners] = decoded.args;
const suppliedConfig = (() => {
try {
return MultisigConfig.from(current);
}
catch {
return undefined;
}
})();
if (!suppliedConfig ||
MultisigConfig.getCommitment(suppliedConfig).toLowerCase() !==
MultisigConfig.getCommitment(currentConfig).toLowerCase())
continue;
const config = (() => {
try {
return MultisigConfig.from({
owners,
salt: suppliedConfig.salt,
threshold,
version: suppliedConfig.version + 1n,
});
}
catch {
return undefined;
}
})();
if (!config)
continue;
await ConfigStore.write(options.store, {
address: options.account,
commitment: MultisigConfig.getCommitment(config),
config,
});
currentConfig = config;
}
}
/** Validates primitive approvals and translates invalid input into an RPC error. */
async function selectApprovals(options) {
try {
return await MultisigOperation.selectApprovals(options);
}
catch (error) {
if (error instanceof MultisigOperation.InvalidApprovalError)
throw new RpcResponse.InvalidParamsError({ message: error.shortMessage });
throw error;
}
}
/** Returns an existing successful operation through the requested send method. */
async function submittedResult(options, operation) {
if (options.method === 'eth_sendRawTransaction' ||
options.method === 'multisig_approveRawTransaction')
return operation.hash;
if (options.method === 'multisig_approveRawTransactionSync')
return MultisigOperation.toRpc(operation);
const timeout = options.request.params?.[1];
const deadline = Date.now() +
(typeof timeout === 'number' && timeout >= 0 ? timeout : submissionTtl);
while (true) {
const receipt = await options.next({
method: 'eth_getTransactionReceipt',
params: [operation.transactionHash],
}, options.requestOptions);
if (receipt && typeof receipt === 'object')
return { ...receipt, multisig: MultisigOperation.toRpc(operation) };
if (Date.now() >= deadline)
throw new Error('Timed out while waiting for the multisig transaction.');
await new Promise((resolve) => setTimeout(resolve, pollingInterval));
}
}
/** Waits for the relay that owns a live submission lease. */
async function submittingResult(options, operation) {
if (options.method === 'eth_sendRawTransaction' ||
options.method === 'multisig_approveRawTransaction')
return operation.hash;
const timeout = options.request.params?.[1];
const deadline = Date.now() +
(typeof timeout === 'number' && timeout >= 0 ? timeout : submissionTtl);
while (true) {
const current = await OperationStore.read(options.store, operation.hash);
if (!current || current.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (current.status === 'success')
return await submittedResult(options, current);
if (current.status === 'pending' || current.expiresAt <= Date.now())
return await submit(options);
if (Date.now() >= deadline)
return pendingResult(options.method, current);
await new Promise((resolve) => setTimeout(resolve, pollingInterval));
}
}
/** Returns a pending result for an operation that has not reached quorum. */
function pendingResult(method, operation) {
if (method === 'eth_sendRawTransaction' ||
method === 'multisig_approveRawTransaction')
return operation.hash;
if (method === 'multisig_approveRawTransactionSync')
return MultisigOperation.toRpc(operation);
return {
blockHash: null,
blockNumber: null,
contractAddress: null,
cumulativeGasUsed: null,
effectiveGasPrice: null,
from: operation.account,
gasUsed: null,
logs: [],
logsBloom: null,
multisig: MultisigOperation.toRpc(operation),
status: 'pending',
to: null,
transactionHash: operation.hash,
transactionIndex: null,
type: '0x76',
};
}
/** Marks a transaction as successful after a lookup proves that it was submitted. */
async function completeSubmission(store, operation, transactionHash) {
if (operation.status !== 'submitting' || !operation.submissionId)
throw new OperationStore.InvalidStoreValueError();
const success = await OperationStore.update(store, operation.hash, (current) => {
if (!current || current.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (current.status === 'success') {
if (current.transactionHash?.toLowerCase() !==
transactionHash.toLowerCase())
throw new OperationStore.InvalidStoreValueError();
return current;
}
if (current.status !== 'submitting' ||
current.submissionId !== operation.submissionId)
throw new OperationStore.InvalidStoreValueError();
const { expiresAt: _, submissionId: __, transactionHash: ___, ...value } = current;
return MultisigOperation.from({
...value,
status: 'success',
transactionHash,
updatedAt: Date.now(),
});
});
await removeSettledSubmission(store, operation.hash, operation.submissionId);
return success;
}
/** Releases a failed submission lease without discarding collected approvals. */
async function releaseSubmission(store, hash, submissionId) {
await OperationStore.update(store, hash, (current) => {
if (!current || current.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (current.status !== 'submitting' ||
current.submissionId !== submissionId)
return current;
const { expiresAt: _, submissionId: __, ...operation } = current;
return MultisigOperation.from({
...operation,
status: 'pending',
updatedAt: Date.now(),
});
});
await OperationStore.removeSubmission(store, hash, submissionId);
}
/** Removes settled submission data without replacing a successful result with a cleanup error. */
async function removeSettledSubmission(store, hash, submissionId) {
try {
await OperationStore.removeSubmission(store, hash, submissionId);
}
catch { }
}
/** Preserves or upgrades a fee-payer envelope without removing an existing signature. */
function mergeTransaction(existing, incoming) {
if (!existing)
return incoming;
const existingTransaction = TxEnvelopeTempo.deserialize(existing);
const incomingTransaction = TxEnvelopeTempo.deserialize(incoming);
if (!('feePayerSignature' in incomingTransaction))
return existing;
if (incomingTransaction.feePayerSignature !== null)
return incoming;
if (!('feePayerSignature' in existingTransaction))
return incoming;
return existing;
}
/** Returns a synthetic transaction while the downstream transaction is unavailable. */
async function toTransaction(client, operation) {
const approvals = await (async () => {
if (operation.status !== 'pending')
return await MultisigOperation.selectApprovals({
account: operation.account,
approvals: operation.approvals,
config: operation.config,
hash: operation.hash,
});
const blockNumber = await getBlockNumber(client, { cacheTime: 0 });
await validateConfig({
account: operation.account,
blockNumber,
client,
config: operation.config,
});
return await selectApprovals({
account: operation.account,
approvals: operation.approvals,
config: operation.config,
hash: operation.hash,
});
})();
const current = operation.status === 'pending'
? MultisigOperation.from({
...operation,
approvals: approvals.approvals,
signatureCount: approvals.signatureCount,
threshold: approvals.threshold,
weight: approvals.weight,
})
: operation;
const serialized = MultisigOperation.serializeTransaction(current, {
approvals: approvals.selectedApprovals,
});
const transaction = deserialize(serialized);
return {
...ox_Transaction.toRpc({
...transaction,
blockHash: null,
blockNumber: null,
from: operation.account,
hash: operation.hash,
transactionIndex: null,
}, { pending: true }),
multisig: MultisigOperation.toRpc(current),
};
}
/** Returns the persisted transaction hash for a submitting or successful operation. */
async function getSubmittedTransactionHash(store, operation) {
if (operation.status === 'success')
return operation.transactionHash;
if (operation.status !== 'submitting' || !operation.submissionId)
throw new OperationStore.InvalidStoreValueError();
return await OperationStore.readSubmission(store, operation, operation.submissionId);
}
/** Renews a submission lease until the returned cleanup function runs. */
function maintainSubmissionLease(store, hash, submissionId, leaseTtl) {
let active = true;
let error;
let renewal = Promise.resolve();
const timer = setInterval(() => {
renewal = renewal.then(async () => {
if (!active || error)
return;
try {
const operation = await OperationStore.update(store, hash, (current) => {
if (!current || current.type !== 'transaction')
throw new OperationStore.InvalidStoreValueError();
if (current.status !== 'submitting' ||
current.submissionId !== submissionId)
return current;
return MultisigOperation.from({
...current,
expiresAt: Math.min(Date.now() + leaseTtl, Number.MAX_SAFE_INTEGER),
updatedAt: Date.now(),
});
});
if (operation.type !== 'transaction' ||
operation.status !== 'submitting' ||
operation.submissionId !== submissionId)
throw new OperationStore.InvalidStoreValueError();
}
catch (cause) {
error = cause;
}
});
}, submissionTtl / 2);
return async () => {
active = false;
clearInterval(timer);
await renewal;
if (error)
throw error;
};
}
/** Canonically serializes an unsigned Tempo transaction. */
function serializeUnsigned(envelope, feePayer) {
return TxEnvelopeTempo.serialize(envelope, feePayer
? envelope.from
? { format: 'feePayer', sender: envelope.from }
: { format: 'feePayer' }
: {});
}
/** Extracts a transaction hash from an RPC submission result. */
function getTransactionHash(result) {
if (typeof result === 'string' && Hash.validate(result))
return result;
if (result &&
typeof result === 'object' &&
'transactionHash' in result &&
typeof result.transactionHash === 'string' &&
Hash.validate(result.transactionHash))
return result.transactionHash;
throw new Error('Expected transaction hash in multisig broadcast result.');
}
/** Resolves and validates the chain used by a handled request. */
async function resolveRequestOptions(options) {
const { request, requestOptions, store } = options;
const chainId_explicit = requestOptions?.chainId;
if (chainId_explicit !== undefined &&
(!Number.isSafeInteger(chainId_explicit) || chainId_explicit <= 0))
throw new RpcResponse.InvalidParamsError({
message: 'Expected a valid chain ID.',
});
const chainId_request = await resolveRequestChainId(request, store);
if (chainId_explicit !== undefined &&
chainId_request !== undefined &&
chainId_explicit !== chainId_request)
throw new RpcResponse.InvalidParamsError({
message: 'Conflicting chain ids.',
});
const chainId = chainId_explicit ?? chainId_request;
if (chainId === undefined)
return requestOptions;
return { ...requestOptions, chainId };
}
/** Resolves a chain from request fields, envelopes, or stored operations. */
async function resolveRequestChainId(request, store) {
const value = request.params?.[0];
const chainId_body = value && typeof value === 'object' && 'chainId' in value
? parseChainId(value.chainId)
: undefined;
const chainId_multisig = await (async () => {
if (request.method === 'eth_sendRawTransaction' ||
request.method === 'eth_sendRawTransactionSync' ||
request.method === 'multisig_approveRawTransaction' ||
request.method === 'multisig_approveRawTransactionSync') {
if (!isSerializedTempoTransaction(value))
return undefined;
try {
return parseChainId(Transaction.deserialize(value).chainId);
}
catch {
return undefined;
}
}
if (request.method === 'multisig_approveKeyAuthorization' &&
value &&
typeof value === 'object' &&
'keyAuthorization' in value) {
try {
return parseChainId(KeyAuthorization.fromRpc(value.keyAuthorization).chainId);
}
catch {
return undefined;
}
}
const hash = (() => {
if (request.method === 'eth_getTransactionByHash' ||
request.method === 'eth_getTransactionReceipt')
return value;
if (request.method === 'multisig_approveKeyAuthorization' &&
value &&
typeof value === 'object' &&
'hash' in value)
return value.hash;
return undefined;
})();
if (typeof hash !== 'string' || !Hash.validate(hash))
return undefined;
const operation = await OperationStore.read(store, hash);
if (!operation)
return undefined;
if (operation.type === 'transaction')
return parseChainId(Transaction.deserialize(operation.transaction).chainId);
return parseChainId(KeyAuthorization.deserialize(operation.keyAuthorization).chainId);
})();
if (chainId_body !== undefined &&
chainId_multisig !== undefined &&
chainId_body !== chainId_multisig)
throw new RpcResponse.InvalidParamsError({
message: 'Conflicting chain ids.',
});
return chainId_body ?? chainId_multisig;
}
/** Parses a supported chain ID representation. */
function parseChainId(value) {
const chainId = (() => {
if (typeof value === 'number')
return value;
if (typeof value === 'bigint')
return Number(value);
if (typeof value === 'string' && Hex.validate(value)) {
try {
return Hex.toNumber(value);
}
catch {
return undefined;
}
}
return undefined;
})();
if (chainId === undefined || !Number.isSafeInteger(chainId) || chainId <= 0)
return undefined;
return chainId;
}
/** Checks whether a value is a serialized Tempo transaction. */
function isSerializedTempoTransaction(value) {
return (typeof value === 'string' &&
(value.startsWith('0x76') || value.startsWith('0x78')));
}
//# sourceMappingURL=Multisig.js.map