@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.
- package/README.md +17 -91
- package/package.json +22 -29
- package/src/cli/config.js +1 -1
- package/src/cli/lp-commands.js +2 -2
- package/src/cli/operator-commands.js +1 -4
- package/src/cli/transfer.js +2 -2
- package/src/constants.js +2 -2
- package/src/operator-kit-interface.js +29 -0
- package/src/pool-share-math.js +18 -26
- package/src/type-guards.js +49 -13
- package/src/types.ts +58 -12
- package/src/utils/fees.js +104 -18
- package/tools/mock-evidence.ts +208 -0
- package/src/add-operators.core.js +0 -63
- package/src/distribute-fees.core.js +0 -93
- package/src/exos/advancer.js +0 -369
- package/src/exos/liquidity-pool.js +0 -414
- package/src/exos/operator-kit.js +0 -124
- package/src/exos/settler.js +0 -393
- package/src/exos/status-manager.js +0 -427
- package/src/exos/transaction-feed.js +0 -259
- package/src/fast-usdc-policy.core.js +0 -65
- package/src/fast-usdc.contract.js +0 -316
- package/src/fast-usdc.flows.js +0 -23
- package/src/start-fast-usdc.core.js +0 -246
- package/src/utils/chain-policies.js +0 -140
- package/src/utils/config-marshal.js +0 -130
- package/src/utils/core-eval.js +0 -73
- package/src/utils/deploy-config.js +0 -127
- package/src/utils/zoe.js +0 -28
|
@@ -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 */
|