From aa7d0dd773061387923ea047e9afe460281ccb73 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 6 Nov 2024 20:45:00 -0500 Subject: [PATCH] feat: `StatusManager` tracks `seenTxs` - use a composite key of `txHash+chainId` to track unique `EventFeed` submissions --- packages/fast-usdc/src/exos/status-manager.js | 29 +++++++++++++++++-- packages/fast-usdc/src/types.ts | 3 ++ .../test/exos/status-manager.test.ts | 23 +++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/fast-usdc/src/exos/status-manager.js b/packages/fast-usdc/src/exos/status-manager.js index f76823917f7d..b0ba21ef9d07 100644 --- a/packages/fast-usdc/src/exos/status-manager.js +++ b/packages/fast-usdc/src/exos/status-manager.js @@ -6,9 +6,9 @@ import { CctpTxEvidenceShape, PendingTxShape } from '../typeGuards.js'; import { PendingTxStatus } from '../constants.js'; /** - * @import {MapStore} from '@agoric/store'; + * @import {MapStore, SetStore} from '@agoric/store'; * @import {Zone} from '@agoric/zone'; - * @import {CctpTxEvidence, NobleAddress, PendingTxKey, PendingTx} from '../types.js'; + * @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx} from '../types.js'; */ /** @@ -29,6 +29,15 @@ const getPendingTxKey = evidence => { return toPendingTxKey(forwardingAddress, amount); }; +/** + * Get the key for the seenTxs SetStore + * @param {CctpTxEvidence} evidence + */ +const getSeenKey = evidence => { + const { txHash, chainId } = evidence; + return /** @type {SeenTxKey} */ (JSON.stringify([txHash, chainId])); +}; + /** * The `StatusManager` keeps track of Pending and Seen Transactions * via {@link PendingTxStatus} states, aiding in coordination between the `Advancer` @@ -45,11 +54,27 @@ export const prepareStatusManager = zone => { valueShape: M.arrayOf(PendingTxShape), }); + /** @type {SetStore} */ + const seenTxs = zone.setStore('SeenTxs', { + keyShape: M.string(), + }); + /** + * Ensures that `txHash+chainId` has not been processed + * and adds entry to `seenTxs` set. + * + * Also records the CctpTxEvidence and status in `pendingTxs`. + * * @param {CctpTxEvidence} evidence * @param {PendingTxStatus} status */ const recordPendingTx = (evidence, status) => { + const seenKey = getSeenKey(evidence); + if (seenTxs.has(seenKey)) { + throw makeError(`Transaction already seen: ${q(seenKey)}`); + } + seenTxs.add(seenKey); + appendToStoredArray( pendingTxs, getPendingTxKey(evidence), diff --git a/packages/fast-usdc/src/types.ts b/packages/fast-usdc/src/types.ts index 13be7ece1a2d..9a991deca2e1 100644 --- a/packages/fast-usdc/src/types.ts +++ b/packages/fast-usdc/src/types.ts @@ -32,4 +32,7 @@ export interface PendingTx extends CctpTxEvidence { /** composite of NobleAddress and transaction amount value */ export type PendingTxKey = `"["${NobleAddress}",${bigint}]"`; +/** composite of EvmHash and chainId */ +export type SeenTxKey = `"["${EvmHash}",${number}]"`; + export type * from './constants.js'; diff --git a/packages/fast-usdc/test/exos/status-manager.test.ts b/packages/fast-usdc/test/exos/status-manager.test.ts index dd6acc60d7c2..db84453110e9 100644 --- a/packages/fast-usdc/test/exos/status-manager.test.ts +++ b/packages/fast-usdc/test/exos/status-manager.test.ts @@ -35,6 +35,29 @@ test('observe creates new entry with OBSERVED status', t => { t.is(entries[0]?.status, PendingTxStatus.Observed); }); +test('cannot process same tx twice', t => { + const zone = provideDurableZone('status-test'); + const statusManager = prepareStatusManager(zone.subZone('status-manager')); + + const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); + statusManager.advance(evidence); + + t.throws(() => statusManager.advance(evidence), { + message: + 'Transaction already seen: "[[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]]"', + }); + + t.throws(() => statusManager.observe(evidence), { + message: + 'Transaction already seen: "[[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]]"', + }); + + // new txHash should not throw + t.notThrows(() => statusManager.advance({ ...evidence, txHash: '0xtest2' })); + // new chainId with existing txHash should not throw + t.notThrows(() => statusManager.advance({ ...evidence, chainId: 9999 })); +}); + test('settle removes entries from PendingTxs', t => { const zone = provideDurableZone('status-test'); const statusManager = prepareStatusManager(zone.subZone('status-manager'));