@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,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 };
|
package/src/constants.js
ADDED
|
@@ -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);
|