@agoric/fast-usdc 0.2.0-u19.2 → 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,393 +0,0 @@
1
- import { AmountMath } from '@agoric/ertp';
2
- import { assertAllDefined, makeTracer } from '@agoric/internal';
3
- import { ChainAddressShape } from '@agoric/orchestration';
4
- import { atob } from '@endo/base64';
5
- import { E } from '@endo/far';
6
- import { M } from '@endo/patterns';
7
-
8
- import { decodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
9
- import { PendingTxStatus } from '../constants.js';
10
- import { makeFeeTools } from '../utils/fees.js';
11
- import {
12
- CctpTxEvidenceShape,
13
- EvmHashShape,
14
- makeNatAmountShape,
15
- } from '../type-guards.js';
16
-
17
- /**
18
- * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
19
- * @import {Amount, Brand, NatValue, Payment} from '@agoric/ertp';
20
- * @import {Denom, OrchestrationAccount, ChainHub, ChainAddress} from '@agoric/orchestration';
21
- * @import {WithdrawToSeat} from '@agoric/orchestration/src/utils/zoe-tools'
22
- * @import {IBCChannelID, IBCPacket, VTransferIBCEvent} from '@agoric/vats';
23
- * @import {Zone} from '@agoric/zone';
24
- * @import {HostOf, HostInterface} from '@agoric/async-flow';
25
- * @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js';
26
- * @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn, CctpTxEvidence} from '../types.js';
27
- * @import {StatusManager} from './status-manager.js';
28
- */
29
-
30
- /**
31
- * @param {IBCPacket} data
32
- * @param {string} remoteDenom
33
- * @returns {{ nfa: NobleAddress, amount: bigint, EUD: string } | {error: object[]}}
34
- */
35
- const decodeEventPacket = ({ data }, remoteDenom) => {
36
- // NB: may not be a FungibleTokenPacketData or even JSON
37
- /** @type {FungibleTokenPacketData} */
38
- let tx;
39
- try {
40
- tx = JSON.parse(atob(data));
41
- } catch (e) {
42
- return { error: ['could not parse packet data', data] };
43
- }
44
-
45
- // given the sourceChannel check, we can be certain of this cast
46
- const nfa = /** @type {NobleAddress} */ (tx.sender);
47
-
48
- if (tx.denom !== remoteDenom) {
49
- const { denom: actual } = tx;
50
- return { error: ['unexpected denom', { actual, expected: remoteDenom }] };
51
- }
52
-
53
- let EUD;
54
- try {
55
- ({ EUD } = decodeAddressHook(tx.receiver).query);
56
- if (!EUD) {
57
- return { error: ['no EUD parameter', tx.receiver] };
58
- }
59
- if (typeof EUD !== 'string') {
60
- return { error: ['EUD is not a string', EUD] };
61
- }
62
- } catch (e) {
63
- return { error: ['no query params', tx.receiver] };
64
- }
65
-
66
- let amount;
67
- try {
68
- amount = BigInt(tx.amount);
69
- } catch (e) {
70
- return { error: ['invalid amount', tx.amount] };
71
- }
72
-
73
- return { nfa, amount, EUD };
74
- };
75
- harden(decodeEventPacket);
76
-
77
- /**
78
- * NOTE: not meant to be parsable.
79
- *
80
- * @param {NobleAddress} addr
81
- * @param {bigint} amount
82
- */
83
- const makeMintedEarlyKey = (addr, amount) =>
84
- `pendingTx:${JSON.stringify([addr, String(amount)])}`;
85
-
86
- /** @param {Brand<'nat'>} USDC */
87
- export const makeAdvanceDetailsShape = USDC =>
88
- harden({
89
- destination: ChainAddressShape,
90
- forwardingAddress: M.string(),
91
- fullAmount: makeNatAmountShape(USDC),
92
- txHash: EvmHashShape,
93
- });
94
-
95
- export const stateShape = harden({
96
- repayer: M.remotable('Repayer'),
97
- settlementAccount: M.remotable('Account'),
98
- registration: M.or(M.undefined(), M.remotable('Registration')),
99
- sourceChannel: M.string(),
100
- remoteDenom: M.string(),
101
- mintedEarly: M.remotable('mintedEarly'),
102
- intermediateRecipient: M.opt(ChainAddressShape),
103
- });
104
-
105
- /**
106
- * @param {Zone} zone
107
- * @param {object} caps
108
- * @param {StatusManager} caps.statusManager
109
- * @param {Brand<'nat'>} caps.USDC
110
- * @param {Pick<ZCF, 'makeEmptySeatKit' | 'atomicRearrange'>} caps.zcf
111
- * @param {FeeConfig} caps.feeConfig
112
- * @param {HostOf<WithdrawToSeat>} caps.withdrawToSeat
113
- * @param {import('@agoric/vow').VowTools} caps.vowTools
114
- * @param {ChainHub} caps.chainHub
115
- * @param {LogFn} [caps.log]
116
- */
117
- export const prepareSettler = (
118
- zone,
119
- {
120
- chainHub,
121
- feeConfig,
122
- log = makeTracer('Settler', true),
123
- statusManager,
124
- USDC,
125
- vowTools,
126
- withdrawToSeat,
127
- zcf,
128
- },
129
- ) => {
130
- assertAllDefined({ statusManager });
131
- return zone.exoClassKit(
132
- 'Fast USDC Settler',
133
- {
134
- creator: M.interface('SettlerCreatorI', {
135
- monitorMintingDeposits: M.callWhen().returns(M.any()),
136
- setIntermediateRecipient: M.call(ChainAddressShape).returns(),
137
- }),
138
- tap: M.interface('SettlerTapI', {
139
- receiveUpcall: M.call(M.record()).returns(M.promise()),
140
- }),
141
- notifier: M.interface('SettlerNotifyI', {
142
- notifyAdvancingResult: M.call(
143
- makeAdvanceDetailsShape(USDC),
144
- M.boolean(),
145
- ).returns(),
146
- checkMintedEarly: M.call(
147
- CctpTxEvidenceShape,
148
- ChainAddressShape,
149
- ).returns(M.boolean()),
150
- }),
151
- self: M.interface('SettlerSelfI', {
152
- disburse: M.call(EvmHashShape, M.nat()).returns(M.promise()),
153
- forward: M.call(EvmHashShape, M.nat(), M.string()).returns(),
154
- }),
155
- transferHandler: M.interface('SettlerTransferI', {
156
- onFulfilled: M.call(M.undefined(), M.string()).returns(),
157
- onRejected: M.call(M.error(), M.string()).returns(),
158
- }),
159
- },
160
- /**
161
- * @param {{
162
- * sourceChannel: IBCChannelID;
163
- * remoteDenom: Denom;
164
- * repayer: LiquidityPoolKit['repayer'];
165
- * settlementAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>
166
- * intermediateRecipient?: ChainAddress;
167
- * }} config
168
- */
169
- config => {
170
- log('config', config);
171
- return {
172
- ...config,
173
- // make sure the state record has this property, perhaps with an undefined value
174
- intermediateRecipient: config.intermediateRecipient,
175
- /** @type {HostInterface<TargetRegistration>|undefined} */
176
- registration: undefined,
177
- /** @type {SetStore<ReturnType<typeof makeMintedEarlyKey>>} */
178
- mintedEarly: zone.detached().setStore('mintedEarly'),
179
- };
180
- },
181
- {
182
- creator: {
183
- async monitorMintingDeposits() {
184
- const { settlementAccount } = this.state;
185
- const registration = await vowTools.when(
186
- settlementAccount.monitorTransfers(this.facets.tap),
187
- );
188
- assert.typeof(registration, 'object');
189
- this.state.registration = registration;
190
- },
191
- /** @param {ChainAddress} intermediateRecipient */
192
- setIntermediateRecipient(intermediateRecipient) {
193
- this.state.intermediateRecipient = intermediateRecipient;
194
- },
195
- },
196
- tap: {
197
- /** @param {VTransferIBCEvent} event */
198
- async receiveUpcall(event) {
199
- log('upcall event', event.packet.sequence, event.blockTime);
200
- const { sourceChannel, remoteDenom } = this.state;
201
- const { packet } = event;
202
- if (packet.source_channel !== sourceChannel) {
203
- const { source_channel: actual } = packet;
204
- log('unexpected channel', { actual, expected: sourceChannel });
205
- return;
206
- }
207
-
208
- const decoded = decodeEventPacket(event.packet, remoteDenom);
209
- if ('error' in decoded) {
210
- log('invalid event packet', decoded.error);
211
- return;
212
- }
213
-
214
- const { nfa, amount, EUD } = decoded;
215
- const { self } = this.facets;
216
- const found = statusManager.dequeueStatus(nfa, amount);
217
- log('dequeued', found, 'for', nfa, amount);
218
- switch (found?.status) {
219
- case PendingTxStatus.Advanced:
220
- return self.disburse(found.txHash, amount);
221
-
222
- case PendingTxStatus.Advancing:
223
- log('⚠️ tap: minted while advancing', nfa, amount);
224
- this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount));
225
- return;
226
-
227
- case PendingTxStatus.Observed:
228
- case PendingTxStatus.AdvanceSkipped:
229
- case PendingTxStatus.AdvanceFailed:
230
- return self.forward(found.txHash, amount, EUD);
231
-
232
- case undefined:
233
- default:
234
- log('⚠️ tap: minted before observed', nfa, amount);
235
- // XXX consider capturing in vstorage
236
- // we would need a new key, as this does not have a txHash
237
- this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount));
238
- }
239
- },
240
- },
241
- notifier: {
242
- /**
243
- * @param {object} ctx
244
- * @param {EvmHash} ctx.txHash
245
- * @param {NobleAddress} ctx.forwardingAddress
246
- * @param {Amount<'nat'>} ctx.fullAmount
247
- * @param {ChainAddress} ctx.destination
248
- * @param {boolean} success
249
- * @returns {void}
250
- */
251
- notifyAdvancingResult(
252
- { txHash, forwardingAddress, fullAmount, destination },
253
- success,
254
- ) {
255
- const { mintedEarly } = this.state;
256
- const { value: fullValue } = fullAmount;
257
- const key = makeMintedEarlyKey(forwardingAddress, fullValue);
258
- if (mintedEarly.has(key)) {
259
- mintedEarly.delete(key);
260
- statusManager.advanceOutcomeForMintedEarly(txHash, success);
261
- if (success) {
262
- void this.facets.self.disburse(txHash, fullValue);
263
- } else {
264
- void this.facets.self.forward(
265
- txHash,
266
- fullValue,
267
- destination.value,
268
- );
269
- }
270
- } else {
271
- statusManager.advanceOutcome(forwardingAddress, fullValue, success);
272
- }
273
- },
274
- /**
275
- * @param {CctpTxEvidence} evidence
276
- * @param {ChainAddress} destination
277
- * @returns {boolean}
278
- * @throws {Error} if minted early, so advancer doesn't advance
279
- */
280
- checkMintedEarly(evidence, destination) {
281
- const {
282
- tx: { forwardingAddress, amount },
283
- txHash,
284
- } = evidence;
285
- const key = makeMintedEarlyKey(forwardingAddress, amount);
286
- const { mintedEarly } = this.state;
287
- if (mintedEarly.has(key)) {
288
- log(
289
- 'matched minted early key, initiating forward',
290
- forwardingAddress,
291
- amount,
292
- );
293
- mintedEarly.delete(key);
294
- statusManager.advanceOutcomeForUnknownMint(evidence);
295
- void this.facets.self.forward(txHash, amount, destination.value);
296
- return true;
297
- }
298
- return false;
299
- },
300
- },
301
- self: {
302
- /**
303
- * @param {EvmHash} txHash
304
- * @param {NatValue} fullValue
305
- */
306
- async disburse(txHash, fullValue) {
307
- const { repayer, settlementAccount } = this.state;
308
- const received = AmountMath.make(USDC, fullValue);
309
- const { zcfSeat: settlingSeat } = zcf.makeEmptySeatKit();
310
- const { calculateSplit } = makeFeeTools(feeConfig);
311
- const split = calculateSplit(received);
312
- log('disbursing', split);
313
-
314
- // If this throws, which arguably can't occur since we don't ever
315
- // withdraw more than has been deposited (as denoted by
316
- // `FungibleTokenPacketData`), funds will remain in the
317
- // `settlementAccount`. A remediation can occur in a future upgrade.
318
- await vowTools.when(
319
- withdrawToSeat(
320
- // @ts-expect-error LocalAccountMethods vs OrchestrationAccount
321
- settlementAccount,
322
- settlingSeat,
323
- harden({ In: received }),
324
- ),
325
- );
326
- zcf.atomicRearrange(
327
- harden([[settlingSeat, settlingSeat, { In: received }, split]]),
328
- );
329
- repayer.repay(settlingSeat, split);
330
- settlingSeat.exit();
331
-
332
- // update status manager, marking tx `DISBURSED`
333
- statusManager.disbursed(txHash, split);
334
- },
335
- /**
336
- * @param {EvmHash} txHash
337
- * @param {NatValue} fullValue
338
- * @param {string} EUD
339
- */
340
- forward(txHash, fullValue, EUD) {
341
- const { settlementAccount, intermediateRecipient } = this.state;
342
-
343
- const dest = (() => {
344
- try {
345
- return chainHub.makeChainAddress(EUD);
346
- } catch (e) {
347
- log('⚠️ forward transfer failed!', e, txHash);
348
- statusManager.forwarded(txHash, false);
349
- return null;
350
- }
351
- })();
352
- if (!dest) return;
353
-
354
- const txfrV = E(settlementAccount).transfer(
355
- dest,
356
- AmountMath.make(USDC, fullValue),
357
- { forwardOpts: { intermediateRecipient } },
358
- );
359
- void vowTools.watch(txfrV, this.facets.transferHandler, txHash);
360
- },
361
- },
362
- transferHandler: {
363
- /**
364
- * @param {unknown} _result
365
- * @param {EvmHash} txHash
366
- */
367
- onFulfilled(_result, txHash) {
368
- // update status manager, marking tx `FORWARDED` without fee split
369
- statusManager.forwarded(txHash, true);
370
- },
371
- /**
372
- * @param {unknown} reason
373
- * @param {EvmHash} txHash
374
- */
375
- onRejected(reason, txHash) {
376
- // funds remain in `settlementAccount` and must be recovered via a
377
- // contract upgrade
378
- log('🚨 forward transfer rejected!', reason, txHash);
379
- // update status manager, flagging a terminal state that needs to be
380
- // manual intervention or a code update to remediate
381
- statusManager.forwarded(txHash, false);
382
- },
383
- },
384
- },
385
- {
386
- stateShape,
387
- },
388
- );
389
- };
390
- harden(prepareSettler);
391
-
392
- // Expose the whole kit because the contract needs `creatorFacet` and the Advancer needs `notifier`
393
- /** @typedef {ReturnType<ReturnType<typeof prepareSettler>>} SettlerKit */