@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.
@@ -1,97 +1,334 @@
1
- import { assertAllDefined } from '@agoric/internal';
1
+ import { AmountMath } from '@agoric/ertp';
2
+ import { assertAllDefined, makeTracer } from '@agoric/internal';
3
+ import { ChainAddressShape } from '@agoric/orchestration';
2
4
  import { atob } from '@endo/base64';
3
- import { makeError, q } from '@endo/errors';
5
+ import { E } from '@endo/far';
4
6
  import { M } from '@endo/patterns';
5
7
 
6
- import { addressTools } from '../utils/address.js';
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 { EvmHashShape } from '../type-guards.js';
7
12
 
8
13
  /**
9
14
  * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
10
- * @import {Denom} from '@agoric/orchestration';
15
+ * @import {Denom, OrchestrationAccount, ChainHub, ChainAddress} from '@agoric/orchestration';
16
+ * @import {WithdrawToSeat} from '@agoric/orchestration/src/utils/zoe-tools'
11
17
  * @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats';
12
18
  * @import {Zone} from '@agoric/zone';
13
- * @import {NobleAddress} from '../types.js';
19
+ * @import {HostOf, HostInterface} from '@agoric/async-flow';
20
+ * @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js';
21
+ * @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn} from '../types.js';
14
22
  * @import {StatusManager} from './status-manager.js';
15
23
  */
16
24
 
25
+ /**
26
+ * NOTE: not meant to be parsable.
27
+ *
28
+ * @param {NobleAddress} addr
29
+ * @param {bigint} amount
30
+ */
31
+ const makeMintedEarlyKey = (addr, amount) =>
32
+ `pendingTx:${JSON.stringify([addr, String(amount)])}`;
33
+
17
34
  /**
18
35
  * @param {Zone} zone
19
36
  * @param {object} caps
20
37
  * @param {StatusManager} caps.statusManager
38
+ * @param {Brand<'nat'>} caps.USDC
39
+ * @param {Pick<ZCF, 'makeEmptySeatKit' | 'atomicRearrange'>} caps.zcf
40
+ * @param {FeeConfig} caps.feeConfig
41
+ * @param {HostOf<WithdrawToSeat>} caps.withdrawToSeat
42
+ * @param {import('@agoric/vow').VowTools} caps.vowTools
43
+ * @param {ChainHub} caps.chainHub
44
+ * @param {LogFn} [caps.log]
21
45
  */
22
- export const prepareSettler = (zone, { statusManager }) => {
46
+ export const prepareSettler = (
47
+ zone,
48
+ {
49
+ chainHub,
50
+ feeConfig,
51
+ log = makeTracer('Settler', true),
52
+ statusManager,
53
+ USDC,
54
+ vowTools,
55
+ withdrawToSeat,
56
+ zcf,
57
+ },
58
+ ) => {
23
59
  assertAllDefined({ statusManager });
24
- return zone.exoClass(
60
+ return zone.exoClassKit(
25
61
  'Fast USDC Settler',
26
- M.interface('SettlerI', {
27
- receiveUpcall: M.call(M.record()).returns(M.promise()),
28
- }),
62
+ {
63
+ creator: M.interface('SettlerCreatorI', {
64
+ monitorMintingDeposits: M.callWhen().returns(M.any()),
65
+ setIntermediateRecipient: M.call(ChainAddressShape).returns(),
66
+ }),
67
+ tap: M.interface('SettlerTapI', {
68
+ receiveUpcall: M.call(M.record()).returns(M.promise()),
69
+ }),
70
+ notify: M.interface('SettlerNotifyI', {
71
+ notifyAdvancingResult: M.call(
72
+ M.record(), // XXX fill in details TODO
73
+ M.boolean(),
74
+ ).returns(),
75
+ }),
76
+ self: M.interface('SettlerSelfI', {
77
+ disburse: M.call(EvmHashShape, M.string(), M.nat()).returns(
78
+ M.promise(),
79
+ ),
80
+ forward: M.call(
81
+ M.opt(EvmHashShape),
82
+ M.string(),
83
+ M.nat(),
84
+ M.string(),
85
+ ).returns(),
86
+ }),
87
+ transferHandler: M.interface('SettlerTransferI', {
88
+ onFulfilled: M.call(M.any(), M.record()).returns(),
89
+ onRejected: M.call(M.any(), M.record()).returns(),
90
+ }),
91
+ },
29
92
  /**
30
- *
31
93
  * @param {{
32
94
  * sourceChannel: IBCChannelID;
33
- * remoteDenom: Denom
95
+ * remoteDenom: Denom;
96
+ * repayer: LiquidityPoolKit['repayer'];
97
+ * settlementAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>
98
+ * intermediateRecipient?: ChainAddress;
34
99
  * }} config
35
100
  */
36
- config => harden(config),
101
+ config => {
102
+ log('config', config);
103
+ return {
104
+ ...config,
105
+ // make sure the state record has this property, perhaps with an undefined value
106
+ intermediateRecipient: config.intermediateRecipient,
107
+ /** @type {HostInterface<TargetRegistration>|undefined} */
108
+ registration: undefined,
109
+ /** @type {SetStore<ReturnType<typeof makeMintedEarlyKey>>} */
110
+ mintedEarly: zone.detached().setStore('mintedEarly'),
111
+ };
112
+ },
37
113
  {
38
- /** @param {VTransferIBCEvent} event */
39
- async receiveUpcall(event) {
40
- if (event.packet.source_channel !== this.state.sourceChannel) {
41
- // TODO #10390 log all early returns
42
- // only interested in packets from the issuing chain
43
- return;
44
- }
45
- const tx = /** @type {FungibleTokenPacketData} */ (
46
- JSON.parse(atob(event.packet.data))
47
- );
48
- if (tx.denom !== this.state.remoteDenom) {
49
- // only interested in uusdc
50
- return;
51
- }
52
-
53
- if (!addressTools.hasQueryParams(tx.receiver)) {
54
- // only interested in receivers with query params
55
- return;
56
- }
57
-
58
- const { EUD } = addressTools.getQueryParams(tx.receiver);
59
- if (!EUD) {
60
- // only interested in receivers with EUD parameter
61
- return;
62
- }
63
-
64
- // TODO discern between SETTLED and OBSERVED; each has different fees/destinations
65
- const hasPendingSettlement = statusManager.hasPendingSettlement(
114
+ creator: {
115
+ async monitorMintingDeposits() {
116
+ const { settlementAccount } = this.state;
117
+ const registration = await vowTools.when(
118
+ settlementAccount.monitorTransfers(this.facets.tap),
119
+ );
120
+ assert.typeof(registration, 'object');
121
+ this.state.registration = registration;
122
+ },
123
+ /** @param {ChainAddress} intermediateRecipient */
124
+ setIntermediateRecipient(intermediateRecipient) {
125
+ this.state.intermediateRecipient = intermediateRecipient;
126
+ },
127
+ },
128
+ tap: {
129
+ /** @param {VTransferIBCEvent} event */
130
+ async receiveUpcall(event) {
131
+ log('upcall event', event.packet.sequence, event.blockTime);
132
+ const { sourceChannel, remoteDenom } = this.state;
133
+ const { packet } = event;
134
+ if (packet.source_channel !== sourceChannel) {
135
+ const { source_channel: actual } = packet;
136
+ log('unexpected channel', { actual, expected: sourceChannel });
137
+ return;
138
+ }
139
+
140
+ // TODO: why is it safe to cast this without a runtime check?
141
+ const tx = /** @type {FungibleTokenPacketData} */ (
142
+ JSON.parse(atob(packet.data))
143
+ );
144
+
66
145
  // given the sourceChannel check, we can be certain of this cast
67
- /** @type {NobleAddress} */ (tx.sender),
68
- BigInt(tx.amount),
69
- );
70
- if (!hasPendingSettlement) {
71
- // TODO FAILURE PATH -> put money in recovery account or .transfer to receiver
72
- // TODO should we have an ORPHANED TxStatus for this?
73
- throw makeError(
74
- `🚨 No pending settlement found for ${q(tx.sender)} ${q(tx.amount)}`,
146
+ const nfa = /** @type {NobleAddress} */ (tx.sender);
147
+
148
+ if (tx.denom !== remoteDenom) {
149
+ const { denom: actual } = tx;
150
+ log('unexpected denom', { actual, expected: remoteDenom });
151
+ return;
152
+ }
153
+
154
+ let EUD;
155
+ try {
156
+ ({ EUD } = decodeAddressHook(tx.receiver).query);
157
+ if (!EUD) {
158
+ log('no EUD parameter', tx.receiver);
159
+ return;
160
+ }
161
+ if (typeof EUD !== 'string') {
162
+ log('EUD is not a string', EUD);
163
+ return;
164
+ }
165
+ } catch (e) {
166
+ log('no query params', tx.receiver);
167
+ return;
168
+ }
169
+
170
+ const amount = BigInt(tx.amount); // TODO: what if this throws?
171
+
172
+ const { self } = this.facets;
173
+ const found = statusManager.dequeueStatus(nfa, amount);
174
+ log('dequeued', found, 'for', nfa, amount);
175
+ switch (found?.status) {
176
+ case PendingTxStatus.Advanced:
177
+ return self.disburse(found.txHash, nfa, amount);
178
+
179
+ case PendingTxStatus.Advancing:
180
+ this.state.mintedEarly.add(makeMintedEarlyKey(nfa, amount));
181
+ return;
182
+
183
+ case PendingTxStatus.Observed:
184
+ case PendingTxStatus.AdvanceSkipped:
185
+ case PendingTxStatus.AdvanceFailed:
186
+ return self.forward(found.txHash, nfa, amount, EUD);
187
+
188
+ case undefined:
189
+ default:
190
+ log('⚠️ tap: no status for ', nfa, amount);
191
+ }
192
+ },
193
+ },
194
+ notify: {
195
+ /**
196
+ * @param {object} ctx
197
+ * @param {EvmHash} ctx.txHash
198
+ * @param {NobleAddress} ctx.forwardingAddress
199
+ * @param {Amount<'nat'>} ctx.fullAmount
200
+ * @param {ChainAddress} ctx.destination
201
+ * @param {boolean} success
202
+ * @returns {void}
203
+ */
204
+ notifyAdvancingResult(
205
+ { txHash, forwardingAddress, fullAmount, destination },
206
+ success,
207
+ ) {
208
+ const { mintedEarly } = this.state;
209
+ const { value: fullValue } = fullAmount;
210
+ const key = makeMintedEarlyKey(forwardingAddress, fullValue);
211
+ if (mintedEarly.has(key)) {
212
+ mintedEarly.delete(key);
213
+ if (success) {
214
+ void this.facets.self.disburse(
215
+ txHash,
216
+ forwardingAddress,
217
+ fullValue,
218
+ );
219
+ } else {
220
+ void this.facets.self.forward(
221
+ txHash,
222
+ forwardingAddress,
223
+ fullValue,
224
+ destination.value,
225
+ );
226
+ }
227
+ } else {
228
+ statusManager.advanceOutcome(forwardingAddress, fullValue, success);
229
+ }
230
+ },
231
+ },
232
+ self: {
233
+ /**
234
+ * @param {EvmHash} txHash
235
+ * @param {NobleAddress} nfa
236
+ * @param {NatValue} fullValue
237
+ */
238
+ async disburse(txHash, nfa, fullValue) {
239
+ const { repayer, settlementAccount } = this.state;
240
+ const received = AmountMath.make(USDC, fullValue);
241
+ const { zcfSeat: settlingSeat } = zcf.makeEmptySeatKit();
242
+ const { calculateSplit } = makeFeeTools(feeConfig);
243
+ const split = calculateSplit(received);
244
+ log('disbursing', split);
245
+
246
+ // If this throws, which arguably can't occur since we don't ever
247
+ // withdraw more than has been deposited (as denoted by
248
+ // `FungibleTokenPacketData`), funds will remain in the
249
+ // `settlementAccount`. A remediation can occur in a future upgrade.
250
+ await vowTools.when(
251
+ withdrawToSeat(
252
+ // @ts-expect-error LocalAccountMethods vs OrchestrationAccount
253
+ settlementAccount,
254
+ settlingSeat,
255
+ harden({ In: received }),
256
+ ),
257
+ );
258
+ zcf.atomicRearrange(
259
+ harden([[settlingSeat, settlingSeat, { In: received }, split]]),
75
260
  );
76
- }
261
+ repayer.repay(settlingSeat, split);
77
262
 
78
- // TODO disperse funds
79
- // ~1. fee to contractFeeAccount
80
- // ~2. remainder in poolAccount
263
+ // update status manager, marking tx `SETTLED`
264
+ statusManager.disbursed(txHash, split);
265
+ },
266
+ /**
267
+ * @param {EvmHash} txHash
268
+ * @param {NobleAddress} nfa
269
+ * @param {NatValue} fullValue
270
+ * @param {string} EUD
271
+ */
272
+ forward(txHash, nfa, fullValue, EUD) {
273
+ const { settlementAccount, intermediateRecipient } = this.state;
81
274
 
82
- // update status manager, marking tx `SETTLED`
83
- statusManager.settle(
84
- /** @type {NobleAddress} */ (tx.sender),
85
- BigInt(tx.amount),
86
- );
275
+ const dest = chainHub.makeChainAddress(EUD);
276
+
277
+ // TODO? statusManager.forwarding(txHash, sender, amount);
278
+ const txfrV = E(settlementAccount).transfer(
279
+ dest,
280
+ AmountMath.make(USDC, fullValue),
281
+ { forwardOpts: { intermediateRecipient } },
282
+ );
283
+ void vowTools.watch(txfrV, this.facets.transferHandler, {
284
+ txHash,
285
+ nfa,
286
+ fullValue,
287
+ });
288
+ },
289
+ },
290
+ transferHandler: {
291
+ /**
292
+ * @param {unknown} _result
293
+ * @param {SettlerTransferCtx} ctx
294
+ *
295
+ * @typedef {{
296
+ * txHash: EvmHash;
297
+ * nfa: NobleAddress;
298
+ * fullValue: NatValue;
299
+ * }} SettlerTransferCtx
300
+ */
301
+ onFulfilled(_result, ctx) {
302
+ const { txHash, nfa, fullValue } = ctx;
303
+ statusManager.forwarded(txHash, nfa, fullValue);
304
+ },
305
+ /**
306
+ * @param {unknown} reason
307
+ * @param {SettlerTransferCtx} ctx
308
+ */
309
+ onRejected(reason, ctx) {
310
+ log('⚠️ transfer rejected!', reason, ctx);
311
+ // const { txHash, nfa, amount } = ctx;
312
+ // TODO(#10510): statusManager.forwardFailed(txHash, nfa, amount);
313
+ },
87
314
  },
88
315
  },
89
316
  {
90
317
  stateShape: harden({
318
+ repayer: M.remotable('Repayer'),
319
+ settlementAccount: M.remotable('Account'),
320
+ registration: M.or(M.undefined(), M.remotable('Registration')),
91
321
  sourceChannel: M.string(),
92
322
  remoteDenom: M.string(),
323
+ mintedEarly: M.remotable('mintedEarly'),
324
+ intermediateRecipient: M.opt(ChainAddressShape),
93
325
  }),
94
326
  },
95
327
  );
96
328
  };
97
329
  harden(prepareSettler);
330
+
331
+ /**
332
+ * XXX consider using pickFacet (do we have pickFacets?)
333
+ * @typedef {ReturnType<ReturnType<typeof prepareSettler>>} SettlerKit
334
+ */