diff --git a/packages/fast-usdc/src/exos/status-manager.js b/packages/fast-usdc/src/exos/status-manager.js index 4b18fc75ee36..89920ff05b70 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'; */ /** @@ -35,6 +35,20 @@ const pendingTxKeyOf = evidence => { return makePendingTxKey(forwardingAddress, amount); }; +/** + * Get the key for the seenTxs SetStore. + * + * The key is a composite of `NobleAddress` and transaction `amount` and not + * meant to be parsable. + * + * @param {CctpTxEvidence} evidence + * @returns {SeenTxKey} + */ +const seenTxKeyOf = evidence => { + const { txHash, chainId } = evidence; + return `seenTx:${JSON.stringify([txHash, chainId])}`; +}; + /** * The `StatusManager` keeps track of Pending and Seen Transactions * via {@link PendingTxStatus} states, aiding in coordination between the `Advancer` @@ -51,11 +65,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 = seenTxKeyOf(evidence); + if (seenTxs.has(seenKey)) { + throw makeError(`Transaction already seen: ${q(seenKey)}`); + } + seenTxs.add(seenKey); + appendToStoredArray( pendingTxs, pendingTxKeyOf(evidence), diff --git a/packages/fast-usdc/src/types.ts b/packages/fast-usdc/src/types.ts index efcdeaa7f4f4..ed6249f2ff4b 100644 --- a/packages/fast-usdc/src/types.ts +++ b/packages/fast-usdc/src/types.ts @@ -32,4 +32,7 @@ export interface PendingTx extends CctpTxEvidence { /** internal key for `StatusManager` exo */ export type PendingTxKey = `pendingTx:${string}`; +/** internal key for `StatusManager` exo */ +export type SeenTxKey = `seenTx:${string}`; + 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 45638f9d771d..95b1e9825348 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: "seenTx:[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]"', + }); + + t.throws(() => statusManager.observe(evidence), { + message: + 'Transaction already seen: "seenTx:[\\"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'));