viem
Version:
170 lines • 7.03 kB
JavaScript
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