Skip to content

Commit a92b0ec

Browse files
committed
feat: StatusManager tracks seenTxs
- use a composite key of `txHash+chainId` to track unique `EventFeed` submissions
1 parent 1d2679d commit a92b0ec

File tree

3 files changed

+58
-2
lines changed

3 files changed

+58
-2
lines changed

packages/fast-usdc/src/exos/status-manager.js

+32-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { CctpTxEvidenceShape, PendingTxShape } from '../typeGuards.js';
66
import { PendingTxStatus } from '../constants.js';
77

88
/**
9-
* @import {MapStore} from '@agoric/store';
9+
* @import {MapStore, SetStore} from '@agoric/store';
1010
* @import {Zone} from '@agoric/zone';
11-
* @import {CctpTxEvidence, NobleAddress, PendingTxKey, PendingTx} from '../types.js';
11+
* @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx} from '../types.js';
1212
*/
1313

1414
/**
@@ -35,6 +35,20 @@ const pendingTxKeyOf = evidence => {
3535
return makePendingTxKey(forwardingAddress, amount);
3636
};
3737

38+
/**
39+
* Get the key for the seenTxs SetStore.
40+
*
41+
* The key is a composite of `NobleAddress` and transaction `amount` and not
42+
* meant to be parsable.
43+
*
44+
* @param {CctpTxEvidence} evidence
45+
* @returns {SeenTxKey}
46+
*/
47+
const seenTxKeyOf = evidence => {
48+
const { txHash, chainId } = evidence;
49+
return `seenTx:${JSON.stringify([txHash, chainId])}`;
50+
};
51+
3852
/**
3953
* The `StatusManager` keeps track of Pending and Seen Transactions
4054
* via {@link PendingTxStatus} states, aiding in coordination between the `Advancer`
@@ -51,11 +65,27 @@ export const prepareStatusManager = zone => {
5165
valueShape: M.arrayOf(PendingTxShape),
5266
});
5367

68+
/** @type {SetStore<SeenTxKey>} */
69+
const seenTxs = zone.setStore('SeenTxs', {
70+
keyShape: M.string(),
71+
});
72+
5473
/**
74+
* Ensures that `txHash+chainId` has not been processed
75+
* and adds entry to `seenTxs` set.
76+
*
77+
* Also records the CctpTxEvidence and status in `pendingTxs`.
78+
*
5579
* @param {CctpTxEvidence} evidence
5680
* @param {PendingTxStatus} status
5781
*/
5882
const recordPendingTx = (evidence, status) => {
83+
const seenKey = seenTxKeyOf(evidence);
84+
if (seenTxs.has(seenKey)) {
85+
throw makeError(`Transaction already seen: ${q(seenKey)}`);
86+
}
87+
seenTxs.add(seenKey);
88+
5989
appendToStoredArray(
6090
pendingTxs,
6191
pendingTxKeyOf(evidence),

packages/fast-usdc/src/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ export interface PendingTx extends CctpTxEvidence {
3232
/** internal key for `StatusManager` exo */
3333
export type PendingTxKey = `pendingTx:${string}`;
3434

35+
/** internal key for `StatusManager` exo */
36+
export type SeenTxKey = `seenTx:${string}`;
37+
3538
export type * from './constants.js';

packages/fast-usdc/test/exos/status-manager.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,29 @@ test('observe creates new entry with OBSERVED status', t => {
3535
t.is(entries[0]?.status, PendingTxStatus.Observed);
3636
});
3737

38+
test('cannot process same tx twice', t => {
39+
const zone = provideDurableZone('status-test');
40+
const statusManager = prepareStatusManager(zone.subZone('status-manager'));
41+
42+
const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO();
43+
statusManager.advance(evidence);
44+
45+
t.throws(() => statusManager.advance(evidence), {
46+
message:
47+
'Transaction already seen: "seenTx:[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]"',
48+
});
49+
50+
t.throws(() => statusManager.observe(evidence), {
51+
message:
52+
'Transaction already seen: "seenTx:[\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\",1]"',
53+
});
54+
55+
// new txHash should not throw
56+
t.notThrows(() => statusManager.advance({ ...evidence, txHash: '0xtest2' }));
57+
// new chainId with existing txHash should not throw
58+
t.notThrows(() => statusManager.advance({ ...evidence, chainId: 9999 }));
59+
});
60+
3861
test('settle removes entries from PendingTxs', t => {
3962
const zone = provideDurableZone('status-test');
4063
const statusManager = prepareStatusManager(zone.subZone('status-manager'));

0 commit comments

Comments
 (0)