@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.
@@ -0,0 +1,91 @@
1
+ /* global globalThis */
2
+
3
+ import { makeVStorage } from '@agoric/client-utils';
4
+ import { depositForBurn, makeProvider } from '../util/cctp.js';
5
+ import {
6
+ makeSigner,
7
+ queryForwardingAccount,
8
+ registerFwdAccount,
9
+ } from '../util/noble.js';
10
+ import { queryFastUSDCLocalChainAccount } from '../util/agoric.js';
11
+
12
+ /** @import { file } from '../util/file' */
13
+ /** @import { VStorage } from '@agoric/client-utils' */
14
+ /** @import { SigningStargateClient } from '@cosmjs/stargate' */
15
+ /** @import { JsonRpcProvider as ethProvider } from 'ethers' */
16
+
17
+ const transfer = async (
18
+ /** @type {file} */ configFile,
19
+ /** @type {string} */ amount,
20
+ /** @type {string} */ destination,
21
+ out = console,
22
+ fetch = globalThis.fetch,
23
+ /** @type {VStorage | undefined} */ vstorage,
24
+ /** @type {{signer: SigningStargateClient, address: string} | undefined} */ nobleSigner,
25
+ /** @type {ethProvider | undefined} */ ethProvider,
26
+ ) => {
27
+ const execute = async (
28
+ /** @type {import('./config').ConfigOpts} */ config,
29
+ ) => {
30
+ vstorage ||= makeVStorage(
31
+ { fetch },
32
+ { chainName: 'agoric', rpcAddrs: [config.agoricRpc] },
33
+ );
34
+ const agoricAddr = await queryFastUSDCLocalChainAccount(vstorage, out);
35
+ const appendedAddr = `${agoricAddr}?EUD=${destination}`;
36
+ out.log(`forwarding destination ${appendedAddr}`);
37
+
38
+ const { exists, address } = await queryForwardingAccount(
39
+ config.nobleApi,
40
+ config.nobleToAgoricChannel,
41
+ appendedAddr,
42
+ out,
43
+ fetch,
44
+ );
45
+
46
+ if (!exists) {
47
+ nobleSigner ||= await makeSigner(config.nobleSeed, config.nobleRpc, out);
48
+ const { address: signerAddress, signer } = nobleSigner;
49
+ try {
50
+ const res = await registerFwdAccount(
51
+ signer,
52
+ signerAddress,
53
+ config.nobleToAgoricChannel,
54
+ appendedAddr,
55
+ out,
56
+ );
57
+ out.log(res);
58
+ } catch (e) {
59
+ out.error(
60
+ `Error registering noble forwarding account for ${appendedAddr} on channel ${config.nobleToAgoricChannel}`,
61
+ );
62
+ throw e;
63
+ }
64
+ }
65
+
66
+ ethProvider ||= makeProvider(config.ethRpc);
67
+ await depositForBurn(
68
+ ethProvider,
69
+ config.ethSeed,
70
+ config.tokenMessengerAddress,
71
+ config.tokenAddress,
72
+ address,
73
+ amount,
74
+ out,
75
+ );
76
+ };
77
+
78
+ let config;
79
+ await null;
80
+ try {
81
+ config = JSON.parse(await configFile.read());
82
+ } catch {
83
+ out.error(
84
+ `No config found at ${configFile.path}. Use "config init" to create one, or "--home" to specify config location.`,
85
+ );
86
+ throw new Error();
87
+ }
88
+ await execute(config);
89
+ };
90
+
91
+ export default { transfer };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Status values for FastUSDC.
3
+ *
4
+ * @enum {(typeof TxStatus)[keyof typeof TxStatus]}
5
+ */
6
+ export const TxStatus = /** @type {const} */ ({
7
+ /** tx was observed but not advanced */
8
+ Observed: 'OBSERVED',
9
+ /** IBC transfer is initiated */
10
+ Advanced: 'ADVANCED',
11
+ /** settlement for matching advance received and funds dispersed */
12
+ Settled: 'SETTLED',
13
+ });
14
+ harden(TxStatus);
15
+
16
+ /**
17
+ * Status values for the StatusManager.
18
+ *
19
+ * @enum {(typeof PendingTxStatus)[keyof typeof PendingTxStatus]}
20
+ */
21
+ export const PendingTxStatus = /** @type {const} */ ({
22
+ /** tx was observed but not advanced */
23
+ Observed: 'OBSERVED',
24
+ /** IBC transfer is initiated */
25
+ Advanced: 'ADVANCED',
26
+ });
27
+ harden(PendingTxStatus);
@@ -0,0 +1,26 @@
1
+ ## **StatusManager** state diagram, showing different transitions
2
+
3
+
4
+ ### Contract state diagram
5
+
6
+ *Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.*
7
+
8
+ ```mermaid
9
+ stateDiagram-v2
10
+ [*] --> Advanced: Advancer .advance()
11
+ Advanced --> Settled: Settler .settle() after fees
12
+ [*] --> Observed: Advancer .observed()
13
+ Observed --> Settled: Settler .settle() sans fees
14
+ Settled --> [*]
15
+ ```
16
+
17
+ ### Complete state diagram (starting from OCW)
18
+
19
+ ```mermaid
20
+ stateDiagram-v2
21
+ Observed --> Qualified
22
+ Observed --> Unqualified
23
+ Qualified --> Advanced
24
+ Advanced --> Settled
25
+ Qualified --> Settled
26
+ ```
@@ -0,0 +1,240 @@
1
+ import { AmountMath, AmountShape, PaymentShape } from '@agoric/ertp';
2
+ import { assertAllDefined } from '@agoric/internal';
3
+ import { ChainAddressShape } from '@agoric/orchestration';
4
+ import { pickFacet } from '@agoric/vat-data';
5
+ import { VowShape } from '@agoric/vow';
6
+ import { q } from '@endo/errors';
7
+ import { E } from '@endo/far';
8
+ import { M } from '@endo/patterns';
9
+ import { CctpTxEvidenceShape, EudParamShape } from '../type-guards.js';
10
+ import { addressTools } from '../utils/address.js';
11
+ import { makeFeeTools } from '../utils/fees.js';
12
+
13
+ const { isGTE } = AmountMath;
14
+
15
+ /**
16
+ * @import {HostInterface} from '@agoric/async-flow';
17
+ * @import {NatAmount} from '@agoric/ertp';
18
+ * @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
19
+ * @import {VowTools} from '@agoric/vow';
20
+ * @import {Zone} from '@agoric/zone';
21
+ * @import {CctpTxEvidence, FeeConfig, LogFn} from '../types.js';
22
+ * @import {StatusManager} from './status-manager.js';
23
+ */
24
+
25
+ /**
26
+ * Expected interface from LiquidityPool
27
+ *
28
+ * @typedef {{
29
+ * lookupBalance(): NatAmount;
30
+ * borrow(amount: Amount<"nat">): Promise<Payment<"nat">>;
31
+ * repay(payments: PaymentKeywordRecord): Promise<void>
32
+ * }} AssetManagerFacet
33
+ */
34
+
35
+ /**
36
+ * @typedef {{
37
+ * chainHub: ChainHub;
38
+ * feeConfig: FeeConfig;
39
+ * log: LogFn;
40
+ * statusManager: StatusManager;
41
+ * usdc: { brand: Brand<'nat'>; denom: Denom; };
42
+ * vowTools: VowTools;
43
+ * }} AdvancerKitPowers
44
+ */
45
+
46
+ /** type guards internal to the AdvancerKit */
47
+ const AdvancerKitI = harden({
48
+ advancer: M.interface('AdvancerI', {
49
+ handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(),
50
+ }),
51
+ depositHandler: M.interface('DepositHandlerI', {
52
+ onFulfilled: M.call(AmountShape, {
53
+ destination: ChainAddressShape,
54
+ payment: PaymentShape,
55
+ }).returns(VowShape),
56
+ onRejected: M.call(M.error(), {
57
+ destination: ChainAddressShape,
58
+ payment: PaymentShape,
59
+ }).returns(),
60
+ }),
61
+ transferHandler: M.interface('TransferHandlerI', {
62
+ // TODO confirm undefined, and not bigint (sequence)
63
+ onFulfilled: M.call(M.undefined(), {
64
+ amount: AmountShape,
65
+ destination: ChainAddressShape,
66
+ }).returns(M.undefined()),
67
+ onRejected: M.call(M.error(), {
68
+ amount: AmountShape,
69
+ destination: ChainAddressShape,
70
+ }).returns(M.undefined()),
71
+ }),
72
+ });
73
+
74
+ /**
75
+ * @param {Zone} zone
76
+ * @param {AdvancerKitPowers} caps
77
+ */
78
+ export const prepareAdvancerKit = (
79
+ zone,
80
+ { chainHub, feeConfig, log, statusManager, usdc, vowTools: { watch, when } },
81
+ ) => {
82
+ assertAllDefined({
83
+ chainHub,
84
+ feeConfig,
85
+ statusManager,
86
+ watch,
87
+ when,
88
+ });
89
+ const feeTools = makeFeeTools(feeConfig);
90
+ /** @param {bigint} value */
91
+ const toAmount = value => AmountMath.make(usdc.brand, value);
92
+
93
+ return zone.exoClassKit(
94
+ 'Fast USDC Advancer',
95
+ AdvancerKitI,
96
+ /**
97
+ * @param {{
98
+ * assetManagerFacet: AssetManagerFacet;
99
+ * poolAccount: ERef<HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>>;
100
+ * }} config
101
+ */
102
+ config => harden(config),
103
+ {
104
+ advancer: {
105
+ /**
106
+ * Must perform a status update for every observed transaction.
107
+ *
108
+ * We do not expect any callers to depend on the settlement of
109
+ * `handleTransactionEvent` - errors caught are communicated to the
110
+ * `StatusManager` - so we don't need to concern ourselves with
111
+ * preserving the vow chain for callers.
112
+ *
113
+ * @param {CctpTxEvidence} evidence
114
+ */
115
+ async handleTransactionEvent(evidence) {
116
+ await null;
117
+ try {
118
+ // TODO poolAccount might be a vow we need to unwrap
119
+ const { assetManagerFacet, poolAccount } = this.state;
120
+ const { recipientAddress } = evidence.aux;
121
+ const { EUD } = addressTools.getQueryParams(
122
+ recipientAddress,
123
+ EudParamShape,
124
+ );
125
+
126
+ // this will throw if the bech32 prefix is not found, but is handled by the catch
127
+ const destination = chainHub.makeChainAddress(EUD);
128
+ const requestedAmount = toAmount(evidence.tx.amount);
129
+ const advanceAmount = feeTools.calculateAdvance(requestedAmount);
130
+
131
+ // TODO: consider skipping and using `borrow()`s internal balance check
132
+ const poolBalance = assetManagerFacet.lookupBalance();
133
+ if (!isGTE(poolBalance, requestedAmount)) {
134
+ log(
135
+ `Insufficient pool funds`,
136
+ `Requested ${q(advanceAmount)} but only have ${q(poolBalance)}`,
137
+ );
138
+ // report `requestedAmount`, not `advancedAmount`... do we need to
139
+ // communicate net to `StatusManger` in case fees change in between?
140
+ statusManager.observe(evidence);
141
+ return;
142
+ }
143
+
144
+ try {
145
+ // Mark as Advanced since `transferV` initiates the advance.
146
+ // Will throw if we've already .skipped or .advanced this evidence.
147
+ statusManager.advance(evidence);
148
+ } catch (e) {
149
+ // Only anticipated error is `assertNotSeen`, so intercept the
150
+ // catch so we don't call .skip which also performs this check
151
+ log('Advancer error:', q(e).toString());
152
+ return;
153
+ }
154
+
155
+ try {
156
+ const payment = await assetManagerFacet.borrow(advanceAmount);
157
+ const depositV = E(poolAccount).deposit(payment);
158
+ void watch(depositV, this.facets.depositHandler, {
159
+ destination,
160
+ payment,
161
+ });
162
+ } catch (e) {
163
+ // `.borrow()` might fail if the balance changes since we
164
+ // requested it. TODO - how to handle this? change ADVANCED -> OBSERVED?
165
+ // Note: `depositHandler` handles the `.deposit()` failure
166
+ log('🚨 advance borrow failed', q(e).toString());
167
+ }
168
+ } catch (e) {
169
+ log('Advancer error:', q(e).toString());
170
+ statusManager.observe(evidence);
171
+ }
172
+ },
173
+ },
174
+ depositHandler: {
175
+ /**
176
+ * @param {NatAmount} amount amount returned from deposit
177
+ * @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
178
+ */
179
+ onFulfilled(amount, { destination }) {
180
+ const { poolAccount } = this.state;
181
+ const transferV = E(poolAccount).transfer(
182
+ destination,
183
+ /** @type {DenomAmount} */ ({
184
+ denom: usdc.denom,
185
+ value: amount.value,
186
+ }),
187
+ );
188
+ return watch(transferV, this.facets.transferHandler, {
189
+ destination,
190
+ amount,
191
+ });
192
+ },
193
+ /**
194
+ * @param {Error} error
195
+ * @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
196
+ */
197
+ onRejected(error, { payment }) {
198
+ // TODO return live payment from ctx to LP
199
+ log('🚨 advance deposit failed', q(error).toString());
200
+ log('TODO live payment to return to LP', q(payment).toString());
201
+ },
202
+ },
203
+ transferHandler: {
204
+ /**
205
+ * @param {undefined} result TODO confirm this is not a bigint (sequence)
206
+ * @param {{ destination: ChainAddress; amount: NatAmount; }} ctx
207
+ */
208
+ onFulfilled(result, { destination, amount }) {
209
+ // TODO vstorage update?
210
+ log(
211
+ 'Advance transfer fulfilled',
212
+ q({ amount, destination, result }).toString(),
213
+ );
214
+ },
215
+ onRejected(error) {
216
+ // XXX retry logic?
217
+ // What do we do if we fail, should we keep a Status?
218
+ log('Advance transfer rejected', q(error).toString());
219
+ },
220
+ },
221
+ },
222
+ {
223
+ stateShape: harden({
224
+ assetManagerFacet: M.remotable(),
225
+ poolAccount: M.or(VowShape, M.remotable()),
226
+ }),
227
+ },
228
+ );
229
+ };
230
+ harden(prepareAdvancerKit);
231
+
232
+ /**
233
+ * @param {Zone} zone
234
+ * @param {AdvancerKitPowers} caps
235
+ */
236
+ export const prepareAdvancer = (zone, caps) => {
237
+ const makeAdvancerKit = prepareAdvancerKit(zone, caps);
238
+ return pickFacet(makeAdvancerKit, 'advancer');
239
+ };
240
+ harden(prepareAdvancer);