@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
package/src/exos/settler.js
CHANGED
|
@@ -1,97 +1,334 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
5
|
+
import { E } from '@endo/far';
|
|
4
6
|
import { M } from '@endo/patterns';
|
|
5
7
|
|
|
6
|
-
import {
|
|
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 {
|
|
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 = (
|
|
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.
|
|
60
|
+
return zone.exoClassKit(
|
|
25
61
|
'Fast USDC Settler',
|
|
26
|
-
|
|
27
|
-
|
|
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 =>
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
*/
|