@agoric/fast-usdc 0.1.1-dev-8fd731c.0 → 0.1.1-dev-aa11d5c.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 CHANGED
@@ -56,3 +56,44 @@ sequenceDiagram
56
56
 
57
57
  A->>TF: notify(evidence)
58
58
  ```
59
+
60
+ # Status Manager
61
+
62
+ ### Pending Advance State Diagram
63
+
64
+ *Transactions are qualified by the OCW and EventFeed before arriving to the Advancer.*
65
+
66
+ ```mermaid
67
+ stateDiagram-v2
68
+ [*] --> Observed: observe()
69
+ [*] --> Advancing: advancing()
70
+
71
+ Advancing --> Advanced: advanceOutcome(...true)
72
+ Advancing --> AdvanceFailed: advanceOutcome(...false)
73
+
74
+ Observed --> [*]: dequeueStatus()
75
+ Advanced --> [*]: dequeueStatus()
76
+ AdvanceFailed --> [*]: dequeueStatus()
77
+
78
+ note right of [*]
79
+ After dequeueStatus():
80
+ Transaction is removed
81
+ from pendingTxs store.
82
+ Settler will .disburse()
83
+ or .forward()
84
+ end note
85
+ ```
86
+
87
+ ### Complete state diagram (starting from Transaction Feed into Advancer)
88
+
89
+ ```mermaid
90
+ stateDiagram-v2
91
+ Observed --> Advancing
92
+ Observed --> Forwarding:Minted
93
+ Forwarding --> Forwarded
94
+ Advancing --> Advanced
95
+ Advanced --> Disbursed
96
+ AdvanceFailed --> Forwarding
97
+ Advancing --> AdvanceFailed
98
+ Forwarding --> ForwardFailed
99
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/fast-usdc",
3
- "version": "0.1.1-dev-8fd731c.0+8fd731c",
3
+ "version": "0.1.1-dev-aa11d5c.0+aa11d5c",
4
4
  "description": "CLI and library for Fast USDC product",
5
5
  "type": "module",
6
6
  "files": [
@@ -21,9 +21,9 @@
21
21
  "lint:eslint": "eslint ."
22
22
  },
23
23
  "devDependencies": {
24
- "@agoric/swingset-liveslots": "0.10.3-dev-8fd731c.0+8fd731c",
25
- "@agoric/vats": "0.15.2-dev-8fd731c.0+8fd731c",
26
- "@agoric/zone": "0.2.3-dev-8fd731c.0+8fd731c",
24
+ "@agoric/swingset-liveslots": "0.10.3-dev-aa11d5c.0+aa11d5c",
25
+ "@agoric/vats": "0.15.2-dev-aa11d5c.0+aa11d5c",
26
+ "@agoric/zone": "0.2.3-dev-aa11d5c.0+aa11d5c",
27
27
  "@fast-check/ava": "^2.0.1",
28
28
  "ava": "^5.3.0",
29
29
  "c8": "^10.1.2",
@@ -31,15 +31,15 @@
31
31
  "ts-blank-space": "^0.4.1"
32
32
  },
33
33
  "dependencies": {
34
- "@agoric/client-utils": "0.1.1-dev-8fd731c.0+8fd731c",
35
- "@agoric/ertp": "0.16.3-dev-8fd731c.0+8fd731c",
36
- "@agoric/internal": "0.3.3-dev-8fd731c.0+8fd731c",
37
- "@agoric/notifier": "0.6.3-dev-8fd731c.0+8fd731c",
38
- "@agoric/orchestration": "0.1.1-dev-8fd731c.0+8fd731c",
39
- "@agoric/store": "0.9.3-dev-8fd731c.0+8fd731c",
40
- "@agoric/vat-data": "0.5.3-dev-8fd731c.0+8fd731c",
41
- "@agoric/vow": "0.1.1-dev-8fd731c.0+8fd731c",
42
- "@agoric/zoe": "0.26.3-dev-8fd731c.0+8fd731c",
34
+ "@agoric/client-utils": "0.1.1-dev-aa11d5c.0+aa11d5c",
35
+ "@agoric/ertp": "0.16.3-dev-aa11d5c.0+aa11d5c",
36
+ "@agoric/internal": "0.3.3-dev-aa11d5c.0+aa11d5c",
37
+ "@agoric/notifier": "0.6.3-dev-aa11d5c.0+aa11d5c",
38
+ "@agoric/orchestration": "0.1.1-dev-aa11d5c.0+aa11d5c",
39
+ "@agoric/store": "0.9.3-dev-aa11d5c.0+aa11d5c",
40
+ "@agoric/vat-data": "0.5.3-dev-aa11d5c.0+aa11d5c",
41
+ "@agoric/vow": "0.1.1-dev-aa11d5c.0+aa11d5c",
42
+ "@agoric/zoe": "0.26.3-dev-aa11d5c.0+aa11d5c",
43
43
  "@cosmjs/proto-signing": "^0.32.4",
44
44
  "@cosmjs/stargate": "^0.32.4",
45
45
  "@endo/base64": "^1.0.9",
@@ -78,5 +78,5 @@
78
78
  "publishConfig": {
79
79
  "access": "public"
80
80
  },
81
- "gitHead": "8fd731ce457512f3ad1b7d8e936b935e0f8d3b9b"
81
+ "gitHead": "aa11d5c2482ac47f1a6ab8ac5e67a9678e2946f7"
82
82
  }
package/src/constants.js CHANGED
@@ -7,12 +7,22 @@ export const TxStatus = /** @type {const} */ ({
7
7
  /** tx was observed but not advanced */
8
8
  Observed: 'OBSERVED',
9
9
  /** IBC transfer is initiated */
10
+ Advancing: 'ADVANCING',
11
+ /** IBC transfer is complete */
10
12
  Advanced: 'ADVANCED',
11
- /** settlement for matching advance received and funds dispersed */
12
- Settled: 'SETTLED',
13
+ /** IBC transfer failed (timed out) */
14
+ AdvanceFailed: 'ADVANCE_FAILED',
15
+ /** settlement for matching advance received and funds disbursed */
16
+ Disbursed: 'DISBURSED',
17
+ /** fallback: do not collect fees */
18
+ Forwarded: 'FORWARDED',
19
+ /** failed to forward to EUD */
20
+ ForwardFailed: 'FORWARD_FAILED',
13
21
  });
14
22
  harden(TxStatus);
15
23
 
24
+ // TODO: define valid state transitions
25
+
16
26
  /**
17
27
  * Status values for the StatusManager.
18
28
  *
@@ -22,6 +32,10 @@ export const PendingTxStatus = /** @type {const} */ ({
22
32
  /** tx was observed but not advanced */
23
33
  Observed: 'OBSERVED',
24
34
  /** IBC transfer is initiated */
35
+ Advancing: 'ADVANCING',
36
+ /** IBC transfer failed (timed out) */
37
+ AdvanceFailed: 'ADVANCE_FAILED',
38
+ /** IBC transfer is complete */
25
39
  Advanced: 'ADVANCED',
26
40
  });
27
41
  harden(PendingTxStatus);
@@ -6,12 +6,14 @@ import { VowShape } from '@agoric/vow';
6
6
  import { q } from '@endo/errors';
7
7
  import { E } from '@endo/far';
8
8
  import { M } from '@endo/patterns';
9
- import { CctpTxEvidenceShape, EudParamShape } from '../type-guards.js';
9
+ import {
10
+ CctpTxEvidenceShape,
11
+ EudParamShape,
12
+ EvmHashShape,
13
+ } from '../type-guards.js';
10
14
  import { addressTools } from '../utils/address.js';
11
15
  import { makeFeeTools } from '../utils/fees.js';
12
16
 
13
- const { isGTE } = AmountMath;
14
-
15
17
  /**
16
18
  * @import {HostInterface} from '@agoric/async-flow';
17
19
  * @import {NatAmount} from '@agoric/ertp';
@@ -19,7 +21,7 @@ const { isGTE } = AmountMath;
19
21
  * @import {ZoeTools} from '@agoric/orchestration/src/utils/zoe-tools.js';
20
22
  * @import {VowTools} from '@agoric/vow';
21
23
  * @import {Zone} from '@agoric/zone';
22
- * @import {CctpTxEvidence, FeeConfig, LogFn} from '../types.js';
24
+ * @import {CctpTxEvidence, EvmHash, FeeConfig, LogFn, NobleAddress} from '../types.js';
23
25
  * @import {StatusManager} from './status-manager.js';
24
26
  * @import {LiquidityPoolKit} from './liquidity-pool.js';
25
27
  */
@@ -46,12 +48,16 @@ const AdvancerKitI = harden({
46
48
  onFulfilled: M.call(M.undefined(), {
47
49
  amount: AmountShape,
48
50
  destination: ChainAddressShape,
51
+ forwardingAddress: M.string(),
49
52
  tmpSeat: M.remotable(),
53
+ txHash: EvmHashShape,
50
54
  }).returns(VowShape),
51
55
  onRejected: M.call(M.error(), {
52
56
  amount: AmountShape,
53
57
  destination: ChainAddressShape,
58
+ forwardingAddress: M.string(),
54
59
  tmpSeat: M.remotable(),
60
+ txHash: EvmHashShape,
55
61
  }).returns(),
56
62
  }),
57
63
  transferHandler: M.interface('TransferHandlerI', {
@@ -59,14 +65,27 @@ const AdvancerKitI = harden({
59
65
  onFulfilled: M.call(M.undefined(), {
60
66
  amount: AmountShape,
61
67
  destination: ChainAddressShape,
68
+ forwardingAddress: M.string(),
69
+ txHash: EvmHashShape,
62
70
  }).returns(M.undefined()),
63
71
  onRejected: M.call(M.error(), {
64
72
  amount: AmountShape,
65
73
  destination: ChainAddressShape,
74
+ forwardingAddress: M.string(),
75
+ txHash: EvmHashShape,
66
76
  }).returns(M.undefined()),
67
77
  }),
68
78
  });
69
79
 
80
+ /**
81
+ * @typedef {{
82
+ * amount: NatAmount;
83
+ * destination: ChainAddress;
84
+ * forwardingAddress: NobleAddress;
85
+ * txHash: EvmHash;
86
+ * }} AdvancerVowCtx
87
+ */
88
+
70
89
  /**
71
90
  * @param {Zone} zone
72
91
  * @param {AdvancerKitPowers} caps
@@ -100,6 +119,7 @@ export const prepareAdvancerKit = (
100
119
  AdvancerKitI,
101
120
  /**
102
121
  * @param {{
122
+ * notifyFacet: import('./settler.js').SettlerKit['notify'];
103
123
  * borrowerFacet: LiquidityPoolKit['borrower'];
104
124
  * poolAccount: HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>;
105
125
  * }} config
@@ -120,51 +140,32 @@ export const prepareAdvancerKit = (
120
140
  async handleTransactionEvent(evidence) {
121
141
  await null;
122
142
  try {
143
+ if (statusManager.hasBeenObserved(evidence)) {
144
+ log('txHash already seen:', evidence.txHash);
145
+ return;
146
+ }
147
+
123
148
  const { borrowerFacet, poolAccount } = this.state;
124
149
  const { recipientAddress } = evidence.aux;
150
+ // throws if EUD is not found
125
151
  const { EUD } = addressTools.getQueryParams(
126
152
  recipientAddress,
127
153
  EudParamShape,
128
154
  );
129
-
130
- // this will throw if the bech32 prefix is not found, but is handled by the catch
155
+ // throws if the bech32 prefix is not found
131
156
  const destination = chainHub.makeChainAddress(EUD);
157
+
132
158
  const requestedAmount = toAmount(evidence.tx.amount);
159
+ // throws if requested does not exceed fees
133
160
  const advanceAmount = feeTools.calculateAdvance(requestedAmount);
134
161
 
135
- // TODO: consider skipping and using `borrow()`s internal balance check
136
- const poolBalance = borrowerFacet.getBalance();
137
- if (!isGTE(poolBalance, requestedAmount)) {
138
- log(
139
- `Insufficient pool funds`,
140
- `Requested ${q(advanceAmount)} but only have ${q(poolBalance)}`,
141
- );
142
- statusManager.observe(evidence);
143
- return;
144
- }
145
-
146
- try {
147
- // Mark as Advanced since `transferV` initiates the advance.
148
- // Will throw if we've already .skipped or .advanced this evidence.
149
- statusManager.advance(evidence);
150
- } catch (e) {
151
- // Only anticipated error is `assertNotSeen`, so intercept the
152
- // catch so we don't call .skip which also performs this check
153
- log('Advancer error:', q(e).toString());
154
- return;
155
- }
156
-
157
162
  const { zcfSeat: tmpSeat } = zcf.makeEmptySeatKit();
158
163
  const amountKWR = harden({ USDC: advanceAmount });
159
- try {
160
- borrowerFacet.borrow(tmpSeat, amountKWR);
161
- } catch (e) {
162
- // We do not expect this to fail since there are no turn boundaries
163
- // between .getBalance() and .borrow().
164
- // We catch to report outside of the normal error flow since this is
165
- // not expected.
166
- log('🚨 advance borrow failed', q(e).toString());
167
- }
164
+ // throws if the pool has insufficient funds
165
+ borrowerFacet.borrow(tmpSeat, amountKWR);
166
+
167
+ // this cannot throw since `.isSeen()` is called in the same turn
168
+ statusManager.advance(evidence);
168
169
 
169
170
  const depositV = localTransfer(
170
171
  tmpSeat,
@@ -175,7 +176,9 @@ export const prepareAdvancerKit = (
175
176
  void watch(depositV, this.facets.depositHandler, {
176
177
  amount: advanceAmount,
177
178
  destination,
179
+ forwardingAddress: evidence.tx.forwardingAddress,
178
180
  tmpSeat,
181
+ txHash: evidence.txHash,
179
182
  });
180
183
  } catch (e) {
181
184
  log('Advancer error:', q(e).toString());
@@ -186,10 +189,11 @@ export const prepareAdvancerKit = (
186
189
  depositHandler: {
187
190
  /**
188
191
  * @param {undefined} result
189
- * @param {{ amount: Amount<'nat'>; destination: ChainAddress; tmpSeat: ZCFSeat }} ctx
192
+ * @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
190
193
  */
191
- onFulfilled(result, { amount, destination }) {
194
+ onFulfilled(result, ctx) {
192
195
  const { poolAccount } = this.state;
196
+ const { amount, destination, forwardingAddress, txHash } = ctx;
193
197
  const transferV = E(poolAccount).transfer(destination, {
194
198
  denom: usdc.denom,
195
199
  value: amount.value,
@@ -197,11 +201,13 @@ export const prepareAdvancerKit = (
197
201
  return watch(transferV, this.facets.transferHandler, {
198
202
  destination,
199
203
  amount,
204
+ forwardingAddress,
205
+ txHash,
200
206
  });
201
207
  },
202
208
  /**
203
209
  * @param {Error} error
204
- * @param {{ amount: Amount<'nat'>; destination: ChainAddress; tmpSeat: ZCFSeat }} ctx
210
+ * @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx
205
211
  */
206
212
  onRejected(error, { tmpSeat }) {
207
213
  // TODO return seat allocation from ctx to LP?
@@ -217,25 +223,44 @@ export const prepareAdvancerKit = (
217
223
  transferHandler: {
218
224
  /**
219
225
  * @param {undefined} result TODO confirm this is not a bigint (sequence)
220
- * @param {{ destination: ChainAddress; amount: NatAmount; }} ctx
226
+ * @param {AdvancerVowCtx} ctx
221
227
  */
222
- onFulfilled(result, { destination, amount }) {
223
- // TODO vstorage update? We don't currently have a status for
224
- // Advanced + transferV settled
228
+ onFulfilled(result, ctx) {
229
+ const { notifyFacet } = this.state;
230
+ const { amount, destination, forwardingAddress, txHash } = ctx;
225
231
  log(
226
232
  'Advance transfer fulfilled',
227
233
  q({ amount, destination, result }).toString(),
228
234
  );
235
+ notifyFacet.notifyAdvancingResult(
236
+ txHash,
237
+ forwardingAddress,
238
+ amount.value,
239
+ destination.value,
240
+ true,
241
+ );
229
242
  },
230
- onRejected(error) {
231
- // TODO #10510 (comprehensive error testing) determine
232
- // course of action here. This might fail due to timeout.
243
+ /**
244
+ * @param {Error} error
245
+ * @param {AdvancerVowCtx} ctx
246
+ */
247
+ onRejected(error, ctx) {
248
+ const { notifyFacet } = this.state;
249
+ const { amount, destination, forwardingAddress, txHash } = ctx;
233
250
  log('Advance transfer rejected', q(error).toString());
251
+ notifyFacet.notifyAdvancingResult(
252
+ txHash,
253
+ forwardingAddress,
254
+ amount.value,
255
+ destination.value,
256
+ false,
257
+ );
234
258
  },
235
259
  },
236
260
  },
237
261
  {
238
262
  stateShape: harden({
263
+ notifyFacet: M.remotable(),
239
264
  borrowerFacet: M.remotable(),
240
265
  poolAccount: M.remotable(),
241
266
  }),
@@ -1,4 +1,4 @@
1
- import { AmountMath, AmountShape } from '@agoric/ertp';
1
+ import { AmountMath } from '@agoric/ertp';
2
2
  import {
3
3
  makeRecorderTopic,
4
4
  TopicsRecordShape,
@@ -84,7 +84,6 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
84
84
  'Liquidity Pool',
85
85
  {
86
86
  borrower: M.interface('borrower', {
87
- getBalance: M.call().returns(AmountShape),
88
87
  borrow: M.call(
89
88
  SeatShape,
90
89
  harden({ USDC: makeNatAmountShape(USDC, 1n) }),
@@ -152,10 +151,6 @@ export const prepareLiquidityPoolKit = (zone, zcf, USDC, tools) => {
152
151
  },
153
152
  {
154
153
  borrower: {
155
- getBalance() {
156
- const { poolSeat } = this.state;
157
- return poolSeat.getAmountAllocated('USDC', USDC);
158
- },
159
154
  /**
160
155
  * @param {ZCFSeat} toSeat
161
156
  * @param {{ USDC: Amount<'nat'>}} amountKWR
@@ -1,97 +1,291 @@
1
- import { assertAllDefined } from '@agoric/internal';
1
+ import { AmountMath } from '@agoric/ertp';
2
+ import { assertAllDefined, makeTracer } from '@agoric/internal';
2
3
  import { atob } from '@endo/base64';
3
- import { makeError, q } from '@endo/errors';
4
+ import { E } from '@endo/far';
4
5
  import { M } from '@endo/patterns';
5
6
 
7
+ import { PendingTxStatus } from '../constants.js';
6
8
  import { addressTools } from '../utils/address.js';
9
+ import { makeFeeTools } from '../utils/fees.js';
10
+ import { EvmHashShape } from '../type-guards.js';
7
11
 
8
12
  /**
9
13
  * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js';
10
- * @import {Denom} from '@agoric/orchestration';
14
+ * @import {Denom, OrchestrationAccount, ChainHub} from '@agoric/orchestration';
15
+ * @import {WithdrawToSeat} from '@agoric/orchestration/src/utils/zoe-tools'
11
16
  * @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats';
12
17
  * @import {Zone} from '@agoric/zone';
13
- * @import {NobleAddress} from '../types.js';
18
+ * @import {HostOf, HostInterface} from '@agoric/async-flow';
19
+ * @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js';
20
+ * @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash} from '../types.js';
14
21
  * @import {StatusManager} from './status-manager.js';
15
22
  */
16
23
 
24
+ const trace = makeTracer('Settler');
25
+
26
+ /**
27
+ * NOTE: not meant to be parsable.
28
+ *
29
+ * @param {NobleAddress} addr
30
+ * @param {bigint} amount
31
+ */
32
+ const makeMintedEarlyKey = (addr, amount) =>
33
+ `pendingTx:${JSON.stringify([addr, String(amount)])}`;
34
+
17
35
  /**
18
36
  * @param {Zone} zone
19
37
  * @param {object} caps
20
38
  * @param {StatusManager} caps.statusManager
39
+ * @param {Brand<'nat'>} caps.USDC
40
+ * @param {Pick<ZCF, 'makeEmptySeatKit' | 'atomicRearrange'>} caps.zcf
41
+ * @param {FeeConfig} caps.feeConfig
42
+ * @param {HostOf<WithdrawToSeat>} caps.withdrawToSeat
43
+ * @param {import('@agoric/vow').VowTools} caps.vowTools
44
+ * @param {ChainHub} caps.chainHub
21
45
  */
22
- export const prepareSettler = (zone, { statusManager }) => {
46
+ export const prepareSettler = (
47
+ zone,
48
+ { statusManager, USDC, zcf, feeConfig, withdrawToSeat, vowTools, chainHub },
49
+ ) => {
23
50
  assertAllDefined({ statusManager });
24
- return zone.exoClass(
51
+ return zone.exoClassKit(
25
52
  'Fast USDC Settler',
26
- M.interface('SettlerI', {
27
- receiveUpcall: M.call(M.record()).returns(M.promise()),
28
- }),
53
+ {
54
+ creator: M.interface('SettlerCreatorI', {
55
+ monitorMintingDeposits: M.callWhen().returns(M.any()),
56
+ }),
57
+ tap: M.interface('SettlerTapI', {
58
+ receiveUpcall: M.call(M.record()).returns(M.promise()),
59
+ }),
60
+ notify: M.interface('SettlerNotifyI', {
61
+ notifyAdvancingResult: M.call(
62
+ M.string(),
63
+ M.nat(),
64
+ M.boolean(),
65
+ ).returns(),
66
+ }),
67
+ self: M.interface('SettlerSelfI', {
68
+ disburse: M.call(EvmHashShape, M.string(), M.nat()).returns(
69
+ M.promise(),
70
+ ),
71
+ forward: M.call(
72
+ M.opt(EvmHashShape),
73
+ M.string(),
74
+ M.nat(),
75
+ M.string(),
76
+ ).returns(),
77
+ }),
78
+ transferHandler: M.interface('SettlerTransferI', {
79
+ onFulfilled: M.call(M.any(), M.record()).returns(),
80
+ onRejected: M.call(M.any(), M.record()).returns(),
81
+ }),
82
+ },
29
83
  /**
30
- *
31
84
  * @param {{
32
85
  * sourceChannel: IBCChannelID;
33
- * remoteDenom: Denom
86
+ * remoteDenom: Denom;
87
+ * repayer: LiquidityPoolKit['repayer'];
88
+ * settlementAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>
34
89
  * }} config
35
90
  */
36
- config => harden(config),
91
+ config => {
92
+ return {
93
+ ...config,
94
+ /** @type {HostInterface<TargetRegistration>|undefined} */
95
+ registration: undefined,
96
+ /** @type {SetStore<ReturnType<typeof makeMintedEarlyKey>>} */
97
+ mintedEarly: zone.detached().setStore('mintedEarly'),
98
+ };
99
+ },
37
100
  {
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(
101
+ creator: {
102
+ async monitorMintingDeposits() {
103
+ const { settlementAccount } = this.state;
104
+ const registration = await vowTools.when(
105
+ settlementAccount.monitorTransfers(this.facets.tap),
106
+ );
107
+ assert.typeof(registration, 'object');
108
+ this.state.registration = registration;
109
+ },
110
+ },
111
+ tap: {
112
+ /** @param {VTransferIBCEvent} event */
113
+ async receiveUpcall(event) {
114
+ const { sourceChannel, remoteDenom } = this.state;
115
+ const { packet } = event;
116
+ if (packet.source_channel !== sourceChannel) {
117
+ const { source_channel: actual } = packet;
118
+ trace('unexpected channel', { actual, expected: sourceChannel });
119
+ return;
120
+ }
121
+
122
+ // TODO: why is it safe to cast this without a runtime check?
123
+ const tx = /** @type {FungibleTokenPacketData} */ (
124
+ JSON.parse(atob(packet.data))
125
+ );
126
+
66
127
  // 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)}`,
128
+ const sender = /** @type {NobleAddress} */ (tx.sender);
129
+
130
+ if (tx.denom !== remoteDenom) {
131
+ const { denom: actual } = tx;
132
+ trace('unexpected denom', { actual, expected: remoteDenom });
133
+ return;
134
+ }
135
+
136
+ if (!addressTools.hasQueryParams(tx.receiver)) {
137
+ console.log('not query params', tx.receiver);
138
+ return;
139
+ }
140
+
141
+ const { EUD } = addressTools.getQueryParams(tx.receiver);
142
+ if (!EUD) {
143
+ console.log('no EUD parameter', tx.receiver);
144
+ return;
145
+ }
146
+
147
+ const amount = BigInt(tx.amount); // TODO: what if this throws?
148
+
149
+ const { self } = this.facets;
150
+ const found = statusManager.dequeueStatus(sender, amount);
151
+ trace('dequeued', found, 'for', sender, amount);
152
+ switch (found?.status) {
153
+ case PendingTxStatus.Advanced:
154
+ return self.disburse(found.txHash, sender, amount);
155
+
156
+ case PendingTxStatus.Advancing:
157
+ this.state.mintedEarly.add(makeMintedEarlyKey(sender, amount));
158
+ return;
159
+
160
+ case undefined:
161
+ case PendingTxStatus.Observed:
162
+ case PendingTxStatus.AdvanceFailed:
163
+ default:
164
+ return self.forward(found?.txHash, sender, amount, EUD);
165
+ }
166
+ },
167
+ },
168
+ notify: {
169
+ /**
170
+ * @param {EvmHash} txHash
171
+ * @param {NobleAddress} sender
172
+ * @param {NatValue} amount
173
+ * @param {string} EUD
174
+ * @param {boolean} success
175
+ * @returns {void}
176
+ */
177
+ notifyAdvancingResult(txHash, sender, amount, EUD, success) {
178
+ const { mintedEarly } = this.state;
179
+ const key = makeMintedEarlyKey(sender, amount);
180
+ if (mintedEarly.has(key)) {
181
+ mintedEarly.delete(key);
182
+ if (success) {
183
+ void this.facets.self.disburse(txHash, sender, amount);
184
+ } else {
185
+ void this.facets.self.forward(txHash, sender, amount, EUD);
186
+ }
187
+ } else {
188
+ statusManager.advanceOutcome(sender, amount, success);
189
+ }
190
+ },
191
+ },
192
+ self: {
193
+ /**
194
+ * @param {EvmHash} txHash
195
+ * @param {NobleAddress} sender
196
+ * @param {NatValue} amount
197
+ */
198
+ async disburse(txHash, sender, amount) {
199
+ const { repayer, settlementAccount } = this.state;
200
+ const received = AmountMath.make(USDC, amount);
201
+ const { zcfSeat: settlingSeat } = zcf.makeEmptySeatKit();
202
+ const { calculateSplit } = makeFeeTools(feeConfig);
203
+ const split = calculateSplit(received);
204
+ trace('disbursing', split);
205
+
206
+ // TODO: what if this throws?
207
+ // arguably, it cannot. Even if deposits
208
+ // and notifications get out of order,
209
+ // we don't ever withdraw more than has been deposited.
210
+ await vowTools.when(
211
+ withdrawToSeat(
212
+ // @ts-expect-error Vow vs. Promise stuff. TODO: is this OK???
213
+ settlementAccount,
214
+ settlingSeat,
215
+ harden({ In: received }),
216
+ ),
217
+ );
218
+ zcf.atomicRearrange(
219
+ harden([[settlingSeat, settlingSeat, { In: received }, split]]),
75
220
  );
76
- }
221
+ repayer.repay(settlingSeat, split);
77
222
 
78
- // TODO disperse funds
79
- // ~1. fee to contractFeeAccount
80
- // ~2. remainder in poolAccount
223
+ // update status manager, marking tx `SETTLED`
224
+ statusManager.disbursed(txHash, sender, amount);
225
+ },
226
+ /**
227
+ * @param {EvmHash | undefined} txHash
228
+ * @param {NobleAddress} sender
229
+ * @param {NatValue} amount
230
+ * @param {string} EUD
231
+ */
232
+ forward(txHash, sender, amount, EUD) {
233
+ const { settlementAccount } = this.state;
81
234
 
82
- // update status manager, marking tx `SETTLED`
83
- statusManager.settle(
84
- /** @type {NobleAddress} */ (tx.sender),
85
- BigInt(tx.amount),
86
- );
235
+ const dest = chainHub.makeChainAddress(EUD);
236
+
237
+ // TODO? statusManager.forwarding(txHash, sender, amount);
238
+ const txfrV = E(settlementAccount).transfer(
239
+ dest,
240
+ AmountMath.make(USDC, amount),
241
+ );
242
+ void vowTools.watch(txfrV, this.facets.transferHandler, {
243
+ txHash,
244
+ sender,
245
+ amount,
246
+ });
247
+ },
248
+ },
249
+ transferHandler: {
250
+ /**
251
+ * @param {unknown} result
252
+ * @param {SettlerTransferCtx} ctx
253
+ *
254
+ * @typedef {{
255
+ * txHash: EvmHash;
256
+ * sender: NobleAddress;
257
+ * amount: NatValue;
258
+ * }} SettlerTransferCtx
259
+ */
260
+ onFulfilled(result, ctx) {
261
+ const { txHash, sender, amount } = ctx;
262
+ statusManager.forwarded(txHash, sender, amount);
263
+ },
264
+ /**
265
+ * @param {unknown} _result
266
+ * @param {SettlerTransferCtx} _ctx
267
+ */
268
+ onRejected(_result, _ctx) {
269
+ // const { txHash, sender, amount } = ctx;
270
+ // TODO: statusManager.forwardFailed(txHash, sender, amount);
271
+ },
87
272
  },
88
273
  },
89
274
  {
90
275
  stateShape: harden({
276
+ repayer: M.remotable('Repayer'),
277
+ settlementAccount: M.remotable('Account'),
278
+ registration: M.or(M.undefined(), M.remotable('Registration')),
91
279
  sourceChannel: M.string(),
92
280
  remoteDenom: M.string(),
281
+ mintedEarly: M.remotable('mintedEarly'),
93
282
  }),
94
283
  },
95
284
  );
96
285
  };
97
286
  harden(prepareSettler);
287
+
288
+ /**
289
+ * XXX consider using pickFacet (do we have pickFacets?)
290
+ * @typedef {ReturnType<ReturnType<typeof prepareSettler>>} SettlerKit
291
+ */
@@ -1,14 +1,18 @@
1
1
  import { M } from '@endo/patterns';
2
- import { makeError, q } from '@endo/errors';
2
+ import { Fail, makeError, q } from '@endo/errors';
3
3
 
4
4
  import { appendToStoredArray } from '@agoric/store/src/stores/store-utils.js';
5
- import { CctpTxEvidenceShape, PendingTxShape } from '../type-guards.js';
5
+ import {
6
+ CctpTxEvidenceShape,
7
+ EvmHashShape,
8
+ PendingTxShape,
9
+ } from '../type-guards.js';
6
10
  import { PendingTxStatus } from '../constants.js';
7
11
 
8
12
  /**
9
13
  * @import {MapStore, SetStore} from '@agoric/store';
10
14
  * @import {Zone} from '@agoric/zone';
11
- * @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx} from '../types.js';
15
+ * @import {CctpTxEvidence, NobleAddress, SeenTxKey, PendingTxKey, PendingTx, EvmHash} from '../types.js';
12
16
  */
13
17
 
14
18
  /**
@@ -96,21 +100,69 @@ export const prepareStatusManager = zone => {
96
100
  return zone.exo(
97
101
  'Fast USDC Status Manager',
98
102
  M.interface('StatusManagerI', {
103
+ // TODO: naming scheme for transition events
99
104
  advance: M.call(CctpTxEvidenceShape).returns(M.undefined()),
105
+ advanceOutcome: M.call(M.string(), M.nat(), M.boolean()).returns(),
100
106
  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()),
107
+ hasBeenObserved: M.call(CctpTxEvidenceShape).returns(M.boolean()),
108
+ dequeueStatus: M.call(M.string(), M.bigint()).returns(
109
+ M.or(
110
+ {
111
+ txHash: EvmHashShape,
112
+ status: M.or(
113
+ PendingTxStatus.Advanced,
114
+ PendingTxStatus.AdvanceFailed,
115
+ PendingTxStatus.Observed,
116
+ ),
117
+ },
118
+ M.undefined(),
119
+ ),
120
+ ),
121
+ disbursed: M.call(EvmHashShape, M.string(), M.nat()).returns(
122
+ M.undefined(),
123
+ ),
124
+ forwarded: M.call(M.opt(EvmHashShape), M.string(), M.nat()).returns(
125
+ M.undefined(),
126
+ ),
103
127
  lookupPending: M.call(M.string(), M.bigint()).returns(
104
128
  M.arrayOf(PendingTxShape),
105
129
  ),
106
130
  }),
107
131
  {
108
132
  /**
109
- * Add a new transaction with ADVANCED status
133
+ * Add a new transaction with ADVANCING status
110
134
  * @param {CctpTxEvidence} evidence
111
135
  */
112
136
  advance(evidence) {
113
- recordPendingTx(evidence, PendingTxStatus.Advanced);
137
+ recordPendingTx(evidence, PendingTxStatus.Advancing);
138
+ },
139
+
140
+ /**
141
+ * Record result of ADVANCING
142
+ *
143
+ * @param {NobleAddress} sender
144
+ * @param {import('@agoric/ertp').NatValue} amount
145
+ * @param {boolean} success - Advanced vs. AdvanceFailed
146
+ * @throws {Error} if nothing to advance
147
+ */
148
+ advanceOutcome(sender, amount, success) {
149
+ const key = makePendingTxKey(sender, amount);
150
+ pendingTxs.has(key) || Fail`no advancing tx with ${{ sender, amount }}`;
151
+ const pending = pendingTxs.get(key);
152
+ const ix = pending.findIndex(
153
+ tx => tx.status === PendingTxStatus.Advancing,
154
+ );
155
+ ix >= 0 || Fail`no advancing tx with ${{ sender, amount }}`;
156
+ const [prefix, tx, suffix] = [
157
+ pending.slice(0, ix),
158
+ pending[ix],
159
+ pending.slice(ix + 1),
160
+ ];
161
+ const status = success
162
+ ? PendingTxStatus.Advanced
163
+ : PendingTxStatus.AdvanceFailed;
164
+ const txpost = { ...tx, status };
165
+ pendingTxs.set(key, harden([...prefix, txpost, ...suffix]));
114
166
  },
115
167
 
116
168
  /**
@@ -122,41 +174,76 @@ export const prepareStatusManager = zone => {
122
174
  },
123
175
 
124
176
  /**
125
- * Find an `ADVANCED` or `OBSERVED` tx waiting to be `SETTLED`
177
+ * Note: ADVANCING state implies tx has been OBSERVED
126
178
  *
127
- * @param {NobleAddress} address
128
- * @param {bigint} amount
129
- * @returns {boolean}
179
+ * @param {CctpTxEvidence} evidence
130
180
  */
131
- hasPendingSettlement(address, amount) {
132
- const key = makePendingTxKey(address, amount);
133
- const pending = pendingTxs.get(key);
134
- return !!pending.length;
181
+ hasBeenObserved(evidence) {
182
+ const seenKey = seenTxKeyOf(evidence);
183
+ return seenTxs.has(seenKey);
135
184
  },
136
185
 
137
186
  /**
138
- * Mark an `ADVANCED` or `OBSERVED` transaction as `SETTLED` and remove it
187
+ * Remove and return an `ADVANCED` or `OBSERVED` tx waiting to be `SETTLED`.
139
188
  *
140
189
  * @param {NobleAddress} address
141
190
  * @param {bigint} amount
191
+ * @returns {Pick<PendingTx, 'status' | 'txHash'> | undefined} undefined if nothing
192
+ * with this address and amount has been marked pending.
142
193
  */
143
- settle(address, amount) {
194
+ dequeueStatus(address, amount) {
144
195
  const key = makePendingTxKey(address, amount);
196
+ if (!pendingTxs.has(key)) return undefined;
145
197
  const pending = pendingTxs.get(key);
146
198
 
147
- if (!pending.length) {
148
- throw makeError(`No unsettled entry for ${q(key)}`);
199
+ const dequeueIdx = pending.findIndex(
200
+ x => x.status !== PendingTxStatus.Advancing,
201
+ );
202
+ if (dequeueIdx < 0) return undefined;
203
+
204
+ if (pending.length > 1) {
205
+ const pendingCopy = [...pending];
206
+ pendingCopy.splice(dequeueIdx, 1);
207
+ pendingTxs.set(key, harden(pendingCopy));
208
+ } else {
209
+ pendingTxs.delete(key);
149
210
  }
150
211
 
151
- const pendingCopy = [...pending];
152
- pendingCopy.shift();
153
- // TODO, vstorage update for `TxStatus.Settled`
154
- pendingTxs.set(key, harden(pendingCopy));
212
+ const { status, txHash } = pending[dequeueIdx];
213
+ // TODO: store txHash -> evidence for txs pending settlement?
214
+ // If necessary for vstorage writes in `forwarded` and `settled`
215
+ return harden({ status, txHash });
216
+ },
217
+
218
+ /**
219
+ * Mark a transaction as `DISBURSED`
220
+ *
221
+ * @param {EvmHash} txHash
222
+ * @param {NobleAddress} address
223
+ * @param {bigint} amount
224
+ */
225
+ disbursed(txHash, address, amount) {
226
+ // TODO: store txHash -> evidence for txs pending settlement?
227
+ console.log('TODO: vstorage update', { txHash, address, amount });
228
+ },
229
+
230
+ /**
231
+ * Mark a transaction as `FORWARDED`
232
+ *
233
+ * @param {EvmHash | undefined} txHash - undefined in case mint before observed
234
+ * @param {NobleAddress} address
235
+ * @param {bigint} amount
236
+ */
237
+ forwarded(txHash, address, amount) {
238
+ // TODO: store txHash -> evidence for txs pending settlement?
239
+ console.log('TODO: vstorage update', { txHash, address, amount });
155
240
  },
156
241
 
157
242
  /**
158
243
  * Lookup all pending entries for a given address and amount
159
244
  *
245
+ * XXX only used in tests. should we remove?
246
+ *
160
247
  * @param {NobleAddress} address
161
248
  * @param {bigint} amount
162
249
  * @returns {PendingTx[]}
@@ -164,7 +251,7 @@ export const prepareStatusManager = zone => {
164
251
  lookupPending(address, amount) {
165
252
  const key = makePendingTxKey(address, amount);
166
253
  if (!pendingTxs.has(key)) {
167
- throw makeError(`Key ${q(key)} not yet observed`);
254
+ return harden([]);
168
255
  }
169
256
  return pendingTxs.get(key);
170
257
  },
@@ -71,14 +71,28 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
71
71
  const terms = zcf.getTerms();
72
72
  assert('USDC' in terms.brands, 'no USDC brand');
73
73
  assert('usdcDenom' in terms, 'no usdcDenom');
74
+
74
75
  const { feeConfig, marshaller } = privateArgs;
75
76
  const { makeRecorderKit } = prepareRecorderKitMakers(
76
77
  zone.mapStore('vstorage'),
77
78
  marshaller,
78
79
  );
80
+
79
81
  const statusManager = prepareStatusManager(zone);
80
- const makeSettler = prepareSettler(zone, { statusManager });
82
+
83
+ const { USDC } = terms.brands;
84
+ const { withdrawToSeat } = tools.zoeTools;
81
85
  const { chainHub, orchestrateAll, vowTools } = tools;
86
+ const makeSettler = prepareSettler(zone, {
87
+ statusManager,
88
+ USDC,
89
+ withdrawToSeat,
90
+ feeConfig,
91
+ vowTools: tools.vowTools,
92
+ zcf,
93
+ chainHub,
94
+ });
95
+
82
96
  const { localTransfer } = makeZoeTools(zcf, vowTools);
83
97
  const makeAdvancer = prepareAdvancer(zone, {
84
98
  chainHub,
@@ -92,8 +106,10 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
92
106
  vowTools,
93
107
  zcf,
94
108
  });
109
+
95
110
  const makeFeedKit = prepareTransactionFeedKit(zone, zcf);
96
111
  assertAllDefined({ makeFeedKit, makeAdvancer, makeSettler, statusManager });
112
+
97
113
  const makeLiquidityPoolKit = prepareLiquidityPoolKit(
98
114
  zone,
99
115
  zcf,
@@ -111,7 +127,6 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
111
127
  const creatorFacet = zone.exo('Fast USDC Creator', undefined, {
112
128
  /** @type {(operatorId: string) => Promise<Invitation<OperatorKit>>} */
113
129
  async makeOperatorInvitation(operatorId) {
114
- // eslint-disable-next-line no-use-before-define
115
130
  return feedKit.creator.makeOperatorInvitation(operatorId);
116
131
  },
117
132
  /**
@@ -157,7 +172,6 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
157
172
  * @param {CctpTxEvidence} evidence
158
173
  */
159
174
  makeTestPushInvitation(evidence) {
160
- // eslint-disable-next-line no-use-before-define
161
175
  void advancer.handleTransactionEvent(evidence);
162
176
  return makeTestInvitation();
163
177
  },
@@ -200,18 +214,27 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
200
214
 
201
215
  const feedKit = zone.makeOnce('Feed Kit', () => makeFeedKit());
202
216
 
203
- const poolAccountV =
204
- // cast to HostInterface
205
- /** @type { Vow<HostInterface<OrchestrationAccount<{chainId: 'agoric';}>>>} */ (
206
- /** @type {unknown}*/ (
207
- zone.makeOnce('Pool Local Orch Account', () => makeLocalAccount())
208
- )
209
- );
210
- const poolAccount = await vowTools.when(poolAccountV);
217
+ const poolAccountV = zone.makeOnce('PoolAccount', () => makeLocalAccount());
218
+ const settleAccountV = zone.makeOnce('SettleAccount', () =>
219
+ makeLocalAccount(),
220
+ );
221
+ // when() is OK here since this clearly resolves promptly.
222
+ /** @type {HostInterface<OrchestrationAccount<{chainId: 'agoric';}>>[]} */
223
+ const [poolAccount, settlementAccount] = await vowTools.when(
224
+ vowTools.all([poolAccountV, settleAccountV]),
225
+ );
226
+
227
+ const settlerKit = makeSettler({
228
+ repayer: poolKit.repayer,
229
+ sourceChannel: 'channel-1234', // TODO: fix this as soon as testing needs it',
230
+ remoteDenom: 'uusdc',
231
+ settlementAccount,
232
+ });
211
233
 
212
234
  const advancer = zone.makeOnce('Advancer', () =>
213
235
  makeAdvancer({
214
236
  borrowerFacet: poolKit.borrower,
237
+ notifyFacet: settlerKit.notify,
215
238
  poolAccount,
216
239
  }),
217
240
  );
@@ -226,6 +249,8 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
226
249
  },
227
250
  });
228
251
 
252
+ await settlerKit.creator.monitorMintingDeposits();
253
+
229
254
  return harden({ creatorFacet, publicFacet });
230
255
  };
231
256
  harden(contract);
package/src/types.ts CHANGED
@@ -73,3 +73,4 @@ export interface FeedPolicy {
73
73
  }
74
74
 
75
75
  export type * from './constants.js';
76
+ export type { LiquidityPoolKit } from './exos/liquidity-pool.js';
@@ -1,26 +0,0 @@
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
- ```