Skip to content

Commit 305c98c

Browse files
committed
feat: Advancer exo behaviors
- refs: #10390
1 parent d1d7829 commit 305c98c

9 files changed

+541
-200
lines changed

packages/fast-usdc/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@agoric/notifier": "^0.6.2",
3838
"@agoric/orchestration": "^0.1.0",
3939
"@agoric/store": "^0.9.2",
40+
"@agoric/vat-data": "^0.5.2",
4041
"@agoric/vow": "^0.1.0",
4142
"@agoric/zoe": "^0.26.2",
4243
"@endo/base64": "^1.0.8",
+192-79
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,237 @@
1+
import { AmountMath, AmountShape, PaymentShape } 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';
68
import { M } from '@endo/patterns';
79
import { CctpTxEvidenceShape } from '../typeGuards.js';
810
import { addressTools } from '../utils/address.js';
911

12+
const { isGTE } = AmountMath;
13+
1014
/**
1115
* @import {HostInterface} from '@agoric/async-flow';
16+
* @import {NatAmount} from '@agoric/ertp';
1217
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
1318
* @import {VowTools} from '@agoric/vow';
1419
* @import {Zone} from '@agoric/zone';
1520
* @import {CctpTxEvidence, LogFn} from '../types.js';
1621
* @import {StatusManager} from './status-manager.js';
17-
* @import {TransactionFeedKit} from './transaction-feed.js';
1822
*/
1923

24+
/**
25+
* Expected interface from LiquidityPool
26+
*
27+
* @typedef {{
28+
* lookupBalance(): NatAmount;
29+
* borrow(amount: Amount<"nat">): Promise<Payment<"nat">>;
30+
* repay(payments: PaymentKeywordRecord): Promise<void>
31+
* }} AssetManagerFacet
32+
*/
33+
34+
/**
35+
* @typedef {{
36+
* chainHub: ChainHub;
37+
* log: LogFn;
38+
* statusManager: StatusManager;
39+
* USDC: { brand: Brand<'nat'>; denom: Denom; };
40+
* vowTools: VowTools;
41+
* }} AdvancerKitPowers
42+
*/
43+
44+
/** type guards internal to the AdvancerKit */
45+
const AdvancerKitI = harden({
46+
advancer: M.interface('AdvancerI', {
47+
handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(),
48+
}),
49+
depositHandler: M.interface('DepositHandlerI', {
50+
onFulfilled: M.call(AmountShape, {
51+
destination: ChainAddressShape,
52+
payment: PaymentShape,
53+
}).returns(VowShape),
54+
onRejected: M.call(M.error(), {
55+
destination: ChainAddressShape,
56+
payment: PaymentShape,
57+
}).returns(),
58+
}),
59+
transferHandler: M.interface('TransferHandlerI', {
60+
// TODO confirm undefined, and not bigint (sequence)
61+
onFulfilled: M.call(M.undefined(), {
62+
amount: AmountShape,
63+
destination: ChainAddressShape,
64+
}).returns(M.undefined()),
65+
onRejected: M.call(M.error(), {
66+
amount: AmountShape,
67+
destination: ChainAddressShape,
68+
}).returns(M.undefined()),
69+
}),
70+
});
71+
2072
/**
2173
* @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
74+
* @param {AdvancerKitPowers} caps
2775
*/
28-
export const prepareAdvancer = (
76+
export const prepareAdvancerKit = (
2977
zone,
30-
{ chainHub, log, statusManager, vowTools: { watch } },
78+
{ chainHub, log, statusManager, USDC, vowTools: { watch, when } },
3179
) => {
32-
assertAllDefined({ statusManager, watch });
80+
assertAllDefined({
81+
chainHub,
82+
statusManager,
83+
watch,
84+
when,
85+
});
3386

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-
}),
47-
{
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-
},
63-
},
64-
);
87+
/** @param {bigint} value */
88+
const toAmount = value => AmountMath.make(USDC.brand, value);
6589

66-
return zone.exoClass(
90+
return zone.exoClassKit(
6791
'Fast USDC Advancer',
68-
M.interface('AdvancerI', {
69-
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
70-
}),
92+
AdvancerKitI,
7193
/**
7294
* @param {{
73-
* localDenom: Denom;
74-
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
95+
* assetManagerFacet: AssetManagerFacet;
96+
* poolAccount: ERef<HostInterface<OrchestrationAccount<{chainId: 'agoric';}>>>
7597
* }} config
7698
*/
7799
config => harden(config),
78100
{
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-
}
90-
91-
// TODO #10391 this can throw, and should make a status update in the catch
92-
const destination = chainHub.makeChainAddress(EUD);
93-
94-
/** @type {DenomAmount} */
95-
const requestedAmount = harden({
96-
denom: this.state.localDenom,
97-
value: BigInt(evidence.tx.amount),
98-
});
101+
advancer: {
102+
/**
103+
* Must perform a status update for every observed transaction.
104+
*
105+
* We do not expect any callers to depend on the settlement of
106+
* `handleTransactionEvent` - errors caught are communicated to the
107+
* `StatusManager` - so we don't need to concern ourselves with
108+
* preserving the vow chain for callers.
109+
*
110+
* @param {CctpTxEvidence} evidence
111+
*/
112+
async handleTransactionEvent(evidence) {
113+
await null;
114+
try {
115+
// TODO poolAccount might be a vow we need to unwrap
116+
const { assetManagerFacet, poolAccount } = this.state;
117+
const { recipientAddress } = evidence.aux;
118+
const { EUD } =
119+
addressTools.getQueryParams(recipientAddress).params;
120+
if (!EUD) {
121+
throw makeError(
122+
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
123+
);
124+
}
99125

100-
// TODO #10391 ensure there's enough funds in poolAccount
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);
101129

102-
const transferV = E(this.state.poolAccount).transfer(
103-
destination,
104-
requestedAmount,
105-
);
130+
// TODO: consider skipping and using `borrow()`s internal balance check
131+
const poolBalance = assetManagerFacet.lookupBalance();
132+
if (!isGTE(poolBalance, requestedAmount)) {
133+
log(
134+
`Insufficient pool funds`,
135+
`Requested ${q(requestedAmount)} but only have ${q(poolBalance)}`,
136+
);
137+
statusManager.observe(evidence);
138+
return;
139+
}
106140

107-
// mark as Advanced since `transferV` initiates the advance
108-
statusManager.advance(evidence);
141+
try {
142+
// Mark as Advanced since `transferV` initiates the advance.
143+
// Will throw if we've already .skipped or .advanced this evidence.
144+
statusManager.advance(evidence);
145+
} catch (e) {
146+
// Only anticipated error is `assertNotSeen`, so intercept the
147+
// catch so we don't call .skip which also performs this check
148+
log('Advancer error:', q(e).toString());
149+
return;
150+
}
109151

110-
return watch(transferV, transferHandler, {
111-
destination,
112-
amount: requestedAmount.value,
113-
});
152+
try {
153+
const payment = await assetManagerFacet.borrow(requestedAmount);
154+
const depositV = E(poolAccount).deposit(payment);
155+
void watch(depositV, this.facets.depositHandler, {
156+
destination,
157+
payment,
158+
});
159+
} catch (e) {
160+
// `.borrow()` might fail if the balance changes since we
161+
// requested it. TODO - how to handle this? change ADVANCED -> OBSERVED?
162+
// Note: `depositHandler` handles the `.deposit()` failure
163+
log('🚨 advance borrow failed', q(e).toString());
164+
}
165+
} catch (e) {
166+
log('Advancer error:', q(e).toString());
167+
statusManager.observe(evidence);
168+
}
169+
},
170+
},
171+
depositHandler: {
172+
/**
173+
* @param {NatAmount} amount amount returned from deposit
174+
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
175+
*/
176+
onFulfilled(amount, { destination }) {
177+
const { poolAccount } = this.state;
178+
const transferV = E(poolAccount).transfer(
179+
destination,
180+
/** @type {DenomAmount} */ ({
181+
denom: USDC.denom,
182+
value: amount.value,
183+
}),
184+
);
185+
return watch(transferV, this.facets.transferHandler, {
186+
destination,
187+
amount,
188+
});
189+
},
190+
/**
191+
* @param {Error} error
192+
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
193+
*/
194+
onRejected(error, { payment }) {
195+
// TODO return live payment from ctx to LP
196+
log('🚨 advance deposit failed', q(error).toString());
197+
log('TODO live payment to return to LP', q(payment).toString());
198+
},
199+
},
200+
transferHandler: {
201+
/**
202+
* @param {undefined} result TODO confirm this is not a bigint (sequence)
203+
* @param {{ destination: ChainAddress; amount: NatAmount; }} ctx
204+
*/
205+
onFulfilled(result, { destination, amount }) {
206+
// TODO vstorage update?
207+
log(
208+
'Advance transfer fulfilled',
209+
q({ amount, destination, result }).toString(),
210+
);
211+
},
212+
onRejected(error) {
213+
// XXX retry logic?
214+
// What do we do if we fail, should we keep a Status?
215+
log('Advance transfer rejected', q(error).toString());
216+
},
114217
},
115218
},
116219
{
117220
stateShape: harden({
118-
localDenom: M.string(),
119-
poolAccount: M.remotable(),
221+
assetManagerFacet: M.remotable(),
222+
poolAccount: M.or(VowShape, M.remotable()),
120223
}),
121224
},
122225
);
123226
};
227+
harden(prepareAdvancerKit);
228+
229+
/**
230+
* @param {Zone} zone
231+
* @param {AdvancerKitPowers} caps
232+
*/
233+
export const prepareAdvancer = (zone, caps) => {
234+
const makeAdvancerKit = prepareAdvancerKit(zone, caps);
235+
return pickFacet(makeAdvancerKit, 'advancer');
236+
};
124237
harden(prepareAdvancer);

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

+10-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { defineInertInvitation } from './utils/zoe.js';
1616
const trace = makeTracer('FastUsdc');
1717

1818
/**
19+
* @import {Denom} from '@agoric/orchestration';
1920
* @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js';
2021
* @import {Zone} from '@agoric/zone';
2122
* @import {OperatorKit} from './exos/operator-kit.js';
@@ -26,13 +27,15 @@ const trace = makeTracer('FastUsdc');
2627
* @typedef {{
2728
* poolFee: Amount<'nat'>;
2829
* contractFee: Amount<'nat'>;
30+
* usdcDenom: Denom;
2931
* }} FastUsdcTerms
3032
*/
3133
const NatAmountShape = { brand: BrandShape, value: M.nat() };
3234
export const meta = {
3335
customTermsShape: {
3436
contractFee: NatAmountShape,
3537
poolFee: NatAmountShape,
38+
usdcDenom: M.string(),
3639
},
3740
};
3841
harden(meta);
@@ -49,6 +52,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
4952
assert(tools, 'no tools');
5053
const terms = zcf.getTerms();
5154
assert('USDC' in terms.brands, 'no USDC brand');
55+
assert('usdcDenom' in terms, 'no usdcDenom');
5256

5357
const { makeRecorderKit } = prepareRecorderKitMakers(
5458
zone.mapStore('vstorage'),
@@ -61,6 +65,10 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
6165
const makeAdvancer = prepareAdvancer(zone, {
6266
chainHub,
6367
log: trace,
68+
USDC: harden({
69+
brand: terms.brands.USDC,
70+
denom: terms.usdcDenom,
71+
}),
6472
statusManager,
6573
vowTools,
6674
});
@@ -75,7 +83,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
7583
void observeIteration(subscribeEach(feedKit.public.getEvidenceSubscriber()), {
7684
updateState(evidence) {
7785
try {
78-
advancer.handleTransactionEvent(evidence);
86+
void advancer.handleTransactionEvent(evidence);
7987
} catch (err) {
8088
trace('🚨 Error handling transaction event', err);
8189
}
@@ -117,7 +125,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
117125
* @param {CctpTxEvidence} evidence
118126
*/
119127
makeTestPushInvitation(evidence) {
120-
advancer.handleTransactionEvent(evidence);
128+
void advancer.handleTransactionEvent(evidence);
121129
return makeTestInvitation();
122130
},
123131
makeDepositInvitation() {

0 commit comments

Comments
 (0)