UNPKG

viem

Version:

TypeScript Interface for Ethereum

170 lines • 7.03 kB
import * as Json from 'ox/Json'; import { MultisigOperation, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'; import { BaseError } from '../../errors/base.js'; /** Bounds parsing and serialization work if a store returns unexpectedly large data. */ const maxStoredValueLength = 1_048_576; /** Prevents sustained compare-and-set contention from retrying indefinitely. */ const maxUpdateAttempts = 32; /** Bounds abandoned operation state while allowing long-lived approval ceremonies. */ const pendingOperationTtl = 30 * 24 * 60 * 60 * 1_000; /** Reads a persisted multisig operation. */ export async function read(store, hash) { const value = await store.getItem(operationKey(hash)); if (value === null || value === undefined) return null; const operation = deserialize(value); if (operation.hash.toLowerCase() !== hash.toLowerCase()) throw new InvalidStoreValueError(); return operation; } /** Atomically updates a multisig operation. */ export async function update(store, hash, update) { const key = operationKey(hash); for (let attempt = 0; attempt < maxUpdateAttempts; attempt++) { const value = (await store.getItem(key)) ?? null; const current = value === null ? null : deserialize(value); if (current && current.hash.toLowerCase() !== hash.toLowerCase()) throw new InvalidStoreValueError(); const value_ = await update(current); let next; try { next = MultisigOperation.from(value_); } catch (cause) { throw new InvalidStoreValueError({ cause }); } if (next.hash.toLowerCase() !== hash.toLowerCase()) throw new InvalidStoreValueError(); const serialized = serialize(next); const options = next.status === 'success' ? undefined : { expiresAt: Date.now() + pendingOperationTtl }; if (await store.compareAndSet(key, value, serialized, options)) return next; } throw new StoreConflictError(); } /** Reads a submission hash without rebuilding its envelope from mutable configurations. */ export async function readSubmission(store, operation, submissionId) { const value = await store.getItem(submissionKey(operation.hash, submissionId)); if (value === null || value === undefined) return null; try { if (value.length > maxStoredValueLength) throw new InvalidStoreValueError(); const transaction = TxEnvelopeTempo.deserialize(value); if (transaction.signature?.type !== 'multisig') throw new InvalidStoreValueError(); const serialized = MultisigOperation.serializeTransaction(operation, { approvals: transaction.signature.signatures.map((signature) => SignatureEnvelope.serialize(signature)), }); if (serialized.toLowerCase() !== value.toLowerCase()) throw new InvalidStoreValueError(); return TxEnvelopeTempo.hash(transaction); } catch (cause) { if (cause instanceof InvalidStoreValueError) throw cause; throw new InvalidStoreValueError({ cause }); } } /** Removes a persisted final envelope after its submission is settled. */ export async function removeSubmission(store, hash, submissionId) { await store.removeItem(submissionKey(hash, submissionId)); } /** Persists the final envelope before broadcast so recovery uses the exact transaction. */ export async function writeSubmission(store, hash, submissionId, transaction) { try { if (transaction.length > maxStoredValueLength) throw new InvalidStoreValueError(); const envelope = TxEnvelopeTempo.deserialize(transaction); if (!envelope.signature) throw new InvalidStoreValueError(); TxEnvelopeTempo.hash(envelope); const written = await store.compareAndSet(submissionKey(hash, submissionId), null, transaction, { expiresAt: Date.now() + pendingOperationTtl }); if (!written) throw new InvalidStoreValueError(); } catch (cause) { if (cause instanceof InvalidStoreValueError) throw cause; throw new InvalidStoreValueError({ cause }); } } /** Deserializes a multisig operation from storage. */ function deserialize(value) { try { if (value.length > maxStoredValueLength) throw new InvalidStoreValueError(); const operation = MultisigOperation.from(Json.parse(value)); assertHash(operation); return operation; } catch (cause) { if (cause instanceof InvalidStoreValueError) throw cause; throw new InvalidStoreValueError({ cause }); } } /** Serializes a multisig operation for storage. */ function serialize(operation) { try { const operation_ = MultisigOperation.from(operation); assertHash(operation_); const value = Json.stringify(operation_); if (value.length > maxStoredValueLength) throw new InvalidStoreValueError(); return value; } catch (cause) { if (cause instanceof InvalidStoreValueError) throw cause; throw new InvalidStoreValueError({ cause }); } } /** Verifies that the operation hash commits to its stored payload. */ function assertHash(operation) { const hash = MultisigOperation.getHash(operation.type === 'transaction' ? { account: operation.account, config: operation.config, transaction: operation.transaction, type: operation.type, } : { account: operation.account, config: operation.config, keyAuthorization: operation.keyAuthorization, type: operation.type, }); if (hash.toLowerCase() !== operation.hash.toLowerCase()) throw new InvalidStoreValueError(); } /** Returns the store key for an operation hash. */ function operationKey(hash) { return `multisig:operation:${hash.toLowerCase()}`; } /** Returns the store key for a submission attempt. */ function submissionKey(hash, submissionId) { return `multisig:submission:${hash.toLowerCase()}:${submissionId.toLowerCase()}`; } /** Thrown when a stored multisig operation is malformed or unsupported. */ export class InvalidStoreValueError extends BaseError { /** Creates an invalid store value error. */ constructor(options = {}) { super('Stored multisig operation is malformed or unsupported.', { cause: options.cause, name: 'Multisig.Operation.InvalidStoreValueError', }); } } /** Thrown when a multisig operation cannot be updated due to contention. */ export class StoreConflictError extends BaseError { /** Creates a store conflict error. */ constructor() { super('Multisig operation could not be updated after repeated conflicts.', { name: 'Multisig.Operation.StoreConflictError', }); } } //# sourceMappingURL=Operation.js.map