viem
Version:
1,000 lines • 41.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleRequest = handleRequest;
const Address_ = require("ox/Address");
const Hash = require("ox/Hash");
const Hex = require("ox/Hex");
const RpcResponse = require("ox/RpcResponse");
const tempo_1 = require("ox/tempo");
const getBlockNumber_js_1 = require("../actions/public/getBlockNumber.js");
const createClient_js_1 = require("../clients/createClient.js");
const custom_js_1 = require("../clients/transports/custom.js");
const decodeFunctionData_js_1 = require("../utils/abi/decodeFunctionData.js");
const isAddressEqual_js_1 = require("../utils/address/isAddressEqual.js");
const Abis = require("./Abis.js");
const Addresses = require("./Addresses.js");
const multisig_js_1 = require("./actions/multisig.js");
const ConfigStore = require("./multisig/Config.js");
const OperationStore = require("./multisig/Operation.js");
const Transaction = require("./Transaction.js");
const submissionTtl = 30_000;
const pollingInterval = 100;
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 = (0, createClient_js_1.createClient)({
transport: (0, custom_js_1.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 (0, getBlockNumber_js_1.getBlockNumber)(client, { cacheTime: 0 });
const commitment = await (0, multisig_js_1.getConfigCommitment)(client, {
account: address,
blockNumber,
});
const config = await ConfigStore.read(parameters.store, {
address,
commitment,
});
if (!config)
return null;
return tempo_1.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 ? tempo_1.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: tempo_1.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: tempo_1.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,
});
};
}
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 = tempo_1.TxEnvelopeTempo.from(unsigned);
const serializedUnsigned = serializeUnsigned(envelope, options.serialized.startsWith(tempo_1.TxEnvelopeTempo.feePayerMagic));
const blockNumber = await (0, getBlockNumber_js_1.getBlockNumber)(options.client, { cacheTime: 0 });
const config = tempo_1.MultisigConfig.from(signature.config);
const configCommitment = await validateConfig({
account: signature.account,
blockNumber,
client: options.client,
config,
});
const operationHash = tempo_1.MultisigOperation.getHash({
account: signature.account,
config,
transaction: serializedUnsigned,
type: 'transaction',
});
const incoming = signature.signatures.map((signature) => tempo_1.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 tempo_1.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 tempo_1.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 (0, getBlockNumber_js_1.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 = tempo_1.MultisigOperation.serializeTransaction(claim, {
approvals: finalApprovals.selectedApprovals,
});
const transactionHash = tempo_1.TxEnvelopeTempo.hash(tempo_1.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 tempo_1.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: tempo_1.MultisigOperation.toRpc(success) };
return await submittedResult(options, success);
}
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 tempo_1.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 ||
!(0, isAddressEqual_js_1.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 = tempo_1.KeyAuthorization.serialize(tempo_1.KeyAuthorization.from(unsigned));
const config = tempo_1.MultisigConfig.from(signature.config);
const hash = tempo_1.MultisigOperation.getHash({
account: signature.account,
config,
keyAuthorization,
type: 'keyAuthorization',
});
return {
account: signature.account,
approvals: signature.signatures.map((signature) => tempo_1.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 tempo_1.SignatureEnvelope.serialize(tempo_1.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 tempo_1.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 (0, getBlockNumber_js_1.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() ||
tempo_1.MultisigConfig.getCommitment(existing.config).toLowerCase() !==
tempo_1.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 tempo_1.MultisigOperation.from({ ...pending, status: 'pending' });
return tempo_1.MultisigOperation.from({
...pending,
keyAuthorization: tempo_1.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 tempo_1.MultisigOperation.toRpc(operation);
}
function deserialize(serialized) {
try {
return Transaction.deserialize(serialized);
}
catch {
throw new RpcResponse.InvalidParamsError({
message: 'Invalid serialized Tempo transaction.',
});
}
}
function assertConfig(options) {
const { account, commitment, config } = options;
if (config.version === 0n &&
tempo_1.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() !==
tempo_1.MultisigConfig.getCommitment(config).toLowerCase())
throw new RpcResponse.InvalidParamsError({
message: `Multisig config does not match account ${account}.`,
});
}
async function validateConfig(options) {
const { account, blockNumber, client, config } = options;
const commitment = await (0, multisig_js_1.getConfigCommitment)(client, {
account,
blockNumber,
});
assertConfig({ account, commitment, config });
return commitment;
}
async function cacheNextConfigs(options) {
let currentConfig = options.config;
for (const call of options.transaction.calls) {
if (!call.to ||
!(0, isAddressEqual_js_1.isAddressEqual)(call.to, Addresses.nativeMultisig) ||
!call.data)
continue;
const decoded = (() => {
try {
return (0, decodeFunctionData_js_1.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 tempo_1.MultisigConfig.from(current);
}
catch {
return undefined;
}
})();
if (!suppliedConfig ||
tempo_1.MultisigConfig.getCommitment(suppliedConfig).toLowerCase() !==
tempo_1.MultisigConfig.getCommitment(currentConfig).toLowerCase())
continue;
const config = (() => {
try {
return tempo_1.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: tempo_1.MultisigConfig.getCommitment(config),
config,
});
currentConfig = config;
}
}
async function selectApprovals(options) {
try {
return await tempo_1.MultisigOperation.selectApprovals(options);
}
catch (error) {
if (error instanceof tempo_1.MultisigOperation.InvalidApprovalError)
throw new RpcResponse.InvalidParamsError({ message: error.shortMessage });
throw error;
}
}
async function submittedResult(options, operation) {
if (options.method === 'eth_sendRawTransaction' ||
options.method === 'multisig_approveRawTransaction')
return operation.hash;
if (options.method === 'multisig_approveRawTransactionSync')
return tempo_1.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: tempo_1.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));
}
}
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));
}
}
function pendingResult(method, operation) {
if (method === 'eth_sendRawTransaction' ||
method === 'multisig_approveRawTransaction')
return operation.hash;
if (method === 'multisig_approveRawTransactionSync')
return tempo_1.MultisigOperation.toRpc(operation);
return {
blockHash: null,
blockNumber: null,
contractAddress: null,
cumulativeGasUsed: null,
effectiveGasPrice: null,
from: operation.account,
gasUsed: null,
logs: [],
logsBloom: null,
multisig: tempo_1.MultisigOperation.toRpc(operation),
status: 'pending',
to: null,
transactionHash: operation.hash,
transactionIndex: null,
type: '0x76',
};
}
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 tempo_1.MultisigOperation.from({
...value,
status: 'success',
transactionHash,
updatedAt: Date.now(),
});
});
await removeSettledSubmission(store, operation.hash, operation.submissionId);
return success;
}
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 tempo_1.MultisigOperation.from({
...operation,
status: 'pending',
updatedAt: Date.now(),
});
});
await OperationStore.removeSubmission(store, hash, submissionId);
}
async function removeSettledSubmission(store, hash, submissionId) {
try {
await OperationStore.removeSubmission(store, hash, submissionId);
}
catch { }
}
function mergeTransaction(existing, incoming) {
if (!existing)
return incoming;
const existingTransaction = tempo_1.TxEnvelopeTempo.deserialize(existing);
const incomingTransaction = tempo_1.TxEnvelopeTempo.deserialize(incoming);
if (!('feePayerSignature' in incomingTransaction))
return existing;
if (incomingTransaction.feePayerSignature !== null)
return incoming;
if (!('feePayerSignature' in existingTransaction))
return incoming;
return existing;
}
async function toTransaction(client, operation) {
const approvals = await (async () => {
if (operation.status !== 'pending')
return await tempo_1.MultisigOperation.selectApprovals({
account: operation.account,
approvals: operation.approvals,
config: operation.config,
hash: operation.hash,
});
const blockNumber = await (0, getBlockNumber_js_1.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'
? tempo_1.MultisigOperation.from({
...operation,
approvals: approvals.approvals,
signatureCount: approvals.signatureCount,
threshold: approvals.threshold,
weight: approvals.weight,
})
: operation;
const serialized = tempo_1.MultisigOperation.serializeTransaction(current, {
approvals: approvals.selectedApprovals,
});
const transaction = deserialize(serialized);
return {
...tempo_1.Transaction.toRpc({
...transaction,
blockHash: null,
blockNumber: null,
from: operation.account,
hash: operation.hash,
transactionIndex: null,
}, { pending: true }),
multisig: tempo_1.MultisigOperation.toRpc(current),
};
}
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);
}
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 tempo_1.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;
};
}
function serializeUnsigned(envelope, feePayer) {
return tempo_1.TxEnvelopeTempo.serialize(envelope, feePayer
? envelope.from
? { format: 'feePayer', sender: envelope.from }
: { format: 'feePayer' }
: {});
}
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.');
}
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 };
}
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(tempo_1.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(tempo_1.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;
}
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;
}
function isSerializedTempoTransaction(value) {
return (typeof value === 'string' &&
(value.startsWith('0x76') || value.startsWith('0x78')));
}
//# sourceMappingURL=Multisig.js.map