@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,176 @@
1
+ import { M } from '@endo/patterns';
2
+ import { makeError, q } from '@endo/errors';
3
+
4
+ import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
5
+ import { CctpTxEvidenceShape, PendingTxShape } from '../type-guards.js';
6
+ import { PendingTxStatus } from '../constants.js';
7
+
8
+ /**
9
+ * @import {MapStore, SetStore} from '@agoric/store';
10
+ * @import {Zone} from '@agoric/zone';
11
+ * @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx} from '../types.js';
12
+ */
13
+
14
+ /**
15
+ * Create the key for the pendingTxs MapStore.
16
+ *
17
+ * The key is a composite of `txHash` and `chainId` and not meant to be
18
+ * parsable.
19
+ *
20
+ * @param {NobleAddress} addr
21
+ * @param {bigint} amount
22
+ * @returns {PendingTxKey}
23
+ */
24
+ const makePendingTxKey = (addr, amount) =>
25
+ `pendingTx:${JSON.stringify([addr, String(amount)])}`;
26
+
27
+ /**
28
+ * Get the key for the pendingTxs MapStore.
29
+ *
30
+ * @param {CctpTxEvidence} evidence
31
+ * @returns {PendingTxKey}
32
+ */
33
+ const pendingTxKeyOf = evidence => {
34
+ const { amount, forwardingAddress } = evidence.tx;
35
+ return makePendingTxKey(forwardingAddress, amount);
36
+ };
37
+
38
+ /**
39
+ * Get the key for the seenTxs SetStore.
40
+ *
41
+ * The key is a composite of `NobleAddress` and transaction `amount` and not
42
+ * meant to be parsable.
43
+ *
44
+ * @param {CctpTxEvidence} evidence
45
+ * @returns {SeenTxKey}
46
+ */
47
+ const seenTxKeyOf = evidence => {
48
+ const { txHash, chainId } = evidence;
49
+ return `seenTx:${JSON.stringify([txHash, chainId])}`;
50
+ };
51
+
52
+ /**
53
+ * The `StatusManager` keeps track of Pending and Seen Transactions
54
+ * via {@link PendingTxStatus} states, aiding in coordination between the `Advancer`
55
+ * and `Settler`.
56
+ *
57
+ * XXX consider separate facets for `Advancing` and `Settling` capabilities.
58
+ *
59
+ * @param {Zone} zone
60
+ */
61
+ export const prepareStatusManager = zone => {
62
+ /** @type {MapStore<PendingTxKey, PendingTx[]>} */
63
+ const pendingTxs = zone.mapStore('PendingTxs', {
64
+ keyShape: M.string(),
65
+ valueShape: M.arrayOf(PendingTxShape),
66
+ });
67
+
68
+ /** @type {SetStore<SeenTxKey>} */
69
+ const seenTxs = zone.setStore('SeenTxs', {
70
+ keyShape: M.string(),
71
+ });
72
+
73
+ /**
74
+ * Ensures that `txHash+chainId` has not been processed
75
+ * and adds entry to `seenTxs` set.
76
+ *
77
+ * Also records the CctpTxEvidence and status in `pendingTxs`.
78
+ *
79
+ * @param {CctpTxEvidence} evidence
80
+ * @param {PendingTxStatus} status
81
+ */
82
+ const recordPendingTx = (evidence, status) => {
83
+ const seenKey = seenTxKeyOf(evidence);
84
+ if (seenTxs.has(seenKey)) {
85
+ throw makeError(`Transaction already seen: ${q(seenKey)}`);
86
+ }
87
+ seenTxs.add(seenKey);
88
+
89
+ appendToStoredArray(
90
+ pendingTxs,
91
+ pendingTxKeyOf(evidence),
92
+ harden({ ...evidence, status }),
93
+ );
94
+ };
95
+
96
+ return zone.exo(
97
+ 'Fast USDC Status Manager',
98
+ M.interface('StatusManagerI', {
99
+ advance: M.call(CctpTxEvidenceShape).returns(M.undefined()),
100
+ observe: M.call(CctpTxEvidenceShape).returns(M.undefined()),
101
+ hasPendingSettlement: M.call(M.string(), M.bigint()).returns(M.boolean()),
102
+ settle: M.call(M.string(), M.bigint()).returns(M.undefined()),
103
+ lookupPending: M.call(M.string(), M.bigint()).returns(
104
+ M.arrayOf(PendingTxShape),
105
+ ),
106
+ }),
107
+ {
108
+ /**
109
+ * Add a new transaction with ADVANCED status
110
+ * @param {CctpTxEvidence} evidence
111
+ */
112
+ advance(evidence) {
113
+ recordPendingTx(evidence, PendingTxStatus.Advanced);
114
+ },
115
+
116
+ /**
117
+ * Add a new transaction with OBSERVED status
118
+ * @param {CctpTxEvidence} evidence
119
+ */
120
+ observe(evidence) {
121
+ recordPendingTx(evidence, PendingTxStatus.Observed);
122
+ },
123
+
124
+ /**
125
+ * Find an `ADVANCED` or `OBSERVED` tx waiting to be `SETTLED`
126
+ *
127
+ * @param {NobleAddress} address
128
+ * @param {bigint} amount
129
+ * @returns {boolean}
130
+ */
131
+ hasPendingSettlement(address, amount) {
132
+ const key = makePendingTxKey(address, amount);
133
+ const pending = pendingTxs.get(key);
134
+ return !!pending.length;
135
+ },
136
+
137
+ /**
138
+ * Mark an `ADVANCED` or `OBSERVED` transaction as `SETTLED` and remove it
139
+ *
140
+ * @param {NobleAddress} address
141
+ * @param {bigint} amount
142
+ */
143
+ settle(address, amount) {
144
+ const key = makePendingTxKey(address, amount);
145
+ const pending = pendingTxs.get(key);
146
+
147
+ if (!pending.length) {
148
+ throw makeError(`No unsettled entry for ${q(key)}`);
149
+ }
150
+
151
+ const pendingCopy = [...pending];
152
+ pendingCopy.shift();
153
+ // TODO, vstorage update for `TxStatus.Settled`
154
+ pendingTxs.set(key, harden(pendingCopy));
155
+ },
156
+
157
+ /**
158
+ * Lookup all pending entries for a given address and amount
159
+ *
160
+ * @param {NobleAddress} address
161
+ * @param {bigint} amount
162
+ * @returns {PendingTx[]}
163
+ */
164
+ lookupPending(address, amount) {
165
+ const key = makePendingTxKey(address, amount);
166
+ if (!pendingTxs.has(key)) {
167
+ throw makeError(`Key ${q(key)} not yet observed`);
168
+ }
169
+ return pendingTxs.get(key);
170
+ },
171
+ },
172
+ );
173
+ };
174
+ harden(prepareStatusManager);
175
+
176
+ /** @typedef {ReturnType<typeof prepareStatusManager>} StatusManager */
@@ -0,0 +1,180 @@
1
+ import { makeTracer } from '@agoric/internal';
2
+ import { prepareDurablePublishKit } from '@agoric/notifier';
3
+ import { M } from '@endo/patterns';
4
+ import { CctpTxEvidenceShape } from '../type-guards.js';
5
+ import { defineInertInvitation } from '../utils/zoe.js';
6
+ import { prepareOperatorKit } from './operator-kit.js';
7
+
8
+ /**
9
+ * @import {Zone} from '@agoric/zone';
10
+ * @import {OperatorKit} from './operator-kit.js';
11
+ * @import {CctpTxEvidence} from '../types.js';
12
+ */
13
+
14
+ const trace = makeTracer('TxFeed', true);
15
+
16
+ /** Name in the invitation purse (keyed also by this contract instance) */
17
+ export const INVITATION_MAKERS_DESC = 'oracle operator invitation';
18
+
19
+ const TransactionFeedKitI = harden({
20
+ operatorPowers: M.interface('Transaction Feed Admin', {
21
+ submitEvidence: M.call(CctpTxEvidenceShape, M.any()).returns(),
22
+ }),
23
+ creator: M.interface('Transaction Feed Creator', {
24
+ // TODO narrow the return shape to OperatorKit
25
+ initOperator: M.call(M.string()).returns(M.record()),
26
+ makeOperatorInvitation: M.call(M.string()).returns(M.promise()),
27
+ removeOperator: M.call(M.string()).returns(),
28
+ }),
29
+ public: M.interface('Transaction Feed Public', {
30
+ getEvidenceSubscriber: M.call().returns(M.remotable()),
31
+ }),
32
+ });
33
+
34
+ /**
35
+ * @param {Zone} zone
36
+ * @param {ZCF} zcf
37
+ */
38
+ export const prepareTransactionFeedKit = (zone, zcf) => {
39
+ const kinds = zone.mapStore('Kinds');
40
+ const makeDurablePublishKit = prepareDurablePublishKit(
41
+ kinds,
42
+ 'Transaction Feed',
43
+ );
44
+ /** @type {PublishKit<CctpTxEvidence>} */
45
+ const { publisher, subscriber } = makeDurablePublishKit();
46
+
47
+ const makeInertInvitation = defineInertInvitation(zcf, 'submitting evidence');
48
+
49
+ const makeOperatorKit = prepareOperatorKit(zone, {
50
+ makeInertInvitation,
51
+ });
52
+
53
+ return zone.exoClassKit(
54
+ 'Fast USDC Feed',
55
+ TransactionFeedKitI,
56
+ () => {
57
+ /** @type {MapStore<string, OperatorKit>} */
58
+ const operators = zone.mapStore('operators', {
59
+ durable: true,
60
+ });
61
+ /** @type {MapStore<string, MapStore<string, CctpTxEvidence>>} */
62
+ const pending = zone.mapStore('pending', {
63
+ durable: true,
64
+ });
65
+ return { operators, pending };
66
+ },
67
+ {
68
+ creator: {
69
+ /**
70
+ * An "operator invitation" is an invitation to be an operator in the
71
+ * oracle netowrk, with the able to submit data to submit evidence of
72
+ * CCTP transactions.
73
+ *
74
+ * @param {string} operatorId unique per contract instance
75
+ * @returns {Promise<Invitation<OperatorKit>>}
76
+ */
77
+ makeOperatorInvitation(operatorId) {
78
+ const { creator } = this.facets;
79
+ trace('makeOperatorInvitation', operatorId);
80
+
81
+ return zcf.makeInvitation(
82
+ /** @type {OfferHandler<OperatorKit>} */
83
+ seat => {
84
+ seat.exit();
85
+ return creator.initOperator(operatorId);
86
+ },
87
+ INVITATION_MAKERS_DESC,
88
+ );
89
+ },
90
+ /** @param {string} operatorId */
91
+ initOperator(operatorId) {
92
+ const { operators, pending } = this.state;
93
+ trace('initOperator', operatorId);
94
+
95
+ const operatorKit = makeOperatorKit(
96
+ operatorId,
97
+ this.facets.operatorPowers,
98
+ );
99
+ operators.init(operatorId, operatorKit);
100
+ pending.init(
101
+ operatorId,
102
+ zone.detached().mapStore('pending evidence'),
103
+ );
104
+
105
+ return operatorKit;
106
+ },
107
+
108
+ /** @param {string} operatorId */
109
+ async removeOperator(operatorId) {
110
+ const { operators } = this.state;
111
+ trace('removeOperator', operatorId);
112
+ const operatorKit = operators.get(operatorId);
113
+ operatorKit.admin.disable();
114
+ operators.delete(operatorId);
115
+ },
116
+ },
117
+ operatorPowers: {
118
+ /**
119
+ * Add evidence from an operator.
120
+ *
121
+ * @param {CctpTxEvidence} evidence
122
+ * @param {OperatorKit} operatorKit
123
+ */
124
+ submitEvidence(evidence, operatorKit) {
125
+ const { pending } = this.state;
126
+ trace(
127
+ 'submitEvidence',
128
+ operatorKit.operator.getStatus().operatorId,
129
+ evidence,
130
+ );
131
+ const { operatorId } = operatorKit.operator.getStatus();
132
+
133
+ // TODO should this verify that the operator is one made by this exo?
134
+ // This doesn't work...
135
+ // operatorKit === operators.get(operatorId) ||
136
+ // Fail`operatorKit mismatch`;
137
+
138
+ // TODO validate that it's a valid for Fast USDC before accepting
139
+ // E.g. that the `recipientAddress` is the FU settlement account and that
140
+ // the EUD is a chain supported by FU.
141
+ const { txHash } = evidence;
142
+
143
+ // accept the evidence
144
+ {
145
+ const pendingStore = pending.get(operatorId);
146
+ if (pendingStore.has(txHash)) {
147
+ trace(`operator ${operatorId} already reported ${txHash}`);
148
+ } else {
149
+ pendingStore.init(txHash, evidence);
150
+ }
151
+ }
152
+
153
+ // check agreement
154
+ const found = [...pending.values()].filter(store =>
155
+ store.has(txHash),
156
+ );
157
+ // TODO determine the real policy for checking agreement
158
+ if (found.length < pending.getSize()) {
159
+ // not all have seen it
160
+ return;
161
+ }
162
+
163
+ // TODO verify that all found deep equal
164
+
165
+ // all agree, so remove from pending and publish
166
+ for (const pendingStore of pending.values()) {
167
+ pendingStore.delete(txHash);
168
+ }
169
+ publisher.publish(evidence);
170
+ },
171
+ },
172
+ public: {
173
+ getEvidenceSubscriber: () => subscriber,
174
+ },
175
+ },
176
+ );
177
+ };
178
+ harden(prepareTransactionFeedKit);
179
+
180
+ /** @typedef {ReturnType<ReturnType<typeof prepareTransactionFeedKit>>} TransactionFeedKit */
@@ -0,0 +1,216 @@
1
+ import { AssetKind } from '@agoric/ertp';
2
+ import {
3
+ assertAllDefined,
4
+ deeplyFulfilledObject,
5
+ makeTracer,
6
+ } from '@agoric/internal';
7
+ import { observeIteration, subscribeEach } from '@agoric/notifier';
8
+ import {
9
+ OrchestrationPowersShape,
10
+ withOrchestration,
11
+ } from '@agoric/orchestration';
12
+ import { provideSingleton } from '@agoric/zoe/src/contractSupport/durability.js';
13
+ import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
14
+ import { E } from '@endo/far';
15
+ import { M, objectMap } from '@endo/patterns';
16
+ import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
17
+ import { prepareAdvancer } from './exos/advancer.js';
18
+ import { prepareLiquidityPoolKit } from './exos/liquidity-pool.js';
19
+ import { prepareSettler } from './exos/settler.js';
20
+ import { prepareStatusManager } from './exos/status-manager.js';
21
+ import { prepareTransactionFeedKit } from './exos/transaction-feed.js';
22
+ import { defineInertInvitation } from './utils/zoe.js';
23
+ import { FastUSDCTermsShape, FeeConfigShape } from './type-guards.js';
24
+
25
+ const trace = makeTracer('FastUsdc');
26
+
27
+ /**
28
+ * @import {Denom} from '@agoric/orchestration';
29
+ * @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js';
30
+ * @import {Zone} from '@agoric/zone';
31
+ * @import {OperatorKit} from './exos/operator-kit.js';
32
+ * @import {CctpTxEvidence, FeeConfig} from './types.js';
33
+ * @import {RepayAmountKWR, RepayPaymentKWR} from './exos/liquidity-pool.js';
34
+ */
35
+
36
+ /**
37
+ * @typedef {{
38
+ * usdcDenom: Denom;
39
+ * }} FastUsdcTerms
40
+ */
41
+
42
+ /** @type {ContractMeta<typeof start>} */
43
+ export const meta = {
44
+ // @ts-expect-error TypedPattern not recognized as record
45
+ customTermsShape: FastUSDCTermsShape,
46
+ privateArgsShape: {
47
+ // @ts-expect-error TypedPattern not recognized as record
48
+ ...OrchestrationPowersShape,
49
+ feeConfig: FeeConfigShape,
50
+ marshaller: M.remotable(),
51
+ },
52
+ };
53
+ harden(meta);
54
+
55
+ /**
56
+ * @param {ZCF<FastUsdcTerms>} zcf
57
+ * @param {OrchestrationPowers & {
58
+ * marshaller: Marshaller;
59
+ * feeConfig: FeeConfig;
60
+ * }} privateArgs
61
+ * @param {Zone} zone
62
+ * @param {OrchestrationTools} tools
63
+ */
64
+ export const contract = async (zcf, privateArgs, zone, tools) => {
65
+ assert(tools, 'no tools');
66
+ const terms = zcf.getTerms();
67
+ assert('USDC' in terms.brands, 'no USDC brand');
68
+ assert('usdcDenom' in terms, 'no usdcDenom');
69
+ const { feeConfig, marshaller } = privateArgs;
70
+ const { makeRecorderKit } = prepareRecorderKitMakers(
71
+ zone.mapStore('vstorage'),
72
+ marshaller,
73
+ );
74
+ const statusManager = prepareStatusManager(zone);
75
+ const makeSettler = prepareSettler(zone, { statusManager });
76
+ const { chainHub, vowTools } = tools;
77
+ const makeAdvancer = prepareAdvancer(zone, {
78
+ chainHub,
79
+ feeConfig,
80
+ log: trace,
81
+ usdc: harden({
82
+ brand: terms.brands.USDC,
83
+ denom: terms.usdcDenom,
84
+ }),
85
+ statusManager,
86
+ vowTools,
87
+ });
88
+ const makeFeedKit = prepareTransactionFeedKit(zone, zcf);
89
+ assertAllDefined({ makeFeedKit, makeAdvancer, makeSettler, statusManager });
90
+ const feedKit = makeFeedKit();
91
+ const advancer = makeAdvancer(
92
+ // @ts-expect-error FIXME
93
+ {},
94
+ );
95
+ // Connect evidence stream to advancer
96
+ void observeIteration(subscribeEach(feedKit.public.getEvidenceSubscriber()), {
97
+ updateState(evidence) {
98
+ try {
99
+ void advancer.handleTransactionEvent(evidence);
100
+ } catch (err) {
101
+ trace('🚨 Error handling transaction event', err);
102
+ }
103
+ },
104
+ });
105
+ const makeLiquidityPoolKit = prepareLiquidityPoolKit(
106
+ zone,
107
+ zcf,
108
+ terms.brands.USDC,
109
+ { makeRecorderKit },
110
+ );
111
+
112
+ const makeTestInvitation = defineInertInvitation(
113
+ zcf,
114
+ 'test of forcing evidence',
115
+ );
116
+
117
+ const creatorFacet = zone.exo('Fast USDC Creator', undefined, {
118
+ /** @type {(operatorId: string) => Promise<Invitation<OperatorKit>>} */
119
+ async makeOperatorInvitation(operatorId) {
120
+ return feedKit.creator.makeOperatorInvitation(operatorId);
121
+ },
122
+ /**
123
+ * @param {{ USDC: Amount<'nat'>}} amounts
124
+ */
125
+ testBorrow(amounts) {
126
+ console.log('🚧🚧 UNTIL: borrow is integrated 🚧🚧', amounts);
127
+ const { zcfSeat: tmpAssetManagerSeat } = zcf.makeEmptySeatKit();
128
+ // eslint-disable-next-line no-use-before-define
129
+ poolKit.borrower.borrow(tmpAssetManagerSeat, amounts);
130
+ return tmpAssetManagerSeat.getCurrentAllocation();
131
+ },
132
+ /**
133
+ *
134
+ * @param {RepayAmountKWR} amounts
135
+ * @param {RepayPaymentKWR} payments
136
+ * @returns {Promise<AmountKeywordRecord>}
137
+ */
138
+ async testRepay(amounts, payments) {
139
+ console.log('🚧🚧 UNTIL: repay is integrated 🚧🚧', amounts);
140
+ const { zcfSeat: tmpAssetManagerSeat } = zcf.makeEmptySeatKit();
141
+ await depositToSeat(
142
+ zcf,
143
+ tmpAssetManagerSeat,
144
+ await deeplyFulfilledObject(
145
+ objectMap(payments, pmt => E(terms.issuers.USDC).getAmountOf(pmt)),
146
+ ),
147
+ payments,
148
+ );
149
+ // eslint-disable-next-line no-use-before-define
150
+ poolKit.repayer.repay(tmpAssetManagerSeat, amounts);
151
+ return tmpAssetManagerSeat.getCurrentAllocation();
152
+ },
153
+ });
154
+
155
+ const publicFacet = zone.exo('Fast USDC Public', undefined, {
156
+ // XXX to be removed before production
157
+ /**
158
+ * NB: Any caller with access to this invitation maker has the ability to
159
+ * force handling of evidence.
160
+ *
161
+ * Provide an API call in the form of an invitation maker, so that the
162
+ * capability is available in the smart-wallet bridge during UI testing.
163
+ *
164
+ * @param {CctpTxEvidence} evidence
165
+ */
166
+ makeTestPushInvitation(evidence) {
167
+ void advancer.handleTransactionEvent(evidence);
168
+ return makeTestInvitation();
169
+ },
170
+ makeDepositInvitation() {
171
+ // eslint-disable-next-line no-use-before-define
172
+ return poolKit.public.makeDepositInvitation();
173
+ },
174
+ makeWithdrawInvitation() {
175
+ // eslint-disable-next-line no-use-before-define
176
+ return poolKit.public.makeWithdrawInvitation();
177
+ },
178
+ getPublicTopics() {
179
+ // eslint-disable-next-line no-use-before-define
180
+ return poolKit.public.getPublicTopics();
181
+ },
182
+ });
183
+
184
+ // ^^^ Define all kinds above this line. Keep remote calls below. vvv
185
+
186
+ // NOTE: Using a ZCFMint is helpful for the usual reasons (
187
+ // synchronous mint/burn, keeping assets out of contract vats, ...).
188
+ // And there's just one pool, which suggests building it with zone.exo().
189
+ //
190
+ // But zone.exo() defines a kind and
191
+ // all kinds have to be defined before any remote calls,
192
+ // such as the one to the zoe vat as part of making a ZCFMint.
193
+ //
194
+ // So we use zone.exoClassKit above to define the liquidity pool kind
195
+ // and pass the shareMint into the maker / init function.
196
+
197
+ const shareMint = await provideSingleton(
198
+ zone.mapStore('mint'),
199
+ 'PoolShare',
200
+ () =>
201
+ zcf.makeZCFMint('PoolShares', AssetKind.NAT, {
202
+ decimalPlaces: 6,
203
+ }),
204
+ );
205
+
206
+ const poolKit = zone.makeOnce('Liquidity Pool kit', () =>
207
+ makeLiquidityPoolKit(shareMint, privateArgs.storageNode),
208
+ );
209
+
210
+ return harden({ creatorFacet, publicFacet });
211
+ };
212
+ harden(contract);
213
+
214
+ export const start = withOrchestration(contract);
215
+ harden(start);
216
+ /** @typedef {typeof start} FastUsdcSF */