@agoric/fast-usdc 0.1.1-other-dev-3eb1a1d.0 → 0.1.1-upgrade-19-dev-c605745.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.
Files changed (40) hide show
  1. package/README.md +43 -0
  2. package/package.json +29 -25
  3. package/src/add-operators.core.js +63 -0
  4. package/src/cli/bridge-action.js +40 -0
  5. package/src/cli/cli.js +46 -153
  6. package/src/cli/config-commands.js +108 -0
  7. package/src/cli/config.js +15 -9
  8. package/src/cli/lp-commands.js +160 -0
  9. package/src/cli/operator-commands.js +146 -0
  10. package/src/cli/transfer.js +63 -13
  11. package/src/{util → cli/util}/agoric.js +1 -1
  12. package/src/cli/util/bank.js +12 -0
  13. package/src/{util → cli/util}/cctp.js +1 -1
  14. package/src/{util → cli/util}/file.js +1 -1
  15. package/src/clientSupport.js +98 -0
  16. package/src/constants.js +25 -2
  17. package/src/distribute-fees.core.js +93 -0
  18. package/src/exos/advancer.js +220 -106
  19. package/src/exos/liquidity-pool.js +130 -81
  20. package/src/exos/operator-kit.js +16 -12
  21. package/src/exos/settler.js +360 -64
  22. package/src/exos/status-manager.js +316 -65
  23. package/src/exos/transaction-feed.js +121 -42
  24. package/src/fast-usdc-policy.core.js +65 -0
  25. package/src/fast-usdc.contract.js +165 -84
  26. package/src/fast-usdc.flows.js +10 -0
  27. package/src/main.js +1 -0
  28. package/src/pool-share-math.js +55 -9
  29. package/src/{fast-usdc.start.js → start-fast-usdc.core.js} +48 -86
  30. package/src/type-guards.js +75 -24
  31. package/src/types.ts +89 -14
  32. package/src/utils/chain-policies.js +140 -0
  33. package/src/utils/core-eval.js +73 -0
  34. package/src/utils/deploy-config.js +127 -0
  35. package/src/utils/fees.js +3 -4
  36. package/tools/cli-tools.ts +9 -0
  37. package/tools/mock-io.ts +14 -0
  38. package/src/exos/README.md +0 -26
  39. package/src/utils/address.js +0 -71
  40. /package/src/{util → cli/util}/noble.js +0 -0
@@ -1,28 +1,39 @@
1
- import { M } from '@endo/patterns';
2
- import { makeError, q } from '@endo/errors';
3
-
1
+ import { makeTracer } from '@agoric/internal';
4
2
  import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
5
- import { CctpTxEvidenceShape, PendingTxShape } from '../type-guards.js';
6
- import { PendingTxStatus } from '../constants.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';
7
13
 
8
14
  /**
9
15
  * @import {MapStore, SetStore} from '@agoric/store';
10
16
  * @import {Zone} from '@agoric/zone';
11
- * @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx} from '../types.js';
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.
12
23
  */
13
24
 
14
25
  /**
15
26
  * Create the key for the pendingTxs MapStore.
16
27
  *
17
- * The key is a composite of `txHash` and `chainId` and not meant to be
18
- * parsable.
28
+ * The key is a composite but not meant to be parsable.
19
29
  *
20
- * @param {NobleAddress} addr
30
+ * @param {NobleAddress} nfa Noble Forwarding Account (implies EUD)
21
31
  * @param {bigint} amount
22
32
  * @returns {PendingTxKey}
23
33
  */
24
- const makePendingTxKey = (addr, amount) =>
25
- `pendingTx:${JSON.stringify([addr, String(amount)])}`;
34
+ const makePendingTxKey = (nfa, amount) =>
35
+ // amount can't contain colon
36
+ `pendingTx:${amount}:${nfa}`;
26
37
 
27
38
  /**
28
39
  * Get the key for the pendingTxs MapStore.
@@ -36,18 +47,17 @@ const pendingTxKeyOf = evidence => {
36
47
  };
37
48
 
38
49
  /**
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}
50
+ * @typedef {{
51
+ * log?: LogFn;
52
+ * marshaller: ERef<Marshaller>;
53
+ * }} StatusManagerPowers
46
54
  */
47
- const seenTxKeyOf = evidence => {
48
- const { txHash, chainId } = evidence;
49
- return `seenTx:${JSON.stringify([txHash, chainId])}`;
50
- };
55
+
56
+ export const stateShape = harden({
57
+ pendingSettleTxs: M.remotable(),
58
+ seenTxs: M.remotable(),
59
+ storedCompletedTxs: M.remotable(),
60
+ });
51
61
 
52
62
  /**
53
63
  * The `StatusManager` keeps track of Pending and Seen Transactions
@@ -57,19 +67,89 @@ const seenTxKeyOf = evidence => {
57
67
  * XXX consider separate facets for `Advancing` and `Settling` capabilities.
58
68
  *
59
69
  * @param {Zone} zone
70
+ * @param {ERef<StorageNode>} txnsNode
71
+ * @param {StatusManagerPowers} caps
60
72
  */
61
- export const prepareStatusManager = zone => {
62
- /** @type {MapStore<PendingTxKey, PendingTx[]>} */
63
- const pendingTxs = zone.mapStore('PendingTxs', {
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', {
64
87
  keyShape: M.string(),
65
88
  valueShape: M.arrayOf(PendingTxShape),
66
89
  });
67
90
 
68
- /** @type {SetStore<SeenTxKey>} */
69
- const seenTxs = zone.setStore('SeenTxs', {
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', {
70
114
  keyShape: M.string(),
71
115
  });
72
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
+
73
153
  /**
74
154
  * Ensures that `txHash+chainId` has not been processed
75
155
  * and adds entry to `seenTxs` set.
@@ -78,39 +158,160 @@ export const prepareStatusManager = zone => {
78
158
  *
79
159
  * @param {CctpTxEvidence} evidence
80
160
  * @param {PendingTxStatus} status
161
+ * @param {string[]} [risksIdentified]
81
162
  */
82
- const recordPendingTx = (evidence, status) => {
83
- const seenKey = seenTxKeyOf(evidence);
84
- if (seenTxs.has(seenKey)) {
85
- throw makeError(`Transaction already seen: ${q(seenKey)}`);
163
+ const initPendingTx = (evidence, status, risksIdentified) => {
164
+ const { txHash } = evidence;
165
+ if (seenTxs.has(txHash)) {
166
+ throw makeError(`Transaction already seen: ${q(txHash)}`);
86
167
  }
87
- seenTxs.add(seenKey);
168
+ seenTxs.init(txHash, evidence.blockTimestamp);
88
169
 
89
170
  appendToStoredArray(
90
- pendingTxs,
171
+ pendingSettleTxs,
91
172
  pendingTxKeyOf(evidence),
92
173
  harden({ ...evidence, status }),
93
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
+ }
94
182
  };
95
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
+
96
206
  return zone.exo(
97
207
  'Fast USDC Status Manager',
98
208
  M.interface('StatusManagerI', {
99
- advance: M.call(CctpTxEvidenceShape).returns(M.undefined()),
100
- observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
101
- hasPendingSettlement: M.call(M.string(), M.bigint()).returns(M.boolean()),
102
- settle: M.call(M.string(), M.bigint()).returns(M.undefined()),
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(),
103
231
  lookupPending: M.call(M.string(), M.bigint()).returns(
104
232
  M.arrayOf(PendingTxShape),
105
233
  ),
106
234
  }),
107
235
  {
108
236
  /**
109
- * Add a new transaction with ADVANCED status
237
+ * Add a new transaction with ADVANCING status
238
+ *
239
+ * NB: this acts like observe() but subsequently records an ADVANCING
240
+ * state
241
+ *
110
242
  * @param {CctpTxEvidence} evidence
111
243
  */
112
244
  advance(evidence) {
113
- recordPendingTx(evidence, PendingTxStatus.Advanced);
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);
114
315
  },
115
316
 
116
317
  /**
@@ -118,57 +319,107 @@ export const prepareStatusManager = zone => {
118
319
  * @param {CctpTxEvidence} evidence
119
320
  */
120
321
  observe(evidence) {
121
- recordPendingTx(evidence, PendingTxStatus.Observed);
322
+ initPendingTx(evidence, PendingTxStatus.Observed);
122
323
  },
123
324
 
124
325
  /**
125
- * Find an `ADVANCED` or `OBSERVED` tx waiting to be `SETTLED`
326
+ * Note: ADVANCING state implies tx has been OBSERVED
126
327
  *
127
- * @param {NobleAddress} address
128
- * @param {bigint} amount
129
- * @returns {boolean}
328
+ * @param {CctpTxEvidence} evidence
130
329
  */
131
- hasPendingSettlement(address, amount) {
132
- const key = makePendingTxKey(address, amount);
133
- const pending = pendingTxs.get(key);
134
- return !!pending.length;
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
+ }
135
345
  },
136
346
 
137
347
  /**
138
- * Mark an `ADVANCED` or `OBSERVED` transaction as `SETTLED` and remove it
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.
139
351
  *
140
- * @param {NobleAddress} address
352
+ * @param {NobleAddress} nfa
141
353
  * @param {bigint} amount
354
+ * @returns {Pick<PendingTx, 'status' | 'txHash'> | undefined} undefined if no pending
355
+ * transactions exist for this address and amount combination.
142
356
  */
143
- settle(address, amount) {
144
- const key = makePendingTxKey(address, amount);
145
- const pending = pendingTxs.get(key);
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;
146
367
 
147
- if (!pending.length) {
148
- throw makeError(`No unsettled entry for ${q(key)}`);
368
+ if (remaining.length) {
369
+ pendingSettleTxs.set(key, harden(remaining));
370
+ } else {
371
+ pendingSettleTxs.delete(key);
149
372
  }
150
373
 
151
- const pendingCopy = [...pending];
152
- pendingCopy.shift();
153
- // TODO, vstorage update for `TxStatus.Settled`
154
- pendingTxs.set(key, harden(pendingCopy));
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
+ );
155
403
  },
156
404
 
157
405
  /**
158
406
  * Lookup all pending entries for a given address and amount
159
407
  *
160
- * @param {NobleAddress} address
408
+ * XXX only used in tests. should we remove?
409
+ *
410
+ * @param {NobleAddress} nfa
161
411
  * @param {bigint} amount
162
412
  * @returns {PendingTx[]}
163
413
  */
164
- lookupPending(address, amount) {
165
- const key = makePendingTxKey(address, amount);
166
- if (!pendingTxs.has(key)) {
167
- throw makeError(`Key ${q(key)} not yet observed`);
414
+ lookupPending(nfa, amount) {
415
+ const key = makePendingTxKey(nfa, amount);
416
+ if (!pendingSettleTxs.has(key)) {
417
+ return harden([]);
168
418
  }
169
- return pendingTxs.get(key);
419
+ return pendingSettleTxs.get(key);
170
420
  },
171
421
  },
422
+ { stateShape },
172
423
  );
173
424
  };
174
425
  harden(prepareStatusManager);