1
+ import { AmountMath , AmountShape , BrandShape } from '@agoric/ertp' ;
1
2
import { assertAllDefined } from '@agoric/internal' ;
2
3
import { ChainAddressShape } from '@agoric/orchestration' ;
4
+ import { pickFacet } from '@agoric/vat-data' ;
3
5
import { VowShape } from '@agoric/vow' ;
4
6
import { makeError , q } from '@endo/errors' ;
5
7
import { E } from '@endo/far' ;
@@ -9,116 +11,220 @@ import { addressTools } from '../utils/address.js';
9
11
10
12
/**
11
13
* @import {HostInterface} from '@agoric/async-flow';
14
+ * @import {NatAmount} from '@agoric/ertp';
12
15
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
13
16
* @import {VowTools} from '@agoric/vow';
14
17
* @import {Zone} from '@agoric/zone';
15
18
* @import {CctpTxEvidence, LogFn} from '../types.js';
16
19
* @import {StatusManager} from './status-manager.js';
17
- * @import {TransactionFeedKit} from './transaction-feed.js';
18
20
*/
19
21
22
+ /**
23
+ * Expected interface from LiquidityPool
24
+ *
25
+ * @typedef {{
26
+ * lookupBalance(): NatAmount;
27
+ * borrowUnderlying(amount: Amount<"nat">): Promise<PaymentPKeywordRecord>;
28
+ * returnUnderlying(principalPayment: Payment<"nat">, feePayment: Payment<"nat">): Promise<void>
29
+ * }} AssetManagerFacet
30
+ */
31
+
32
+ /**
33
+ * @typedef {{
34
+ * chainHub: ChainHub;
35
+ * log: LogFn;
36
+ * statusManager: StatusManager;
37
+ * vowTools: VowTools;
38
+ * }} AdvancerKitCaps
39
+ */
40
+
41
+ /** type guards internal to the AdvancerKit */
42
+ const WatcherHandlersShape = {
43
+ depositHandler : M . interface ( 'DepositHandlerI' , {
44
+ onFulfilled : M . call ( AmountShape , ChainAddressShape ) . returns ( VowShape ) ,
45
+ } ) ,
46
+ transferHandler : M . interface ( 'TransferHandlerI' , {
47
+ // TODO confirm undefined, and not bigint (sequence)
48
+ onFulfilled : M . call ( M . undefined ( ) , {
49
+ amount : AmountShape ,
50
+ destination : ChainAddressShape ,
51
+ } ) . returns ( M . undefined ( ) ) ,
52
+ onRejected : M . call ( M . error ( ) , {
53
+ amount : AmountShape ,
54
+ destination : ChainAddressShape ,
55
+ } ) . returns ( M . undefined ( ) ) ,
56
+ } ) ,
57
+ } ;
58
+
20
59
/**
21
60
* @param {Zone } zone
22
- * @param {object } caps
23
- * @param {ChainHub } caps.chainHub
24
- * @param {LogFn } caps.log
25
- * @param {StatusManager } caps.statusManager
26
- * @param {VowTools } caps.vowTools
61
+ * @param {AdvancerKitCaps } caps
27
62
*/
28
- export const prepareAdvancer = (
63
+ export const prepareAdvancerKit = (
29
64
zone ,
30
- { chainHub, log, statusManager, vowTools : { watch } } ,
65
+ { chainHub, log, statusManager, vowTools : { watch, when } } ,
31
66
) => {
32
- assertAllDefined ( { statusManager, watch } ) ;
67
+ assertAllDefined ( {
68
+ chainHub,
69
+ statusManager,
70
+ watch,
71
+ when,
72
+ } ) ;
33
73
34
- const transferHandler = zone . exo (
35
- 'Fast USDC Advance Transfer Handler' ,
36
- M . interface ( 'TransferHandlerI' , {
37
- // TODO confirm undefined, and not bigint (sequence)
38
- onFulfilled : M . call ( M . undefined ( ) , {
39
- amount : M . bigint ( ) ,
40
- destination : ChainAddressShape ,
41
- } ) . returns ( M . undefined ( ) ) ,
42
- onRejected : M . call ( M . error ( ) , {
43
- amount : M . bigint ( ) ,
44
- destination : ChainAddressShape ,
45
- } ) . returns ( M . undefined ( ) ) ,
46
- } ) ,
74
+ return zone . exoClassKit (
75
+ 'Fast USDC Advancer' ,
47
76
{
48
- /**
49
- * @param {undefined } result TODO confirm this is not a bigint (sequence)
50
- * @param {{ destination: ChainAddress; amount: bigint; } } ctx
51
- */
52
- onFulfilled ( result , { destination, amount } ) {
53
- log (
54
- 'Advance transfer fulfilled' ,
55
- q ( { amount, destination, result } ) . toString ( ) ,
56
- ) ;
57
- } ,
58
- onRejected ( error ) {
59
- // XXX retry logic?
60
- // What do we do if we fail, should we keep a Status?
61
- log ( 'Advance transfer rejected' , q ( error ) . toString ( ) ) ;
62
- } ,
77
+ advancer : M . interface ( 'AdvancerI' , {
78
+ handleTransactionEvent : M . callWhen ( CctpTxEvidenceShape ) . returns (
79
+ M . or ( M . undefined ( ) , VowShape ) ,
80
+ ) ,
81
+ } ) ,
82
+ ...WatcherHandlersShape ,
63
83
} ,
64
- ) ;
65
-
66
- return zone . exoClass (
67
- 'Fast USDC Advancer' ,
68
- M . interface ( 'AdvancerI' , {
69
- handleTransactionEvent : M . call ( CctpTxEvidenceShape ) . returns ( VowShape ) ,
70
- } ) ,
71
84
/**
72
85
* @param {{
86
+ * assetManagerFacet: AssetManagerFacet;
73
87
* localDenom: Denom;
74
- * poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
88
+ * poolAccount: ERef<HostInterface<OrchestrationAccount<{chainId: 'agoric';}>>>
89
+ * usdcBrand: Brand<'nat'>;
75
90
* }} config
76
91
*/
77
92
config => harden ( config ) ,
78
93
{
79
- /** @param {CctpTxEvidence } evidence */
80
- handleTransactionEvent ( evidence ) {
81
- // TODO EventFeed will perform input validation checks.
82
- const { recipientAddress } = evidence . aux ;
83
- const { EUD } = addressTools . getQueryParams ( recipientAddress ) . params ;
84
- if ( ! EUD ) {
85
- statusManager . observe ( evidence ) ;
86
- throw makeError (
87
- `recipientAddress does not contain EUD param: ${ q ( recipientAddress ) } ` ,
88
- ) ;
89
- }
94
+ advancer : {
95
+ /**
96
+ * Returns a Promise for a Vow in the happy path, or undefined
97
+ * when conditions are not met. Aims to perform a status update for
98
+ * every observed transaction.
99
+ *
100
+ * We do not expect any callers to depend on the settlement of
101
+ * `handleTransactionEvent` - errors caught are communicated to the
102
+ * `StatusManager` - so we don't need to concern ourselves with
103
+ * preserving the vow chain for callers.
104
+ *
105
+ * @param {CctpTxEvidence } evidence
106
+ */
107
+ async handleTransactionEvent ( evidence ) {
108
+ await null ;
109
+ try {
110
+ const { assetManagerFacet, usdcBrand } = this . state ;
111
+ // XXX better way?
112
+ const poolAccount = await when ( this . state . poolAccount ) ;
113
+ const { recipientAddress } = evidence . aux ;
114
+ const { EUD } =
115
+ addressTools . getQueryParams ( recipientAddress ) . params ;
116
+ if ( ! EUD ) {
117
+ throw makeError (
118
+ `recipientAddress does not contain EUD param: ${ q ( recipientAddress ) } ` ,
119
+ ) ;
120
+ }
90
121
91
- // TODO #10391 this can throw, and should make a status update in the catch
92
- const destination = chainHub . makeChainAddress ( EUD ) ;
122
+ // this will throw if the bech32 prefix is not found, but is handled by the catch
123
+ const destination = chainHub . makeChainAddress ( EUD ) ;
93
124
94
- /** @type {DenomAmount } */
95
- const requestedAmount = harden ( {
96
- denom : this . state . localDenom ,
97
- value : BigInt ( evidence . tx . amount ) ,
98
- } ) ;
125
+ const requestedValue = BigInt ( evidence . tx . amount ) ;
126
+ const requestedAmount = AmountMath . make ( usdcBrand , requestedValue ) ;
127
+ const poolBalance = assetManagerFacet . lookupBalance ( ) ;
99
128
100
- // TODO #10391 ensure there's enough funds in poolAccount
129
+ if ( ! AmountMath . isGTE ( poolBalance , requestedAmount ) ) {
130
+ log (
131
+ `Insufficient pool funds` ,
132
+ `Requested ${ q ( requestedAmount ) } but only have ${ q ( poolBalance ) } ` ,
133
+ ) ;
134
+ statusManager . observe ( evidence ) ;
135
+ return ;
136
+ }
101
137
102
- const transferV = E ( this . state . poolAccount ) . transfer (
103
- destination ,
104
- requestedAmount ,
105
- ) ;
138
+ try {
139
+ // mark as Advanced since `transferV` initiates the advance
140
+ // will throw if we've already .skipped or .advanced this evidence
141
+ statusManager . advance ( evidence ) ;
142
+ } catch ( e ) {
143
+ // only anticipated error is `assertNotSeen`, so
144
+ // intercept the catch so we don't call .skip which
145
+ // also performs this check
146
+ log ( 'Advancer error:' , q ( e ) . toString ( ) ) ;
147
+ return ;
148
+ }
106
149
107
- // mark as Advanced since `transferV` initiates the advance
108
- statusManager . advance ( evidence ) ;
150
+ try {
151
+ // should LiquidityPool return a vow here?
152
+ const { USDC : advancePmtP } =
153
+ await assetManagerFacet . borrowUnderlying ( requestedAmount ) ;
109
154
110
- return watch ( transferV , transferHandler , {
111
- destination,
112
- amount : requestedAmount . value ,
113
- } ) ;
155
+ // do we actually need to await here?
156
+ const advancePmt = await advancePmtP ;
157
+ const depositV = E ( poolAccount ) . deposit ( advancePmt ) ;
158
+ return watch ( depositV , this . facets . depositHandler , destination ) ;
159
+ } catch ( e ) {
160
+ // TODO how should we think about failure here?
161
+ log ( 'Ruh roh' , q ( e ) . toString ( ) ) ;
162
+ throw e ;
163
+ }
164
+ } catch ( e ) {
165
+ log ( 'Advancer error:' , q ( e ) . toString ( ) ) ;
166
+ statusManager . observe ( evidence ) ;
167
+ }
168
+ } ,
169
+ } ,
170
+ depositHandler : {
171
+ /**
172
+ * @param {NatAmount } amount amount returned from deposit
173
+ * @param {ChainAddress } destination
174
+ */
175
+ onFulfilled ( amount , destination ) {
176
+ const { localDenom, poolAccount } = this . state ;
177
+ const transferV = E ( poolAccount ) . transfer (
178
+ destination ,
179
+ /** @type {DenomAmount } */ ( {
180
+ denom : localDenom ,
181
+ value : amount . value ,
182
+ } ) ,
183
+ ) ;
184
+ return watch ( transferV , this . facets . transferHandler , {
185
+ destination,
186
+ amount,
187
+ } ) ;
188
+ } ,
189
+ // xxx return payment on rejected
190
+ } ,
191
+ transferHandler : {
192
+ /**
193
+ * @param {undefined } result TODO confirm this is not a bigint (sequence)
194
+ * @param {{ destination: ChainAddress; amount: NatAmount; } } ctx
195
+ */
196
+ onFulfilled ( result , { destination, amount } ) {
197
+ // TODO vstorage update?
198
+ log (
199
+ 'Advance transfer fulfilled' ,
200
+ q ( { amount, destination, result } ) . toString ( ) ,
201
+ ) ;
202
+ } ,
203
+ onRejected ( error ) {
204
+ // XXX retry logic?
205
+ // What do we do if we fail, should we keep a Status?
206
+ log ( 'Advance transfer rejected' , q ( error ) . toString ( ) ) ;
207
+ } ,
114
208
} ,
115
209
} ,
116
210
{
117
211
stateShape : harden ( {
212
+ assetManagerFacet : M . remotable ( ) ,
118
213
localDenom : M . string ( ) ,
119
- poolAccount : M . remotable ( ) ,
214
+ poolAccount : M . or ( VowShape , M . remotable ( ) ) ,
215
+ usdcBrand : BrandShape ,
120
216
} ) ,
121
217
} ,
122
218
) ;
123
219
} ;
220
+ harden ( prepareAdvancerKit ) ;
221
+
222
+ /**
223
+ * @param {Zone } zone
224
+ * @param {AdvancerKitCaps } caps
225
+ */
226
+ export const prepareAdvancer = ( zone , caps ) => {
227
+ const makeAdvancerKit = prepareAdvancerKit ( zone , caps ) ;
228
+ return pickFacet ( makeAdvancerKit , 'advancer' ) ;
229
+ } ;
124
230
harden ( prepareAdvancer ) ;
0 commit comments