@agoric/fast-usdc 0.1.1-dev-02967de.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/LICENSE +201 -0
- package/README.md +58 -0
- package/package.json +81 -0
- package/src/cli/bin.js +6 -0
- package/src/cli/cli.js +211 -0
- package/src/cli/config.js +101 -0
- package/src/cli/transfer.js +91 -0
- package/src/constants.js +27 -0
- package/src/exos/README.md +26 -0
- package/src/exos/advancer.js +240 -0
- package/src/exos/liquidity-pool.js +365 -0
- package/src/exos/operator-kit.js +120 -0
- package/src/exos/settler.js +97 -0
- package/src/exos/status-manager.js +176 -0
- package/src/exos/transaction-feed.js +180 -0
- package/src/fast-usdc.contract.js +216 -0
- package/src/fast-usdc.start.js +264 -0
- package/src/pool-share-math.js +189 -0
- package/src/type-guards.js +95 -0
- package/src/types-index.d.ts +1 -0
- package/src/types-index.js +1 -0
- package/src/types.ts +58 -0
- package/src/util/agoric.js +12 -0
- package/src/util/cctp.js +71 -0
- package/src/util/file.js +30 -0
- package/src/util/noble.js +110 -0
- package/src/utils/address.js +71 -0
- package/src/utils/config-marshal.js +130 -0
- package/src/utils/fees.js +58 -0
- package/src/utils/zoe.js +28 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { AmountMath, AmountShape } from '@agoric/ertp';
|
|
2
|
+
import {
|
|
3
|
+
makeRecorderTopic,
|
|
4
|
+
TopicsRecordShape,
|
|
5
|
+
} from '@agoric/zoe/src/contractSupport/topics.js';
|
|
6
|
+
import { SeatShape } from '@agoric/zoe/src/typeGuards.js';
|
|
7
|
+
import { M } from '@endo/patterns';
|
|
8
|
+
import { Fail, q } from '@endo/errors';
|
|
9
|
+
import {
|
|
10
|
+
borrowCalc,
|
|
11
|
+
depositCalc,
|
|
12
|
+
makeParity,
|
|
13
|
+
repayCalc,
|
|
14
|
+
withdrawCalc,
|
|
15
|
+
} from '../pool-share-math.js';
|
|
16
|
+
import {
|
|
17
|
+
makeNatAmountShape,
|
|
18
|
+
makeProposalShapes,
|
|
19
|
+
PoolMetricsShape,
|
|
20
|
+
} from '../type-guards.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @import {Zone} from '@agoric/zone';
|
|
24
|
+
* @import {Remote} from '@agoric/internal'
|
|
25
|
+
* @import {StorageNode} from '@agoric/internal/src/lib-chainStorage.js'
|
|
26
|
+
* @import {MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'
|
|
27
|
+
* @import {USDCProposalShapes, ShareWorth} from '../pool-share-math.js'
|
|
28
|
+
* @import {PoolStats} from '../types.js';
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { add, isEqual, makeEmpty } = AmountMath;
|
|
32
|
+
|
|
33
|
+
/** @param {Brand} brand */
|
|
34
|
+
const makeDust = brand => AmountMath.make(brand, 1n);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verifies that the total pool balance (unencumbered + encumbered) matches the
|
|
38
|
+
* shareWorth numerator. The total pool balance consists of:
|
|
39
|
+
* 1. unencumbered balance - USDC available in the pool for borrowing
|
|
40
|
+
* 2. encumbered balance - USDC currently lent out
|
|
41
|
+
*
|
|
42
|
+
* A negligible `dust` amount is used to initialize shareWorth with a non-zero
|
|
43
|
+
* denominator. It must remain in the pool at all times.
|
|
44
|
+
*
|
|
45
|
+
* @param {ZCFSeat} poolSeat
|
|
46
|
+
* @param {ShareWorth} shareWorth
|
|
47
|
+
* @param {Brand} USDC
|
|
48
|
+
* @param {Amount<'nat'>} encumberedBalance
|
|
49
|
+
*/
|
|
50
|
+
const checkPoolBalance = (poolSeat, shareWorth, USDC, encumberedBalance) => {
|
|
51
|
+
const unencumberedBalance = poolSeat.getAmountAllocated('USDC', USDC);
|
|
52
|
+
const dust = makeDust(USDC);
|
|
53
|
+
const grossBalance = add(add(unencumberedBalance, dust), encumberedBalance);
|
|
54
|
+
isEqual(grossBalance, shareWorth.numerator) ||
|
|
55
|
+
Fail`🚨 pool balance ${q(unencumberedBalance)} and encumbered balance ${q(encumberedBalance)} inconsistent with shareWorth ${q(shareWorth)}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {{
|
|
60
|
+
* Principal: Amount<'nat'>;
|
|
61
|
+
* PoolFee: Amount<'nat'>;
|
|
62
|
+
* ContractFee: Amount<'nat'>;
|
|
63
|
+
* }} RepayAmountKWR
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {{
|
|
68
|
+
* Principal: Payment<'nat'>;
|
|
69
|
+
* PoolFee: Payment<'nat'>;
|
|
70
|
+
* ContractFee: Payment<'nat'>;
|
|
71
|
+
* }} RepayPaymentKWR
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {Zone} zone
|
|
76
|
+
* @param {ZCF} zcf
|
|
77
|
+
* @param {Brand<'nat'>} USDC
|
|
78
|
+
* @param {{
|
|
79
|
+
* makeRecorderKit: MakeRecorderKit;
|
|
80
|
+
* }} tools
|
|
81
|
+
*/
|
|
82
|
+
export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
|
|
83
|
+
return zone.exoClassKit(
|
|
84
|
+
'Liquidity Pool',
|
|
85
|
+
{
|
|
86
|
+
borrower: M.interface('borrower', {
|
|
87
|
+
getBalance: M.call().returns(AmountShape),
|
|
88
|
+
borrow: M.call(
|
|
89
|
+
SeatShape,
|
|
90
|
+
harden({ USDC: makeNatAmountShape(USDC, 1n) }),
|
|
91
|
+
).returns(),
|
|
92
|
+
}),
|
|
93
|
+
repayer: M.interface('repayer', {
|
|
94
|
+
repay: M.call(
|
|
95
|
+
SeatShape,
|
|
96
|
+
harden({
|
|
97
|
+
Principal: makeNatAmountShape(USDC, 1n),
|
|
98
|
+
PoolFee: makeNatAmountShape(USDC, 0n),
|
|
99
|
+
ContractFee: makeNatAmountShape(USDC, 0n),
|
|
100
|
+
}),
|
|
101
|
+
).returns(),
|
|
102
|
+
}),
|
|
103
|
+
external: M.interface('external', {
|
|
104
|
+
publishPoolMetrics: M.call().returns(),
|
|
105
|
+
}),
|
|
106
|
+
depositHandler: M.interface('depositHandler', {
|
|
107
|
+
handle: M.call(SeatShape, M.any()).returns(M.promise()),
|
|
108
|
+
}),
|
|
109
|
+
withdrawHandler: M.interface('withdrawHandler', {
|
|
110
|
+
handle: M.call(SeatShape, M.any()).returns(M.promise()),
|
|
111
|
+
}),
|
|
112
|
+
public: M.interface('public', {
|
|
113
|
+
makeDepositInvitation: M.call().returns(M.promise()),
|
|
114
|
+
makeWithdrawInvitation: M.call().returns(M.promise()),
|
|
115
|
+
getPublicTopics: M.call().returns(TopicsRecordShape),
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
/**
|
|
119
|
+
* @param {ZCFMint<'nat'>} shareMint
|
|
120
|
+
* @param {Remote<StorageNode>} node
|
|
121
|
+
*/
|
|
122
|
+
(shareMint, node) => {
|
|
123
|
+
const { brand: PoolShares } = shareMint.getIssuerRecord();
|
|
124
|
+
const proposalShapes = makeProposalShapes({ USDC, PoolShares });
|
|
125
|
+
const shareWorth = makeParity(makeDust(USDC), PoolShares);
|
|
126
|
+
const { zcfSeat: poolSeat } = zcf.makeEmptySeatKit();
|
|
127
|
+
const { zcfSeat: feeSeat } = zcf.makeEmptySeatKit();
|
|
128
|
+
const poolMetricsRecorderKit = tools.makeRecorderKit(
|
|
129
|
+
node,
|
|
130
|
+
PoolMetricsShape,
|
|
131
|
+
);
|
|
132
|
+
const encumberedBalance = makeEmpty(USDC);
|
|
133
|
+
/** @type {PoolStats} */
|
|
134
|
+
const poolStats = harden({
|
|
135
|
+
totalBorrows: makeEmpty(USDC),
|
|
136
|
+
totalContractFees: makeEmpty(USDC),
|
|
137
|
+
totalPoolFees: makeEmpty(USDC),
|
|
138
|
+
totalRepays: makeEmpty(USDC),
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
/** used for `checkPoolBalance` invariant. aka 'outstanding borrows' */
|
|
142
|
+
encumberedBalance,
|
|
143
|
+
feeSeat,
|
|
144
|
+
poolStats,
|
|
145
|
+
poolMetricsRecorderKit,
|
|
146
|
+
poolSeat,
|
|
147
|
+
PoolShares,
|
|
148
|
+
proposalShapes,
|
|
149
|
+
shareMint,
|
|
150
|
+
shareWorth,
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
borrower: {
|
|
155
|
+
getBalance() {
|
|
156
|
+
const { poolSeat } = this.state;
|
|
157
|
+
return poolSeat.getAmountAllocated('USDC', USDC);
|
|
158
|
+
},
|
|
159
|
+
/**
|
|
160
|
+
* @param {ZCFSeat} toSeat
|
|
161
|
+
* @param {{ USDC: Amount<'nat'>}} amountKWR
|
|
162
|
+
*/
|
|
163
|
+
borrow(toSeat, amountKWR) {
|
|
164
|
+
const { encumberedBalance, poolSeat, poolStats } = this.state;
|
|
165
|
+
|
|
166
|
+
// Validate amount is available in pool
|
|
167
|
+
const post = borrowCalc(
|
|
168
|
+
amountKWR.USDC,
|
|
169
|
+
poolSeat.getAmountAllocated('USDC', USDC),
|
|
170
|
+
encumberedBalance,
|
|
171
|
+
poolStats,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// COMMIT POINT
|
|
175
|
+
try {
|
|
176
|
+
zcf.atomicRearrange(harden([[poolSeat, toSeat, amountKWR]]));
|
|
177
|
+
} catch (cause) {
|
|
178
|
+
const reason = Error('🚨 cannot commit borrow', { cause });
|
|
179
|
+
console.error(reason.message, cause);
|
|
180
|
+
zcf.shutdownWithFailure(reason);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Object.assign(this.state, post);
|
|
184
|
+
this.facets.external.publishPoolMetrics();
|
|
185
|
+
},
|
|
186
|
+
// TODO method to repay failed `LOA.deposit()`
|
|
187
|
+
},
|
|
188
|
+
repayer: {
|
|
189
|
+
/**
|
|
190
|
+
* @param {ZCFSeat} fromSeat
|
|
191
|
+
* @param {RepayAmountKWR} amounts
|
|
192
|
+
*/
|
|
193
|
+
repay(fromSeat, amounts) {
|
|
194
|
+
const {
|
|
195
|
+
encumberedBalance,
|
|
196
|
+
feeSeat,
|
|
197
|
+
poolSeat,
|
|
198
|
+
poolStats,
|
|
199
|
+
shareWorth,
|
|
200
|
+
} = this.state;
|
|
201
|
+
checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
|
|
202
|
+
|
|
203
|
+
const fromSeatAllocation = fromSeat.getCurrentAllocation();
|
|
204
|
+
// Validate allocation equals amounts and Principal <= encumberedBalance
|
|
205
|
+
const post = repayCalc(
|
|
206
|
+
shareWorth,
|
|
207
|
+
fromSeatAllocation,
|
|
208
|
+
amounts,
|
|
209
|
+
encumberedBalance,
|
|
210
|
+
poolStats,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const { ContractFee, ...rest } = amounts;
|
|
214
|
+
|
|
215
|
+
// COMMIT POINT
|
|
216
|
+
try {
|
|
217
|
+
zcf.atomicRearrange(
|
|
218
|
+
harden([
|
|
219
|
+
[
|
|
220
|
+
fromSeat,
|
|
221
|
+
poolSeat,
|
|
222
|
+
rest,
|
|
223
|
+
{ USDC: add(amounts.PoolFee, amounts.Principal) },
|
|
224
|
+
],
|
|
225
|
+
[fromSeat, feeSeat, { ContractFee }, { USDC: ContractFee }],
|
|
226
|
+
]),
|
|
227
|
+
);
|
|
228
|
+
} catch (cause) {
|
|
229
|
+
const reason = Error('🚨 cannot commit repay', { cause });
|
|
230
|
+
console.error(reason.message, cause);
|
|
231
|
+
zcf.shutdownWithFailure(reason);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Object.assign(this.state, post);
|
|
235
|
+
this.facets.external.publishPoolMetrics();
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
external: {
|
|
239
|
+
publishPoolMetrics() {
|
|
240
|
+
const { poolStats, shareWorth, encumberedBalance } = this.state;
|
|
241
|
+
const { recorder } = this.state.poolMetricsRecorderKit;
|
|
242
|
+
// Consumers of this .write() are off-chain / outside the VM.
|
|
243
|
+
// And there's no way to recover from a failed write.
|
|
244
|
+
// So don't await.
|
|
245
|
+
void recorder.write({
|
|
246
|
+
encumberedBalance,
|
|
247
|
+
shareWorth,
|
|
248
|
+
...poolStats,
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
depositHandler: {
|
|
254
|
+
/** @param {ZCFSeat} lp */
|
|
255
|
+
async handle(lp) {
|
|
256
|
+
const { shareWorth, shareMint, poolSeat, encumberedBalance } =
|
|
257
|
+
this.state;
|
|
258
|
+
const { external } = this.facets;
|
|
259
|
+
|
|
260
|
+
/** @type {USDCProposalShapes['deposit']} */
|
|
261
|
+
// @ts-expect-error ensured by proposalShape
|
|
262
|
+
const proposal = lp.getProposal();
|
|
263
|
+
checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
|
|
264
|
+
const post = depositCalc(shareWorth, proposal);
|
|
265
|
+
|
|
266
|
+
// COMMIT POINT
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const mint = shareMint.mintGains(post.payouts);
|
|
270
|
+
this.state.shareWorth = post.shareWorth;
|
|
271
|
+
zcf.atomicRearrange(
|
|
272
|
+
harden([
|
|
273
|
+
// zoe guarantees lp has proposal.give allocated
|
|
274
|
+
[lp, poolSeat, proposal.give],
|
|
275
|
+
// mintGains() above establishes that mint has post.payouts
|
|
276
|
+
[mint, lp, post.payouts],
|
|
277
|
+
]),
|
|
278
|
+
);
|
|
279
|
+
lp.exit();
|
|
280
|
+
mint.exit();
|
|
281
|
+
} catch (cause) {
|
|
282
|
+
const reason = Error('🚨 cannot commit deposit', { cause });
|
|
283
|
+
console.error(reason.message, cause);
|
|
284
|
+
zcf.shutdownWithFailure(reason);
|
|
285
|
+
}
|
|
286
|
+
external.publishPoolMetrics();
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
withdrawHandler: {
|
|
290
|
+
/** @param {ZCFSeat} lp */
|
|
291
|
+
async handle(lp) {
|
|
292
|
+
const { shareWorth, shareMint, poolSeat, encumberedBalance } =
|
|
293
|
+
this.state;
|
|
294
|
+
const { external } = this.facets;
|
|
295
|
+
|
|
296
|
+
/** @type {USDCProposalShapes['withdraw']} */
|
|
297
|
+
// @ts-expect-error ensured by proposalShape
|
|
298
|
+
const proposal = lp.getProposal();
|
|
299
|
+
const { zcfSeat: burn } = zcf.makeEmptySeatKit();
|
|
300
|
+
checkPoolBalance(poolSeat, shareWorth, USDC, encumberedBalance);
|
|
301
|
+
const post = withdrawCalc(shareWorth, proposal);
|
|
302
|
+
|
|
303
|
+
// COMMIT POINT
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
this.state.shareWorth = post.shareWorth;
|
|
307
|
+
zcf.atomicRearrange(
|
|
308
|
+
harden([
|
|
309
|
+
// zoe guarantees lp has proposal.give allocated
|
|
310
|
+
[lp, burn, proposal.give],
|
|
311
|
+
// checkPoolBalance() + withdrawCalc() guarantee poolSeat has enough
|
|
312
|
+
[poolSeat, lp, post.payouts],
|
|
313
|
+
]),
|
|
314
|
+
);
|
|
315
|
+
shareMint.burnLosses(proposal.give, burn);
|
|
316
|
+
lp.exit();
|
|
317
|
+
burn.exit();
|
|
318
|
+
} catch (cause) {
|
|
319
|
+
const reason = Error('🚨 cannot commit withdraw', { cause });
|
|
320
|
+
console.error(reason.message, cause);
|
|
321
|
+
zcf.shutdownWithFailure(reason);
|
|
322
|
+
}
|
|
323
|
+
external.publishPoolMetrics();
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
public: {
|
|
327
|
+
makeDepositInvitation() {
|
|
328
|
+
return zcf.makeInvitation(
|
|
329
|
+
this.facets.depositHandler,
|
|
330
|
+
'Deposit',
|
|
331
|
+
undefined,
|
|
332
|
+
this.state.proposalShapes.deposit,
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
makeWithdrawInvitation() {
|
|
336
|
+
return zcf.makeInvitation(
|
|
337
|
+
this.facets.withdrawHandler,
|
|
338
|
+
'Withdraw',
|
|
339
|
+
undefined,
|
|
340
|
+
this.state.proposalShapes.withdraw,
|
|
341
|
+
);
|
|
342
|
+
},
|
|
343
|
+
getPublicTopics() {
|
|
344
|
+
const { poolMetricsRecorderKit } = this.state;
|
|
345
|
+
return {
|
|
346
|
+
poolMetrics: makeRecorderTopic(
|
|
347
|
+
'poolMetrics',
|
|
348
|
+
poolMetricsRecorderKit,
|
|
349
|
+
),
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
finish: ({ facets: { external } }) => {
|
|
356
|
+
void external.publishPoolMetrics();
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
harden(prepareLiquidityPoolKit);
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @typedef {ReturnType<ReturnType<typeof prepareLiquidityPoolKit>>} LiquidityPoolKit
|
|
365
|
+
*/
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { makeTracer } from '@agoric/internal';
|
|
2
|
+
import { Fail } from '@endo/errors';
|
|
3
|
+
import { M } from '@endo/patterns';
|
|
4
|
+
import { CctpTxEvidenceShape } from '../type-guards.js';
|
|
5
|
+
|
|
6
|
+
const trace = makeTracer('TxOperator');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @import {Zone} from '@agoric/zone';
|
|
10
|
+
* @import {CctpTxEvidence} from '../types.js';
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} OperatorPowers
|
|
15
|
+
* @property {(evidence: CctpTxEvidence, operatorKit: OperatorKit) => void} submitEvidence
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} OperatorStatus
|
|
20
|
+
* @property {boolean} [disabled]
|
|
21
|
+
* @property {string} operatorId
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Readonly<{ operatorId: string, powers: OperatorPowers }> & {disabled: boolean}} State
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const OperatorKitI = {
|
|
29
|
+
admin: M.interface('Admin', {
|
|
30
|
+
disable: M.call().returns(),
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
invitationMakers: M.interface('InvitationMakers', {
|
|
34
|
+
SubmitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()),
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
operator: M.interface('Operator', {
|
|
38
|
+
submitEvidence: M.call(CctpTxEvidenceShape).returns(M.promise()),
|
|
39
|
+
getStatus: M.call().returns(M.record()),
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Zone} zone
|
|
45
|
+
* @param {{ makeInertInvitation: Function }} staticPowers
|
|
46
|
+
*/
|
|
47
|
+
export const prepareOperatorKit = (zone, staticPowers) =>
|
|
48
|
+
zone.exoClassKit(
|
|
49
|
+
'Operator Kit',
|
|
50
|
+
OperatorKitI,
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} operatorId
|
|
53
|
+
* @param {OperatorPowers} powers facet of the durable transaction feed
|
|
54
|
+
* @returns {State}
|
|
55
|
+
*/
|
|
56
|
+
(operatorId, powers) => {
|
|
57
|
+
return {
|
|
58
|
+
operatorId,
|
|
59
|
+
powers,
|
|
60
|
+
disabled: false,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
admin: {
|
|
65
|
+
disable() {
|
|
66
|
+
trace(`operator ${this.state.operatorId} disabled`);
|
|
67
|
+
this.state.disabled = true;
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* NB: when this kit is an offer result, the smart-wallet will detect the `invitationMakers`
|
|
72
|
+
* key and save it for future offers.
|
|
73
|
+
*/
|
|
74
|
+
invitationMakers: {
|
|
75
|
+
/**
|
|
76
|
+
* Provide an API call in the form of an invitation maker, so that the
|
|
77
|
+
* capability is available in the smart-wallet bridge.
|
|
78
|
+
*
|
|
79
|
+
* NB: The `Invitation` object is evidence that the operation took
|
|
80
|
+
* place, rather than as a means of performing it as in the
|
|
81
|
+
* fluxAggregator contract used for price oracles.
|
|
82
|
+
*
|
|
83
|
+
* @param {CctpTxEvidence} evidence
|
|
84
|
+
* @returns {Promise<Invitation>}
|
|
85
|
+
*/
|
|
86
|
+
async SubmitEvidence(evidence) {
|
|
87
|
+
const { operator } = this.facets;
|
|
88
|
+
// TODO(bootstrap integration): cause this call to throw and confirm that it
|
|
89
|
+
// shows up in the the smart-wallet UpdateRecord `error` property
|
|
90
|
+
await operator.submitEvidence(evidence);
|
|
91
|
+
return staticPowers.makeInertInvitation(
|
|
92
|
+
'evidence was pushed in the invitation maker call',
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
operator: {
|
|
97
|
+
/**
|
|
98
|
+
* submit evidence from this operator
|
|
99
|
+
*
|
|
100
|
+
* @param {CctpTxEvidence} evidence
|
|
101
|
+
*/
|
|
102
|
+
async submitEvidence(evidence) {
|
|
103
|
+
const { state } = this;
|
|
104
|
+
!state.disabled || Fail`submitEvidence for disabled operator`;
|
|
105
|
+
const result = state.powers.submitEvidence(evidence, this.facets);
|
|
106
|
+
return result;
|
|
107
|
+
},
|
|
108
|
+
/** @returns {OperatorStatus} */
|
|
109
|
+
getStatus() {
|
|
110
|
+
const { state } = this;
|
|
111
|
+
return {
|
|
112
|
+
operatorId: state.operatorId,
|
|
113
|
+
disabled: state.disabled,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
/** @typedef {ReturnType<ReturnType<typeof prepareOperatorKit>>} OperatorKit */
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { assertAllDefined } from '@agoric/internal';
|
|
2
|
+
import { atob } from '@endo/base64';
|
|
3
|
+
import { makeError, q } from '@endo/errors';
|
|
4
|
+
import { M } from '@endo/patterns';
|
|
5
|
+
|
|
6
|
+
import { addressTools } from '../utils/address.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
|
|
10
|
+
* @import {Denom} from '@agoric/orchestration';
|
|
11
|
+
* @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats';
|
|
12
|
+
* @import {Zone} from '@agoric/zone';
|
|
13
|
+
* @import {NobleAddress} from '../types.js';
|
|
14
|
+
* @import {StatusManager} from './status-manager.js';
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Zone} zone
|
|
19
|
+
* @param {object} caps
|
|
20
|
+
* @param {StatusManager} caps.statusManager
|
|
21
|
+
*/
|
|
22
|
+
export const prepareSettler = (zone, { statusManager }) => {
|
|
23
|
+
assertAllDefined({ statusManager });
|
|
24
|
+
return zone.exoClass(
|
|
25
|
+
'Fast USDC Settler',
|
|
26
|
+
M.interface('SettlerI', {
|
|
27
|
+
receiveUpcall: M.call(M.record()).returns(M.promise()),
|
|
28
|
+
}),
|
|
29
|
+
/**
|
|
30
|
+
*
|
|
31
|
+
* @param {{
|
|
32
|
+
* sourceChannel: IBCChannelID;
|
|
33
|
+
* remoteDenom: Denom
|
|
34
|
+
* }} config
|
|
35
|
+
*/
|
|
36
|
+
config => harden(config),
|
|
37
|
+
{
|
|
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(
|
|
66
|
+
// 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)}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// TODO disperse funds
|
|
79
|
+
// ~1. fee to contractFeeAccount
|
|
80
|
+
// ~2. remainder in poolAccount
|
|
81
|
+
|
|
82
|
+
// update status manager, marking tx `SETTLED`
|
|
83
|
+
statusManager.settle(
|
|
84
|
+
/** @type {NobleAddress} */ (tx.sender),
|
|
85
|
+
BigInt(tx.amount),
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
stateShape: harden({
|
|
91
|
+
sourceChannel: M.string(),
|
|
92
|
+
remoteDenom: M.string(),
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
harden(prepareSettler);
|