@agoric/fast-usdc 0.2.0-u19.1 → 0.2.0-u20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,427 +0,0 @@
1
- import { makeTracer } from '@agoric/internal';
2
- import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
3
- import { AmountKeywordRecordShape } from '@agoric/zoe/src/typeGuards.js';
4
- import { Fail, makeError, q } from '@endo/errors';
5
- import { E } from '@endo/eventual-send';
6
- import { M } from '@endo/patterns';
7
- import { PendingTxStatus, TerminalTxStatus, TxStatus } from '../constants.js';
8
- import {
9
- CctpTxEvidenceShape,
10
- EvmHashShape,
11
- PendingTxShape,
12
- } from '../type-guards.js';
13
-
14
- /**
15
- * @import {MapStore, SetStore} from '@agoric/store';
16
- * @import {Zone} from '@agoric/zone';
17
- * @import {CctpTxEvidence, NobleAddress, PendingTx, EvmHash, LogFn, TransactionRecord, EvidenceWithRisk, RiskAssessment} from '../types.js';
18
- */
19
-
20
- /**
21
- * @typedef {`pendingTx:${bigint}:${NobleAddress}`} PendingTxKey
22
- * The string template is for developer visibility but not meant to ever be parsed.
23
- */
24
-
25
- /**
26
- * Create the key for the pendingTxs MapStore.
27
- *
28
- * The key is a composite but not meant to be parsable.
29
- *
30
- * @param {NobleAddress} nfa Noble Forwarding Account (implies EUD)
31
- * @param {bigint} amount
32
- * @returns {PendingTxKey}
33
- */
34
- const makePendingTxKey = (nfa, amount) =>
35
- // amount can't contain colon
36
- `pendingTx:${amount}:${nfa}`;
37
-
38
- /**
39
- * Get the key for the pendingTxs MapStore.
40
- *
41
- * @param {CctpTxEvidence} evidence
42
- * @returns {PendingTxKey}
43
- */
44
- const pendingTxKeyOf = evidence => {
45
- const { amount, forwardingAddress } = evidence.tx;
46
- return makePendingTxKey(forwardingAddress, amount);
47
- };
48
-
49
- /**
50
- * @typedef {{
51
- * log?: LogFn;
52
- * marshaller: ERef<Marshaller>;
53
- * }} StatusManagerPowers
54
- */
55
-
56
- export const stateShape = harden({
57
- pendingSettleTxs: M.remotable(),
58
- seenTxs: M.remotable(),
59
- storedCompletedTxs: M.remotable(),
60
- });
61
-
62
- /**
63
- * The `StatusManager` keeps track of Pending and Seen Transactions
64
- * via {@link PendingTxStatus} states, aiding in coordination between the `Advancer`
65
- * and `Settler`.
66
- *
67
- * XXX consider separate facets for `Advancing` and `Settling` capabilities.
68
- *
69
- * @param {Zone} zone
70
- * @param {ERef<StorageNode>} txnsNode
71
- * @param {StatusManagerPowers} caps
72
- */
73
- export const prepareStatusManager = (
74
- zone,
75
- txnsNode,
76
- {
77
- marshaller,
78
- // eslint-disable-next-line no-unused-vars
79
- log = makeTracer('StatusManager', true),
80
- } = /** @type {StatusManagerPowers} */ ({}),
81
- ) => {
82
- /**
83
- * Keyed by a tuple of the Noble Forwarding Account and amount.
84
- * @type {MapStore<PendingTxKey, PendingTx[]>}
85
- */
86
- const pendingSettleTxs = zone.mapStore('PendingSettleTxs', {
87
- keyShape: M.string(),
88
- valueShape: M.arrayOf(PendingTxShape),
89
- });
90
-
91
- /**
92
- * Transactions seen *ever* by the contract.
93
- *
94
- * Note that like all durable stores, this MapStore is kept in IAVL. It stores
95
- * the `blockTimestamp` so that later we can prune old transactions.
96
- *
97
- * Note that `blockTimestamp` can drift between chains. Fortunately all CCTP
98
- * chains use the same Unix epoch and won't drift more than minutes apart,
99
- * which is more than enough precision for pruning old transaction.
100
- *
101
- * @type {MapStore<EvmHash, NatValue>}
102
- */
103
- const seenTxs = zone.mapStore('SeenTxs', {
104
- keyShape: M.string(),
105
- valueShape: M.nat(),
106
- });
107
-
108
- /**
109
- * Transactions that have completed, but are still in vstorage.
110
- *
111
- * @type {SetStore<EvmHash>}
112
- */
113
- const storedCompletedTxs = zone.setStore('StoredCompletedTxs', {
114
- keyShape: M.string(),
115
- });
116
-
117
- /**
118
- * @param {EvmHash} txId
119
- * @param {TransactionRecord} record
120
- * @returns {Promise<void>}
121
- */
122
- const publishTxnRecord = async (txId, record) => {
123
- const txNode = E(txnsNode).makeChildNode(txId, {
124
- sequence: true, // avoid overwriting other output in the block
125
- });
126
-
127
- // XXX awkward for publish* to update a store, but it's temporary
128
- if (record.status && TerminalTxStatus[record.status]) {
129
- // UNTIL https://github.com/Agoric/agoric-sdk/issues/7405
130
- // Queue it for deletion later because if we deleted it now the earlier
131
- // writes in this block would be wiped. For now we keep track of what to
132
- // delete when we know it'll be another block.
133
- storedCompletedTxs.add(txId);
134
- }
135
-
136
- const capData = await E(marshaller).toCapData(record);
137
-
138
- await E(txNode).setValue(JSON.stringify(capData));
139
- };
140
-
141
- /**
142
- * @param {CctpTxEvidence['txHash']} hash
143
- * @param {CctpTxEvidence} evidence
144
- */
145
- const publishEvidence = (hash, evidence) => {
146
- // Don't await, just writing to vstorage.
147
- void publishTxnRecord(
148
- hash,
149
- harden({ evidence, status: TxStatus.Observed }),
150
- );
151
- };
152
-
153
- /**
154
- * Ensures that `txHash+chainId` has not been processed
155
- * and adds entry to `seenTxs` set.
156
- *
157
- * Also records the CctpTxEvidence and status in `pendingTxs`.
158
- *
159
- * @param {CctpTxEvidence} evidence
160
- * @param {PendingTxStatus} status
161
- * @param {string[]} [risksIdentified]
162
- */
163
- const initPendingTx = (evidence, status, risksIdentified) => {
164
- const { txHash } = evidence;
165
- if (seenTxs.has(txHash)) {
166
- throw makeError(`Transaction already seen: ${q(txHash)}`);
167
- }
168
- seenTxs.init(txHash, evidence.blockTimestamp);
169
-
170
- appendToStoredArray(
171
- pendingSettleTxs,
172
- pendingTxKeyOf(evidence),
173
- harden({ ...evidence, status }),
174
- );
175
- publishEvidence(txHash, evidence);
176
- if (status === PendingTxStatus.AdvanceSkipped) {
177
- void publishTxnRecord(txHash, harden({ status, risksIdentified }));
178
- } else if (status !== PendingTxStatus.Observed) {
179
- // publishEvidence publishes Observed
180
- void publishTxnRecord(txHash, harden({ status }));
181
- }
182
- };
183
-
184
- /**
185
- * Update the pending transaction status.
186
- *
187
- * @param {{nfa: NobleAddress, amount: bigint}} keyParts
188
- * @param {PendingTxStatus} status
189
- */
190
- function setPendingTxStatus({ nfa, amount }, status) {
191
- const key = makePendingTxKey(nfa, amount);
192
- pendingSettleTxs.has(key) || Fail`no advancing tx with ${{ nfa, amount }}`;
193
- const pending = pendingSettleTxs.get(key);
194
- const ix = pending.findIndex(tx => tx.status === PendingTxStatus.Advancing);
195
- ix >= 0 || Fail`no advancing tx with ${{ nfa, amount }}`;
196
- const [prefix, tx, suffix] = [
197
- pending.slice(0, ix),
198
- pending[ix],
199
- pending.slice(ix + 1),
200
- ];
201
- const txpost = { ...tx, status };
202
- pendingSettleTxs.set(key, harden([...prefix, txpost, ...suffix]));
203
- void publishTxnRecord(tx.txHash, harden({ status }));
204
- }
205
-
206
- return zone.exo(
207
- 'Fast USDC Status Manager',
208
- M.interface('StatusManagerI', {
209
- // TODO: naming scheme for transition events
210
- advance: M.call(CctpTxEvidenceShape).returns(),
211
- advanceOutcome: M.call(M.string(), M.nat(), M.boolean()).returns(),
212
- skipAdvance: M.call(CctpTxEvidenceShape, M.arrayOf(M.string())).returns(),
213
- advanceOutcomeForMintedEarly: M.call(EvmHashShape, M.boolean()).returns(),
214
- advanceOutcomeForUnknownMint: M.call(CctpTxEvidenceShape).returns(),
215
- observe: M.call(CctpTxEvidenceShape).returns(),
216
- hasBeenObserved: M.call(CctpTxEvidenceShape).returns(M.boolean()),
217
- deleteCompletedTxs: M.call().returns(M.undefined()),
218
- dequeueStatus: M.call(M.string(), M.bigint()).returns(
219
- M.or(
220
- {
221
- txHash: EvmHashShape,
222
- status: M.or(...Object.values(PendingTxStatus)),
223
- },
224
- M.undefined(),
225
- ),
226
- ),
227
- disbursed: M.call(EvmHashShape, AmountKeywordRecordShape).returns(
228
- M.undefined(),
229
- ),
230
- forwarded: M.call(EvmHashShape, M.boolean()).returns(),
231
- lookupPending: M.call(M.string(), M.bigint()).returns(
232
- M.arrayOf(PendingTxShape),
233
- ),
234
- }),
235
- {
236
- /**
237
- * Add a new transaction with ADVANCING status
238
- *
239
- * NB: this acts like observe() but subsequently records an ADVANCING
240
- * state
241
- *
242
- * @param {CctpTxEvidence} evidence
243
- */
244
- advance(evidence) {
245
- initPendingTx(evidence, PendingTxStatus.Advancing);
246
- },
247
-
248
- /**
249
- * Add a new transaction with ADVANCE_SKIPPED status
250
- *
251
- * NB: this acts like observe() but subsequently records an
252
- * ADVANCE_SKIPPED state along with risks identified
253
- *
254
- * @param {CctpTxEvidence} evidence
255
- * @param {string[]} risksIdentified
256
- */
257
- skipAdvance(evidence, risksIdentified) {
258
- initPendingTx(
259
- evidence,
260
- PendingTxStatus.AdvanceSkipped,
261
- risksIdentified,
262
- );
263
- },
264
-
265
- /**
266
- * Record result of an ADVANCING transaction
267
- *
268
- * @param {NobleAddress} nfa Noble Forwarding Account
269
- * @param {import('@agoric/ertp').NatValue} amount
270
- * @param {boolean} success - Advanced vs. AdvanceFailed
271
- * @throws {Error} if nothing to advance
272
- */
273
- advanceOutcome(nfa, amount, success) {
274
- setPendingTxStatus(
275
- { nfa, amount },
276
- success ? PendingTxStatus.Advanced : PendingTxStatus.AdvanceFailed,
277
- );
278
- },
279
-
280
- /**
281
- * If minted while advancing, publish a status update for the advance
282
- * to vstorage.
283
- *
284
- * Does not add or amend `pendingSettleTxs` as this has
285
- * already settled.
286
- *
287
- * @param {EvmHash} txHash
288
- * @param {boolean} success whether the Transfer succeeded
289
- */
290
- advanceOutcomeForMintedEarly(txHash, success) {
291
- void publishTxnRecord(
292
- txHash,
293
- harden({
294
- status: success
295
- ? PendingTxStatus.Advanced
296
- : PendingTxStatus.AdvanceFailed,
297
- }),
298
- );
299
- },
300
-
301
- /**
302
- * If minted before observed and the evidence is eventually
303
- * reported, publish the evidence without adding to `pendingSettleTxs`
304
- *
305
- * @param {CctpTxEvidence} evidence
306
- */
307
- advanceOutcomeForUnknownMint(evidence) {
308
- const { txHash } = evidence;
309
- // unexpected path, since `hasBeenObserved` will be called before this
310
- if (seenTxs.has(txHash)) {
311
- throw makeError(`Transaction already seen: ${q(txHash)}`);
312
- }
313
- seenTxs.init(txHash, evidence.blockTimestamp);
314
- publishEvidence(txHash, evidence);
315
- },
316
-
317
- /**
318
- * Add a new transaction with OBSERVED status
319
- * @param {CctpTxEvidence} evidence
320
- */
321
- observe(evidence) {
322
- initPendingTx(evidence, PendingTxStatus.Observed);
323
- },
324
-
325
- /**
326
- * Note: ADVANCING state implies tx has been OBSERVED
327
- *
328
- * @param {CctpTxEvidence} evidence
329
- */
330
- hasBeenObserved(evidence) {
331
- return seenTxs.has(evidence.txHash);
332
- },
333
-
334
- // UNTIL https://github.com/Agoric/agoric-sdk/issues/7405
335
- deleteCompletedTxs() {
336
- for (const txHash of storedCompletedTxs.values()) {
337
- // As of now, setValue('') on a non-sequence node will delete it
338
- const txNode = E(txnsNode).makeChildNode(txHash, {
339
- sequence: false,
340
- });
341
- void E(txNode)
342
- .setValue('')
343
- .then(() => storedCompletedTxs.delete(txHash));
344
- }
345
- },
346
-
347
- /**
348
- * Remove and return the oldest pending settlement transaction that matches the given
349
- * forwarding account and amount. Since multiple pending transactions may exist with
350
- * identical (account, amount) pairs, we process them in FIFO order.
351
- *
352
- * @param {NobleAddress} nfa
353
- * @param {bigint} amount
354
- * @returns {Pick<PendingTx, 'status' | 'txHash'> | undefined} undefined if no pending
355
- * transactions exist for this address and amount combination.
356
- */
357
- dequeueStatus(nfa, amount) {
358
- const key = makePendingTxKey(nfa, amount);
359
- if (!pendingSettleTxs.has(key)) return undefined;
360
- const pending = pendingSettleTxs.get(key);
361
-
362
- if (pending.length === 0) {
363
- return undefined;
364
- }
365
- // extract first item
366
- const [{ status, txHash }, ...remaining] = pending;
367
-
368
- if (remaining.length) {
369
- pendingSettleTxs.set(key, harden(remaining));
370
- } else {
371
- pendingSettleTxs.delete(key);
372
- }
373
-
374
- return harden({ status, txHash });
375
- },
376
-
377
- /**
378
- * Mark a transaction as `DISBURSED`
379
- *
380
- * @param {EvmHash} txHash
381
- * @param {import('./liquidity-pool.js').RepayAmountKWR} split
382
- */
383
- disbursed(txHash, split) {
384
- void publishTxnRecord(
385
- txHash,
386
- harden({ split, status: TxStatus.Disbursed }),
387
- );
388
- },
389
-
390
- /**
391
- * Mark a transaction as `FORWARDED` or `FORWARD_FAILED`
392
- *
393
- * @param {EvmHash} txHash
394
- * @param {boolean} success
395
- */
396
- forwarded(txHash, success) {
397
- void publishTxnRecord(
398
- txHash,
399
- harden({
400
- status: success ? TxStatus.Forwarded : TxStatus.ForwardFailed,
401
- }),
402
- );
403
- },
404
-
405
- /**
406
- * Lookup all pending entries for a given address and amount
407
- *
408
- * XXX only used in tests. should we remove?
409
- *
410
- * @param {NobleAddress} nfa
411
- * @param {bigint} amount
412
- * @returns {PendingTx[]}
413
- */
414
- lookupPending(nfa, amount) {
415
- const key = makePendingTxKey(nfa, amount);
416
- if (!pendingSettleTxs.has(key)) {
417
- return harden([]);
418
- }
419
- return pendingSettleTxs.get(key);
420
- },
421
- },
422
- { stateShape },
423
- );
424
- };
425
- harden(prepareStatusManager);
426
-
427
- /** @typedef {ReturnType<typeof prepareStatusManager>} StatusManager */
@@ -1,259 +0,0 @@
1
- import { makeTracer } from '@agoric/internal';
2
- import { prepareDurablePublishKit } from '@agoric/notifier';
3
- import { keyEQ, M } from '@endo/patterns';
4
- import { Fail, quote } from '@endo/errors';
5
- import { CctpTxEvidenceShape, RiskAssessmentShape } from '../type-guards.js';
6
- import { defineInertInvitation } from '../utils/zoe.js';
7
- import { prepareOperatorKit } from './operator-kit.js';
8
-
9
- /**
10
- * @import {Zone} from '@agoric/zone';
11
- * @import {MapStore} from '@agoric/store';
12
- * @import {OperatorKit} from './operator-kit.js';
13
- * @import {CctpTxEvidence, EvidenceWithRisk, RiskAssessment} from '../types.js';
14
- */
15
-
16
- const trace = makeTracer('TxFeed', true);
17
-
18
- /**
19
- * @typedef {Pick<OperatorKit, 'invitationMakers' | 'operator'>} OperatorOfferResult
20
- */
21
-
22
- /** Name in the invitation purse (keyed also by this contract instance) */
23
- export const INVITATION_MAKERS_DESC = 'oracle operator invitation';
24
-
25
- const TransactionFeedKitI = harden({
26
- operatorPowers: M.interface('Transaction Feed Admin', {
27
- attest: M.call(
28
- CctpTxEvidenceShape,
29
- RiskAssessmentShape,
30
- M.string(),
31
- ).returns(),
32
- }),
33
- creator: M.interface('Transaction Feed Creator', {
34
- initOperator: M.call(M.string()).returns({
35
- invitationMakers: M.remotable(),
36
- operator: M.remotable(),
37
- }),
38
- makeOperatorInvitation: M.call(M.string()).returns(M.promise()),
39
- removeOperator: M.call(M.string()).returns(),
40
- }),
41
- public: M.interface('Transaction Feed Public', {
42
- getEvidenceSubscriber: M.call().returns(M.remotable()),
43
- }),
44
- });
45
-
46
- /**
47
- * @param {MapStore<string, RiskAssessment>[]} riskStores
48
- * @param {string} txHash
49
- */
50
- const allRisksIdentified = (riskStores, txHash) => {
51
- /** @type {Set<string>} */
52
- const setOfRisks = new Set();
53
- for (const store of riskStores) {
54
- const next = store.get(txHash);
55
- for (const risk of next.risksIdentified ?? []) {
56
- setOfRisks.add(risk);
57
- }
58
- }
59
- return [...setOfRisks.values()].sort();
60
- };
61
-
62
- export const stateShape = {
63
- operators: M.remotable(),
64
- pending: M.remotable(),
65
- risks: M.remotable(),
66
- };
67
-
68
- /**
69
- * @param {Zone} zone
70
- * @param {ZCF} zcf
71
- */
72
- export const prepareTransactionFeedKit = (zone, zcf) => {
73
- const kinds = zone.mapStore('Kinds');
74
- const makeDurablePublishKit = prepareDurablePublishKit(
75
- kinds,
76
- 'Transaction Feed',
77
- );
78
- /** @type {PublishKit<EvidenceWithRisk>} */
79
- const { publisher, subscriber } = makeDurablePublishKit();
80
-
81
- const makeInertInvitation = defineInertInvitation(zcf, 'submitting evidence');
82
-
83
- const makeOperatorKit = prepareOperatorKit(zone, {
84
- makeInertInvitation,
85
- });
86
-
87
- return zone.exoClassKit(
88
- 'Fast USDC Feed',
89
- TransactionFeedKitI,
90
- () => {
91
- /** @type {MapStore<string, OperatorKit>} */
92
- const operators = zone.mapStore('operators');
93
- /** @type {MapStore<string, MapStore<string, CctpTxEvidence>>} */
94
- const pending = zone.mapStore('pending');
95
- /** @type {MapStore<string, MapStore<string, RiskAssessment>>} */
96
- const risks = zone.mapStore('risks');
97
- return { operators, pending, risks };
98
- },
99
- {
100
- creator: {
101
- /**
102
- * An "operator invitation" is an invitation to be an operator in the
103
- * oracle network, with the able to submit data to submit evidence of
104
- * CCTP transactions.
105
- *
106
- * @param {string} operatorId unique per contract instance
107
- * @returns {Promise<Invitation<OperatorOfferResult>>}
108
- */
109
- makeOperatorInvitation(operatorId) {
110
- const { creator } = this.facets;
111
- trace('makeOperatorInvitation', operatorId);
112
-
113
- return zcf.makeInvitation(
114
- /** @type {OfferHandler<OperatorOfferResult>} */
115
- seat => {
116
- seat.exit();
117
- return creator.initOperator(operatorId);
118
- },
119
- INVITATION_MAKERS_DESC,
120
- );
121
- },
122
- /**
123
- * @param {string} operatorId
124
- * @returns {OperatorOfferResult}
125
- */
126
- initOperator(operatorId) {
127
- const { operators, pending, risks } = this.state;
128
- trace('initOperator', operatorId);
129
-
130
- const operatorKit = makeOperatorKit(
131
- operatorId,
132
- this.facets.operatorPowers,
133
- );
134
- operators.init(operatorId, operatorKit);
135
- pending.init(
136
- operatorId,
137
- zone.detached().mapStore('pending evidence'),
138
- );
139
- risks.init(operatorId, zone.detached().mapStore('risk assessments'));
140
-
141
- // Subset facets to all the off-chain operator needs
142
- const { invitationMakers, operator } = operatorKit;
143
- return {
144
- invitationMakers,
145
- operator,
146
- };
147
- },
148
-
149
- /** @param {string} operatorId */
150
- removeOperator(operatorId) {
151
- const { operators } = this.state;
152
- trace('removeOperator', operatorId);
153
- const operatorKit = operators.get(operatorId);
154
- operatorKit.admin.disable();
155
- operators.delete(operatorId);
156
- },
157
- },
158
- operatorPowers: {
159
- /**
160
- * Add evidence from an operator.
161
- *
162
- * NB: the operatorKit is responsible for
163
- *
164
- * @param {CctpTxEvidence} evidence
165
- * @param {RiskAssessment} riskAssessment
166
- * @param {string} operatorId
167
- */
168
- attest(evidence, riskAssessment, operatorId) {
169
- const { operators, pending, risks } = this.state;
170
- trace('attest', operatorId, evidence);
171
-
172
- // TODO https://github.com/Agoric/agoric-sdk/pull/10720
173
- // TODO validate that it's a valid for Fast USDC before accepting
174
- // E.g. that the `recipientAddress` is the FU settlement account and that
175
- // the EUD is a chain supported by FU.
176
- const { txHash } = evidence;
177
-
178
- // accept the evidence
179
- {
180
- const pendingStore = pending.get(operatorId);
181
- if (pendingStore.has(txHash)) {
182
- trace(`operator ${operatorId} already reported ${txHash}`);
183
- } else {
184
- pendingStore.init(txHash, evidence);
185
- // accept the risk assessment as well
186
- const riskStore = risks.get(operatorId);
187
- riskStore.init(txHash, riskAssessment);
188
- }
189
- }
190
-
191
- // check agreement
192
- const found = [...pending.values()].filter(store =>
193
- store.has(txHash),
194
- );
195
- const minAttestations = Math.ceil(operators.getSize() / 2);
196
- trace(
197
- 'transaction',
198
- txHash,
199
- 'has',
200
- found.length,
201
- 'of',
202
- minAttestations,
203
- 'necessary attestations',
204
- );
205
- if (found.length < minAttestations) {
206
- return;
207
- }
208
-
209
- let lastEvidence;
210
- for (const store of found) {
211
- const next = store.get(txHash);
212
- if (lastEvidence) {
213
- if (keyEQ(lastEvidence, next)) {
214
- lastEvidence = next;
215
- } else {
216
- trace(
217
- '🚨 conflicting evidence for',
218
- txHash,
219
- ':',
220
- lastEvidence,
221
- '!=',
222
- next,
223
- );
224
- Fail`conflicting evidence for ${quote(txHash)}`;
225
- }
226
- }
227
- lastEvidence = next;
228
- }
229
-
230
- const riskStores = [...risks.values()].filter(store =>
231
- store.has(txHash),
232
- );
233
- // take the union of risks identified from all operators
234
- const risksIdentified = allRisksIdentified(riskStores, txHash);
235
-
236
- // sufficient agreement, so remove from pending risks, then publish
237
- for (const store of found) {
238
- store.delete(txHash);
239
- }
240
- for (const store of riskStores) {
241
- store.delete(txHash);
242
- }
243
- trace('publishing evidence', evidence, risksIdentified);
244
- publisher.publish({
245
- evidence,
246
- risk: { risksIdentified },
247
- });
248
- },
249
- },
250
- public: {
251
- getEvidenceSubscriber: () => subscriber,
252
- },
253
- },
254
- { stateShape },
255
- );
256
- };
257
- harden(prepareTransactionFeedKit);
258
-
259
- /** @typedef {ReturnType<ReturnType<typeof prepareTransactionFeedKit>>} TransactionFeedKit */