Skip to content

Commit 37d455f

Browse files
committed
feat: Advancer exo behaviors
- refs: #10390
1 parent b18b817 commit 37d455f

8 files changed

+518
-198
lines changed

packages/fast-usdc/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@agoric/notifier": "^0.6.2",
3737
"@agoric/orchestration": "^0.1.0",
3838
"@agoric/store": "^0.9.2",
39+
"@agoric/vat-data": "^0.5.2",
3940
"@agoric/vow": "^0.1.0",
4041
"@agoric/zoe": "^0.26.2",
4142
"@endo/base64": "^1.0.8",
+181-75
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { AmountMath, AmountShape, BrandShape } from '@agoric/ertp';
12
import { assertAllDefined } from '@agoric/internal';
23
import { ChainAddressShape } from '@agoric/orchestration';
4+
import { pickFacet } from '@agoric/vat-data';
35
import { VowShape } from '@agoric/vow';
46
import { makeError, q } from '@endo/errors';
57
import { E } from '@endo/far';
@@ -9,116 +11,220 @@ import { addressTools } from '../utils/address.js';
911

1012
/**
1113
* @import {HostInterface} from '@agoric/async-flow';
14+
* @import {NatAmount} from '@agoric/ertp';
1215
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
1316
* @import {VowTools} from '@agoric/vow';
1417
* @import {Zone} from '@agoric/zone';
1518
* @import {CctpTxEvidence, LogFn} from '../types.js';
1619
* @import {StatusManager} from './status-manager.js';
17-
* @import {TransactionFeedKit} from './transaction-feed.js';
1820
*/
1921

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+
2059
/**
2160
* @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
2762
*/
28-
export const prepareAdvancer = (
63+
export const prepareAdvancerKit = (
2964
zone,
30-
{ chainHub, log, statusManager, vowTools: { watch } },
65+
{ chainHub, log, statusManager, vowTools: { watch, when } },
3166
) => {
32-
assertAllDefined({ statusManager, watch });
67+
assertAllDefined({
68+
chainHub,
69+
statusManager,
70+
watch,
71+
when,
72+
});
3373

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',
4776
{
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,
6383
},
64-
);
65-
66-
return zone.exoClass(
67-
'Fast USDC Advancer',
68-
M.interface('AdvancerI', {
69-
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
70-
}),
7184
/**
7285
* @param {{
86+
* assetManagerFacet: AssetManagerFacet;
7387
* localDenom: Denom;
74-
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
88+
* poolAccount: ERef<HostInterface<OrchestrationAccount<{chainId: 'agoric';}>>>
89+
* usdcBrand: Brand<'nat'>;
7590
* }} config
7691
*/
7792
config => harden(config),
7893
{
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+
}
90121

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);
93124

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();
99128

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+
}
101137

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+
}
106149

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);
109154

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+
},
114208
},
115209
},
116210
{
117211
stateShape: harden({
212+
assetManagerFacet: M.remotable(),
118213
localDenom: M.string(),
119-
poolAccount: M.remotable(),
214+
poolAccount: M.or(VowShape, M.remotable()),
215+
usdcBrand: BrandShape,
120216
}),
121217
},
122218
);
123219
};
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+
};
124230
harden(prepareAdvancer);

packages/fast-usdc/src/fast-usdc.contract.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
7272
// Connect evidence stream to advancer
7373
void observeIteration(subscribeEach(feedKit.public.getEvidenceStream()), {
7474
updateState(evidence) {
75-
try {
76-
advancer.handleTransactionEvent(evidence);
77-
} catch (err) {
78-
trace('🚨 Error handling transaction event', err);
79-
}
75+
void advancer.handleTransactionEvent(evidence);
8076
},
8177
});
8278
const makeLiquidityPoolKit = prepareLiquidityPoolKit(

0 commit comments

Comments
 (0)