@agoric/fast-usdc 0.1.1-other-dev-3eb1a1d.0 → 0.2.0-u18.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 +43 -0
- package/package.json +19 -16
- package/src/cli/bridge-action.js +40 -0
- package/src/cli/cli.js +45 -152
- package/src/cli/config-commands.js +108 -0
- package/src/cli/config.js +15 -9
- package/src/cli/lp-commands.js +178 -0
- package/src/cli/operator-commands.js +145 -0
- package/src/cli/transfer.js +61 -11
- package/src/constants.js +25 -2
- package/src/exos/advancer.js +149 -103
- package/src/exos/liquidity-pool.js +52 -50
- package/src/exos/operator-kit.js +16 -10
- package/src/exos/settler.js +295 -58
- package/src/exos/status-manager.js +270 -57
- package/src/exos/transaction-feed.js +93 -35
- package/src/fast-usdc-policy.core.js +75 -0
- package/src/fast-usdc.contract.js +152 -63
- package/src/fast-usdc.flows.js +10 -0
- package/src/fast-usdc.start.js +44 -14
- package/src/type-guards.js +42 -20
- package/src/types.ts +58 -12
- package/src/util/agoric.js +1 -1
- package/src/util/bank.js +12 -0
- package/src/util/cctp.js +1 -1
- package/src/util/file.js +1 -1
- package/src/utils/deploy-config.js +166 -0
- package/tools/cli-tools.ts +9 -0
- package/tools/mock-io.ts +14 -0
- package/src/exos/README.md +0 -26
- package/src/utils/address.js +0 -71
|
@@ -1,28 +1,39 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
6
|
-
import {
|
|
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,
|
|
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
|
|
18
|
-
* parsable.
|
|
28
|
+
* The key is a composite but not meant to be parsable.
|
|
19
29
|
*
|
|
20
|
-
* @param {NobleAddress}
|
|
30
|
+
* @param {NobleAddress} nfa Noble Forwarding Account (implies EUD)
|
|
21
31
|
* @param {bigint} amount
|
|
22
32
|
* @returns {PendingTxKey}
|
|
23
33
|
*/
|
|
24
|
-
const makePendingTxKey = (
|
|
25
|
-
|
|
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,11 @@ const pendingTxKeyOf = evidence => {
|
|
|
36
47
|
};
|
|
37
48
|
|
|
38
49
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
};
|
|
51
55
|
|
|
52
56
|
/**
|
|
53
57
|
* The `StatusManager` keeps track of Pending and Seen Transactions
|
|
@@ -57,19 +61,83 @@ const seenTxKeyOf = evidence => {
|
|
|
57
61
|
* XXX consider separate facets for `Advancing` and `Settling` capabilities.
|
|
58
62
|
*
|
|
59
63
|
* @param {Zone} zone
|
|
64
|
+
* @param {ERef<StorageNode>} txnsNode
|
|
65
|
+
* @param {StatusManagerPowers} caps
|
|
60
66
|
*/
|
|
61
|
-
export const prepareStatusManager =
|
|
62
|
-
|
|
67
|
+
export const prepareStatusManager = (
|
|
68
|
+
zone,
|
|
69
|
+
txnsNode,
|
|
70
|
+
{
|
|
71
|
+
marshaller,
|
|
72
|
+
log = makeTracer('Advancer', true),
|
|
73
|
+
} = /** @type {StatusManagerPowers} */ ({}),
|
|
74
|
+
) => {
|
|
75
|
+
/**
|
|
76
|
+
* Keyed by a tuple of the Noble Forwarding Account and amount.
|
|
77
|
+
* @type {MapStore<PendingTxKey, PendingTx[]>}
|
|
78
|
+
*/
|
|
63
79
|
const pendingTxs = zone.mapStore('PendingTxs', {
|
|
64
80
|
keyShape: M.string(),
|
|
65
81
|
valueShape: M.arrayOf(PendingTxShape),
|
|
66
82
|
});
|
|
67
83
|
|
|
68
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Transactions seen *ever* by the contract.
|
|
86
|
+
*
|
|
87
|
+
* Note that like all durable stores, this SetStore is stored in IAVL. It
|
|
88
|
+
* grows without bound (though the amount of growth per incoming message to
|
|
89
|
+
* the contract is bounded). At some point in the future we may want to prune.
|
|
90
|
+
* @type {SetStore<EvmHash>}
|
|
91
|
+
*/
|
|
69
92
|
const seenTxs = zone.setStore('SeenTxs', {
|
|
70
93
|
keyShape: M.string(),
|
|
71
94
|
});
|
|
72
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Transactions that have completed, but are still in vstorage.
|
|
98
|
+
*
|
|
99
|
+
* @type {SetStore<EvmHash>}
|
|
100
|
+
*/
|
|
101
|
+
const storedCompletedTxs = zone.setStore('StoredCompletedTxs', {
|
|
102
|
+
keyShape: M.string(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {EvmHash} txId
|
|
107
|
+
* @param {TransactionRecord} record
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
110
|
+
const publishTxnRecord = async (txId, record) => {
|
|
111
|
+
const txNode = E(txnsNode).makeChildNode(txId, {
|
|
112
|
+
sequence: true, // avoid overwriting other output in the block
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// XXX awkward for publish* to update a store, but it's temporary
|
|
116
|
+
if (record.status && TerminalTxStatus[record.status]) {
|
|
117
|
+
// UNTIL https://github.com/Agoric/agoric-sdk/issues/7405
|
|
118
|
+
// Queue it for deletion later because if we deleted it now the earlier
|
|
119
|
+
// writes in this block would be wiped. For now we keep track of what to
|
|
120
|
+
// delete when we know it'll be another block.
|
|
121
|
+
storedCompletedTxs.add(txId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const capData = await E(marshaller).toCapData(record);
|
|
125
|
+
|
|
126
|
+
await E(txNode).setValue(JSON.stringify(capData));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {CctpTxEvidence['txHash']} hash
|
|
131
|
+
* @param {CctpTxEvidence} evidence
|
|
132
|
+
*/
|
|
133
|
+
const publishEvidence = (hash, evidence) => {
|
|
134
|
+
// Don't await, just writing to vstorage.
|
|
135
|
+
void publishTxnRecord(
|
|
136
|
+
hash,
|
|
137
|
+
harden({ evidence, status: TxStatus.Observed }),
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
73
141
|
/**
|
|
74
142
|
* Ensures that `txHash+chainId` has not been processed
|
|
75
143
|
* and adds entry to `seenTxs` set.
|
|
@@ -78,39 +146,130 @@ export const prepareStatusManager = zone => {
|
|
|
78
146
|
*
|
|
79
147
|
* @param {CctpTxEvidence} evidence
|
|
80
148
|
* @param {PendingTxStatus} status
|
|
149
|
+
* @param {string[]} [risksIdentified]
|
|
81
150
|
*/
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
if (seenTxs.has(
|
|
85
|
-
throw makeError(`Transaction already seen: ${q(
|
|
151
|
+
const initPendingTx = (evidence, status, risksIdentified) => {
|
|
152
|
+
const { txHash } = evidence;
|
|
153
|
+
if (seenTxs.has(txHash)) {
|
|
154
|
+
throw makeError(`Transaction already seen: ${q(txHash)}`);
|
|
86
155
|
}
|
|
87
|
-
seenTxs.add(
|
|
156
|
+
seenTxs.add(txHash);
|
|
88
157
|
|
|
89
158
|
appendToStoredArray(
|
|
90
159
|
pendingTxs,
|
|
91
160
|
pendingTxKeyOf(evidence),
|
|
92
161
|
harden({ ...evidence, status }),
|
|
93
162
|
);
|
|
163
|
+
publishEvidence(txHash, evidence);
|
|
164
|
+
if (status === PendingTxStatus.AdvanceSkipped) {
|
|
165
|
+
void publishTxnRecord(txHash, harden({ status, risksIdentified }));
|
|
166
|
+
} else if (status !== PendingTxStatus.Observed) {
|
|
167
|
+
// publishEvidence publishes Observed
|
|
168
|
+
void publishTxnRecord(txHash, harden({ status }));
|
|
169
|
+
}
|
|
94
170
|
};
|
|
95
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Update the pending transaction status.
|
|
174
|
+
*
|
|
175
|
+
* @param {{nfa: NobleAddress, amount: bigint}} keyParts
|
|
176
|
+
* @param {PendingTxStatus} status
|
|
177
|
+
*/
|
|
178
|
+
function setPendingTxStatus({ nfa, amount }, status) {
|
|
179
|
+
const key = makePendingTxKey(nfa, amount);
|
|
180
|
+
pendingTxs.has(key) || Fail`no advancing tx with ${{ nfa, amount }}`;
|
|
181
|
+
const pending = pendingTxs.get(key);
|
|
182
|
+
const ix = pending.findIndex(tx => tx.status === PendingTxStatus.Advancing);
|
|
183
|
+
ix >= 0 || Fail`no advancing tx with ${{ nfa, amount }}`;
|
|
184
|
+
const [prefix, tx, suffix] = [
|
|
185
|
+
pending.slice(0, ix),
|
|
186
|
+
pending[ix],
|
|
187
|
+
pending.slice(ix + 1),
|
|
188
|
+
];
|
|
189
|
+
const txpost = { ...tx, status };
|
|
190
|
+
pendingTxs.set(key, harden([...prefix, txpost, ...suffix]));
|
|
191
|
+
void publishTxnRecord(tx.txHash, harden({ status }));
|
|
192
|
+
}
|
|
193
|
+
|
|
96
194
|
return zone.exo(
|
|
97
195
|
'Fast USDC Status Manager',
|
|
98
196
|
M.interface('StatusManagerI', {
|
|
197
|
+
// TODO: naming scheme for transition events
|
|
99
198
|
advance: M.call(CctpTxEvidenceShape).returns(M.undefined()),
|
|
199
|
+
advanceOutcome: M.call(M.string(), M.nat(), M.boolean()).returns(),
|
|
200
|
+
skipAdvance: M.call(CctpTxEvidenceShape, M.arrayOf(M.string())).returns(
|
|
201
|
+
M.undefined(),
|
|
202
|
+
),
|
|
100
203
|
observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
|
|
101
|
-
|
|
102
|
-
|
|
204
|
+
hasBeenObserved: M.call(CctpTxEvidenceShape).returns(M.boolean()),
|
|
205
|
+
deleteCompletedTxs: M.call().returns(M.undefined()),
|
|
206
|
+
dequeueStatus: M.call(M.string(), M.bigint()).returns(
|
|
207
|
+
M.or(
|
|
208
|
+
{
|
|
209
|
+
txHash: EvmHashShape,
|
|
210
|
+
status: M.or(
|
|
211
|
+
PendingTxStatus.Advanced,
|
|
212
|
+
PendingTxStatus.AdvanceSkipped,
|
|
213
|
+
PendingTxStatus.AdvanceFailed,
|
|
214
|
+
PendingTxStatus.Observed,
|
|
215
|
+
),
|
|
216
|
+
},
|
|
217
|
+
M.undefined(),
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
disbursed: M.call(EvmHashShape, AmountKeywordRecordShape).returns(
|
|
221
|
+
M.undefined(),
|
|
222
|
+
),
|
|
223
|
+
forwarded: M.call(M.opt(EvmHashShape), M.string(), M.nat()).returns(
|
|
224
|
+
M.undefined(),
|
|
225
|
+
),
|
|
103
226
|
lookupPending: M.call(M.string(), M.bigint()).returns(
|
|
104
227
|
M.arrayOf(PendingTxShape),
|
|
105
228
|
),
|
|
106
229
|
}),
|
|
107
230
|
{
|
|
108
231
|
/**
|
|
109
|
-
* Add a new transaction with
|
|
232
|
+
* Add a new transaction with ADVANCING status
|
|
233
|
+
*
|
|
234
|
+
* NB: this acts like observe() but subsequently records an ADVANCING
|
|
235
|
+
* state
|
|
236
|
+
*
|
|
110
237
|
* @param {CctpTxEvidence} evidence
|
|
111
238
|
*/
|
|
112
239
|
advance(evidence) {
|
|
113
|
-
|
|
240
|
+
initPendingTx(evidence, PendingTxStatus.Advancing);
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Add a new transaction with ADVANCE_SKIPPED status
|
|
245
|
+
*
|
|
246
|
+
* NB: this acts like observe() but subsequently records an
|
|
247
|
+
* ADVANCE_SKIPPED state along with risks identified
|
|
248
|
+
*
|
|
249
|
+
* @param {CctpTxEvidence} evidence
|
|
250
|
+
* @param {string[]} risksIdentified
|
|
251
|
+
*/
|
|
252
|
+
skipAdvance(evidence, risksIdentified) {
|
|
253
|
+
initPendingTx(
|
|
254
|
+
evidence,
|
|
255
|
+
PendingTxStatus.AdvanceSkipped,
|
|
256
|
+
risksIdentified,
|
|
257
|
+
);
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Record result of ADVANCING
|
|
262
|
+
*
|
|
263
|
+
* @param {NobleAddress} nfa Noble Forwarding Account
|
|
264
|
+
* @param {import('@agoric/ertp').NatValue} amount
|
|
265
|
+
* @param {boolean} success - Advanced vs. AdvanceFailed
|
|
266
|
+
* @throws {Error} if nothing to advance
|
|
267
|
+
*/
|
|
268
|
+
advanceOutcome(nfa, amount, success) {
|
|
269
|
+
setPendingTxStatus(
|
|
270
|
+
{ nfa, amount },
|
|
271
|
+
success ? PendingTxStatus.Advanced : PendingTxStatus.AdvanceFailed,
|
|
272
|
+
);
|
|
114
273
|
},
|
|
115
274
|
|
|
116
275
|
/**
|
|
@@ -118,53 +277,107 @@ export const prepareStatusManager = zone => {
|
|
|
118
277
|
* @param {CctpTxEvidence} evidence
|
|
119
278
|
*/
|
|
120
279
|
observe(evidence) {
|
|
121
|
-
|
|
280
|
+
initPendingTx(evidence, PendingTxStatus.Observed);
|
|
122
281
|
},
|
|
123
282
|
|
|
124
283
|
/**
|
|
125
|
-
*
|
|
284
|
+
* Note: ADVANCING state implies tx has been OBSERVED
|
|
126
285
|
*
|
|
127
|
-
* @param {
|
|
128
|
-
* @param {bigint} amount
|
|
129
|
-
* @returns {boolean}
|
|
286
|
+
* @param {CctpTxEvidence} evidence
|
|
130
287
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
288
|
+
hasBeenObserved(evidence) {
|
|
289
|
+
return seenTxs.has(evidence.txHash);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
// UNTIL https://github.com/Agoric/agoric-sdk/issues/7405
|
|
293
|
+
deleteCompletedTxs() {
|
|
294
|
+
for (const txHash of storedCompletedTxs.values()) {
|
|
295
|
+
// As of now, setValue('') on a non-sequence node will delete it
|
|
296
|
+
const txNode = E(txnsNode).makeChildNode(txHash, {
|
|
297
|
+
sequence: false,
|
|
298
|
+
});
|
|
299
|
+
void E(txNode)
|
|
300
|
+
.setValue('')
|
|
301
|
+
.then(() => storedCompletedTxs.delete(txHash));
|
|
302
|
+
}
|
|
135
303
|
},
|
|
136
304
|
|
|
137
305
|
/**
|
|
138
|
-
*
|
|
306
|
+
* Remove and return an `ADVANCED` or `OBSERVED` tx waiting to be `SETTLED`.
|
|
139
307
|
*
|
|
140
|
-
* @param {NobleAddress}
|
|
308
|
+
* @param {NobleAddress} nfa
|
|
141
309
|
* @param {bigint} amount
|
|
310
|
+
* @returns {Pick<PendingTx, 'status' | 'txHash'> | undefined} undefined if nothing
|
|
311
|
+
* with this address and amount has been marked pending.
|
|
142
312
|
*/
|
|
143
|
-
|
|
144
|
-
const key = makePendingTxKey(
|
|
313
|
+
dequeueStatus(nfa, amount) {
|
|
314
|
+
const key = makePendingTxKey(nfa, amount);
|
|
315
|
+
if (!pendingTxs.has(key)) return undefined;
|
|
145
316
|
const pending = pendingTxs.get(key);
|
|
146
317
|
|
|
147
|
-
|
|
148
|
-
|
|
318
|
+
const dequeueIdx = pending.findIndex(
|
|
319
|
+
x => x.status !== PendingTxStatus.Advancing,
|
|
320
|
+
);
|
|
321
|
+
if (dequeueIdx < 0) return undefined;
|
|
322
|
+
|
|
323
|
+
if (pending.length > 1) {
|
|
324
|
+
const pendingCopy = [...pending];
|
|
325
|
+
pendingCopy.splice(dequeueIdx, 1);
|
|
326
|
+
pendingTxs.set(key, harden(pendingCopy));
|
|
327
|
+
} else {
|
|
328
|
+
pendingTxs.delete(key);
|
|
149
329
|
}
|
|
150
330
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
|
|
331
|
+
const { status, txHash } = pending[dequeueIdx];
|
|
332
|
+
// TODO: store txHash -> evidence for txs pending settlement?
|
|
333
|
+
// If necessary for vstorage writes in `forwarded` and `settled`
|
|
334
|
+
return harden({ status, txHash });
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Mark a transaction as `DISBURSED`
|
|
339
|
+
*
|
|
340
|
+
* @param {EvmHash} txHash
|
|
341
|
+
* @param {import('./liquidity-pool.js').RepayAmountKWR} split
|
|
342
|
+
*/
|
|
343
|
+
disbursed(txHash, split) {
|
|
344
|
+
void publishTxnRecord(
|
|
345
|
+
txHash,
|
|
346
|
+
harden({ split, status: TxStatus.Disbursed }),
|
|
347
|
+
);
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Mark a transaction as `FORWARDED`
|
|
352
|
+
*
|
|
353
|
+
* @param {EvmHash | undefined} txHash - undefined in case mint before observed
|
|
354
|
+
* @param {NobleAddress} nfa
|
|
355
|
+
* @param {bigint} amount
|
|
356
|
+
*/
|
|
357
|
+
forwarded(txHash, nfa, amount) {
|
|
358
|
+
if (txHash) {
|
|
359
|
+
void publishTxnRecord(txHash, harden({ status: TxStatus.Forwarded }));
|
|
360
|
+
} else {
|
|
361
|
+
// TODO store (early) `Minted` transactions to check against incoming evidence
|
|
362
|
+
log(
|
|
363
|
+
`⚠️ Forwarded minted amount ${amount} from account ${nfa} before it was observed.`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
155
366
|
},
|
|
156
367
|
|
|
157
368
|
/**
|
|
158
369
|
* Lookup all pending entries for a given address and amount
|
|
159
370
|
*
|
|
160
|
-
*
|
|
371
|
+
* XXX only used in tests. should we remove?
|
|
372
|
+
*
|
|
373
|
+
* @param {NobleAddress} nfa
|
|
161
374
|
* @param {bigint} amount
|
|
162
375
|
* @returns {PendingTx[]}
|
|
163
376
|
*/
|
|
164
|
-
lookupPending(
|
|
165
|
-
const key = makePendingTxKey(
|
|
377
|
+
lookupPending(nfa, amount) {
|
|
378
|
+
const key = makePendingTxKey(nfa, amount);
|
|
166
379
|
if (!pendingTxs.has(key)) {
|
|
167
|
-
|
|
380
|
+
return harden([]);
|
|
168
381
|
}
|
|
169
382
|
return pendingTxs.get(key);
|
|
170
383
|
},
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { makeTracer } from '@agoric/internal';
|
|
2
2
|
import { prepareDurablePublishKit } from '@agoric/notifier';
|
|
3
|
-
import { M } from '@endo/patterns';
|
|
4
|
-
import {
|
|
3
|
+
import { keyEQ, M } from '@endo/patterns';
|
|
4
|
+
import { Fail } from '@endo/errors';
|
|
5
|
+
import { CctpTxEvidenceShape, RiskAssessmentShape } from '../type-guards.js';
|
|
5
6
|
import { defineInertInvitation } from '../utils/zoe.js';
|
|
6
7
|
import { prepareOperatorKit } from './operator-kit.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @import {Zone} from '@agoric/zone';
|
|
11
|
+
* @import {MapStore} from '@agoric/store';
|
|
10
12
|
* @import {OperatorKit} from './operator-kit.js';
|
|
11
|
-
* @import {CctpTxEvidence} from '../types.js';
|
|
13
|
+
* @import {CctpTxEvidence, EvidenceWithRisk, RiskAssessment} from '../types.js';
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
const trace = makeTracer('TxFeed', true);
|
|
@@ -18,7 +20,11 @@ export const INVITATION_MAKERS_DESC = 'oracle operator invitation';
|
|
|
18
20
|
|
|
19
21
|
const TransactionFeedKitI = harden({
|
|
20
22
|
operatorPowers: M.interface('Transaction Feed Admin', {
|
|
21
|
-
|
|
23
|
+
attest: M.call(
|
|
24
|
+
CctpTxEvidenceShape,
|
|
25
|
+
RiskAssessmentShape,
|
|
26
|
+
M.string(),
|
|
27
|
+
).returns(),
|
|
22
28
|
}),
|
|
23
29
|
creator: M.interface('Transaction Feed Creator', {
|
|
24
30
|
// TODO narrow the return shape to OperatorKit
|
|
@@ -31,6 +37,22 @@ const TransactionFeedKitI = harden({
|
|
|
31
37
|
}),
|
|
32
38
|
});
|
|
33
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @param {MapStore<string, RiskAssessment>[]} riskStores
|
|
42
|
+
* @param {string} txHash
|
|
43
|
+
*/
|
|
44
|
+
const allRisksIdentified = (riskStores, txHash) => {
|
|
45
|
+
/** @type {Set<string>} */
|
|
46
|
+
const setOfRisks = new Set();
|
|
47
|
+
for (const store of riskStores) {
|
|
48
|
+
const next = store.get(txHash);
|
|
49
|
+
for (const risk of next.risksIdentified ?? []) {
|
|
50
|
+
setOfRisks.add(risk);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...setOfRisks.values()].sort();
|
|
54
|
+
};
|
|
55
|
+
|
|
34
56
|
/**
|
|
35
57
|
* @param {Zone} zone
|
|
36
58
|
* @param {ZCF} zcf
|
|
@@ -41,7 +63,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
|
|
|
41
63
|
kinds,
|
|
42
64
|
'Transaction Feed',
|
|
43
65
|
);
|
|
44
|
-
/** @type {PublishKit<
|
|
66
|
+
/** @type {PublishKit<EvidenceWithRisk>} */
|
|
45
67
|
const { publisher, subscriber } = makeDurablePublishKit();
|
|
46
68
|
|
|
47
69
|
const makeInertInvitation = defineInertInvitation(zcf, 'submitting evidence');
|
|
@@ -55,14 +77,12 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
|
|
|
55
77
|
TransactionFeedKitI,
|
|
56
78
|
() => {
|
|
57
79
|
/** @type {MapStore<string, OperatorKit>} */
|
|
58
|
-
const operators = zone.mapStore('operators'
|
|
59
|
-
durable: true,
|
|
60
|
-
});
|
|
80
|
+
const operators = zone.mapStore('operators');
|
|
61
81
|
/** @type {MapStore<string, MapStore<string, CctpTxEvidence>>} */
|
|
62
|
-
const pending = zone.mapStore('pending'
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return { operators, pending };
|
|
82
|
+
const pending = zone.mapStore('pending');
|
|
83
|
+
/** @type {MapStore<string, MapStore<string, RiskAssessment>>} */
|
|
84
|
+
const risks = zone.mapStore('risks');
|
|
85
|
+
return { operators, pending, risks };
|
|
66
86
|
},
|
|
67
87
|
{
|
|
68
88
|
creator: {
|
|
@@ -89,7 +109,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
|
|
|
89
109
|
},
|
|
90
110
|
/** @param {string} operatorId */
|
|
91
111
|
initOperator(operatorId) {
|
|
92
|
-
const { operators, pending } = this.state;
|
|
112
|
+
const { operators, pending, risks } = this.state;
|
|
93
113
|
trace('initOperator', operatorId);
|
|
94
114
|
|
|
95
115
|
const operatorKit = makeOperatorKit(
|
|
@@ -101,6 +121,7 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
|
|
|
101
121
|
operatorId,
|
|
102
122
|
zone.detached().mapStore('pending evidence'),
|
|
103
123
|
);
|
|
124
|
+
risks.init(operatorId, zone.detached().mapStore('risk assessments'));
|
|
104
125
|
|
|
105
126
|
return operatorKit;
|
|
106
127
|
},
|
|
@@ -118,23 +139,17 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
|
|
|
118
139
|
/**
|
|
119
140
|
* Add evidence from an operator.
|
|
120
141
|
*
|
|
142
|
+
* NB: the operatorKit is responsible for
|
|
143
|
+
*
|
|
121
144
|
* @param {CctpTxEvidence} evidence
|
|
122
|
-
* @param {
|
|
145
|
+
* @param {RiskAssessment} riskAssessment
|
|
146
|
+
* @param {string} operatorId
|
|
123
147
|
*/
|
|
124
|
-
|
|
125
|
-
const { pending } = this.state;
|
|
126
|
-
trace(
|
|
127
|
-
'submitEvidence',
|
|
128
|
-
operatorKit.operator.getStatus().operatorId,
|
|
129
|
-
evidence,
|
|
130
|
-
);
|
|
131
|
-
const { operatorId } = operatorKit.operator.getStatus();
|
|
132
|
-
|
|
133
|
-
// TODO should this verify that the operator is one made by this exo?
|
|
134
|
-
// This doesn't work...
|
|
135
|
-
// operatorKit === operators.get(operatorId) ||
|
|
136
|
-
// Fail`operatorKit mismatch`;
|
|
148
|
+
attest(evidence, riskAssessment, operatorId) {
|
|
149
|
+
const { operators, pending, risks } = this.state;
|
|
150
|
+
trace('attest', operatorId, evidence);
|
|
137
151
|
|
|
152
|
+
// TODO https://github.com/Agoric/agoric-sdk/pull/10720
|
|
138
153
|
// TODO validate that it's a valid for Fast USDC before accepting
|
|
139
154
|
// E.g. that the `recipientAddress` is the FU settlement account and that
|
|
140
155
|
// the EUD is a chain supported by FU.
|
|
@@ -147,6 +162,9 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
|
|
|
147
162
|
trace(`operator ${operatorId} already reported ${txHash}`);
|
|
148
163
|
} else {
|
|
149
164
|
pendingStore.init(txHash, evidence);
|
|
165
|
+
// accept the risk assessment as well
|
|
166
|
+
const riskStore = risks.get(operatorId);
|
|
167
|
+
riskStore.init(txHash, riskAssessment);
|
|
150
168
|
}
|
|
151
169
|
}
|
|
152
170
|
|
|
@@ -154,19 +172,59 @@ export const prepareTransactionFeedKit = (zone, zcf) => {
|
|
|
154
172
|
const found = [...pending.values()].filter(store =>
|
|
155
173
|
store.has(txHash),
|
|
156
174
|
);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
175
|
+
const minAttestations = Math.ceil(operators.getSize() / 2);
|
|
176
|
+
trace(
|
|
177
|
+
'transaction',
|
|
178
|
+
txHash,
|
|
179
|
+
'has',
|
|
180
|
+
found.length,
|
|
181
|
+
'of',
|
|
182
|
+
minAttestations,
|
|
183
|
+
'necessary attestations',
|
|
184
|
+
);
|
|
185
|
+
if (found.length < minAttestations) {
|
|
160
186
|
return;
|
|
161
187
|
}
|
|
162
188
|
|
|
163
|
-
|
|
189
|
+
let lastEvidence;
|
|
190
|
+
for (const store of found) {
|
|
191
|
+
const next = store.get(txHash);
|
|
192
|
+
if (lastEvidence) {
|
|
193
|
+
if (keyEQ(lastEvidence, next)) {
|
|
194
|
+
lastEvidence = next;
|
|
195
|
+
} else {
|
|
196
|
+
trace(
|
|
197
|
+
'🚨 conflicting evidence for',
|
|
198
|
+
txHash,
|
|
199
|
+
':',
|
|
200
|
+
lastEvidence,
|
|
201
|
+
'!=',
|
|
202
|
+
next,
|
|
203
|
+
);
|
|
204
|
+
Fail`conflicting evidence for ${txHash}`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
lastEvidence = next;
|
|
208
|
+
}
|
|
164
209
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
210
|
+
const riskStores = [...risks.values()].filter(store =>
|
|
211
|
+
store.has(txHash),
|
|
212
|
+
);
|
|
213
|
+
// take the union of risks identified from all operators
|
|
214
|
+
const risksIdentified = allRisksIdentified(riskStores, txHash);
|
|
215
|
+
|
|
216
|
+
// sufficient agreement, so remove from pending risks, then publish
|
|
217
|
+
for (const store of found) {
|
|
218
|
+
store.delete(txHash);
|
|
168
219
|
}
|
|
169
|
-
|
|
220
|
+
for (const store of riskStores) {
|
|
221
|
+
store.delete(txHash);
|
|
222
|
+
}
|
|
223
|
+
trace('publishing evidence', evidence, risksIdentified);
|
|
224
|
+
publisher.publish({
|
|
225
|
+
evidence,
|
|
226
|
+
risk: { risksIdentified },
|
|
227
|
+
});
|
|
170
228
|
},
|
|
171
229
|
},
|
|
172
230
|
public: {
|