Skip to content

Commit 751d762

Browse files
committed
feat: Advancer exo behaviors
- refs: #10390
1 parent 757e0b2 commit 751d762

File tree

4 files changed

+363
-145
lines changed

4 files changed

+363
-145
lines changed

packages/fast-usdc/src/exos/advancer.js

+84-37
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AmountMath, BrandShape } from '@agoric/ertp';
12
import { assertAllDefined } from '@agoric/internal';
23
import { ChainAddressShape } from '@agoric/orchestration';
34
import { VowShape } from '@agoric/vow';
@@ -28,7 +29,7 @@ import { addressTools } from '../utils/address.js';
2829
*/
2930
export const prepareAdvancer = (
3031
zone,
31-
{ chainHub, feed, log, statusManager, vowTools: { watch } },
32+
{ chainHub, feed, log, statusManager, vowTools: { watch, when } },
3233
) => {
3334
assertAllDefined({ feed, statusManager, watch });
3435

@@ -51,6 +52,7 @@ export const prepareAdvancer = (
5152
* @param {{ destination: ChainAddress; amount: bigint; }} ctx
5253
*/
5354
onFulfilled(result, { destination, amount }) {
55+
// TODO vstorage update?
5456
log(
5557
'Advance transfer fulfilled',
5658
q({ amount, destination, result }).toString(),
@@ -67,12 +69,15 @@ export const prepareAdvancer = (
6769
return zone.exoClass(
6870
'Fast USDC Advancer',
6971
M.interface('AdvancerI', {
70-
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
72+
handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(
73+
M.or(M.undefined(), VowShape),
74+
),
7175
}),
7276
/**
7377
* @param {{
7478
* localDenom: Denom;
7579
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
80+
* usdcBrand: Brand<'nat'>;
7681
* }} config
7782
*/
7883
config => harden(config),
@@ -81,56 +86,98 @@ export const prepareAdvancer = (
8186
* XXX For now, assume this is invoked when a new entry is
8287
* observed via the EventFeed.
8388
*
89+
* Returns a Promise for a Vow, since we we `await vt.when()`the
90+
* `poolAccount.getBalance()` call. `getBalance()` interfaces with
91+
* `BankManager` and `ChainHub`, so we can expect the calls to resolve
92+
* promptly.
93+
*
94+
*
95+
* This also might return `undefined` (unwrapped) if precondition checks
96+
* fail.
97+
*
98+
* We don't expect any callers to depend on the settlement of
99+
* `handleTransactionEvent` - errors caught are communicated to the
100+
* `StatusManager` - so we don't need to concern ourselves with
101+
* preserving the vow chain for callers.
102+
*
84103
* @param {CctpTxEvidence} evidence
85104
*/
86-
handleTransactionEvent(evidence) {
87-
// XXX assume EventFeed performs validation checks. Advancer will assert uniqueness.
105+
async handleTransactionEvent(evidence) {
106+
await null;
107+
try {
108+
const { recipientAddress } = evidence.aux;
109+
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
110+
// XXX assume EventFeed performs validation checks. Advancer will assert uniqueness.
111+
if (!EUD) {
112+
throw makeError(
113+
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
114+
);
115+
}
88116

89-
const { recipientAddress } = evidence.aux;
90-
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
91-
if (!EUD) {
92-
statusManager.skip(evidence);
93-
throw makeError(
94-
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
95-
);
96-
}
117+
// this will throw if the bech32 prefix is not found
118+
const { chainId } = chainHub.getChainInfoByAddress(EUD);
97119

98-
// TODO #10391 this can throw, and should make a status update in the catch
99-
const { chainId } = chainHub.getChainInfoByAddress(EUD);
100-
101-
/** @type {DenomAmount} */
102-
const requestedAmount = harden({
103-
denom: this.state.localDenom,
104-
value: BigInt(evidence.tx.amount),
105-
});
120+
/** @type {DenomAmount} */
121+
const requestedAmount = harden({
122+
denom: this.state.localDenom,
123+
value: BigInt(evidence.tx.amount),
124+
});
125+
// ensure there's enough funds in poolAccount
126+
// XXX safe? `getBalance` calls out to `BankManager` and `ChainHub`
127+
const poolBalance = await when(
128+
E(this.state.poolAccount).getBalance(this.state.localDenom),
129+
);
106130

107-
// TODO #10391 ensure there's enough funds in poolAccount
131+
if (
132+
!AmountMath.isGTE(
133+
AmountMath.make(this.state.usdcBrand, poolBalance.value),
134+
AmountMath.make(this.state.usdcBrand, requestedAmount.value),
135+
)
136+
) {
137+
log(
138+
`Insufficient pool funds`,
139+
`Requested ${q(requestedAmount)} but only have ${q(poolBalance)}`,
140+
);
141+
statusManager.skip(evidence);
142+
return;
143+
}
108144

109-
/** @type {ChainAddress} */
110-
const destination = harden({
111-
chainId,
112-
value: EUD,
113-
encoding: /** @type {const} */ ('bech32'),
114-
});
145+
/** @type {ChainAddress} */
146+
const destination = harden({
147+
chainId,
148+
value: EUD,
149+
encoding: /** @type {const} */ ('bech32'),
150+
});
115151

116-
const transferV = E(this.state.poolAccount).transfer(
117-
destination,
118-
requestedAmount,
119-
);
152+
// mark as Advanced since we're initiating the advance
153+
try {
154+
// will throw if we've already .skipped or .advanced this evidence
155+
statusManager.advance(evidence);
156+
} catch (e) {
157+
log('Advancer error:', q(e).toString());
158+
return;
159+
}
120160

121-
// mark as Advanced since we're initiating the advance
122-
statusManager.advance(evidence);
161+
const transferV = E(this.state.poolAccount).transfer(
162+
destination,
163+
requestedAmount,
164+
);
123165

124-
return watch(transferV, transferHandler, {
125-
destination,
126-
amount: requestedAmount.value,
127-
});
166+
return watch(transferV, transferHandler, {
167+
destination,
168+
amount: requestedAmount.value,
169+
});
170+
} catch (e) {
171+
log(`Advancer error:`, q(e).toString());
172+
statusManager.skip(evidence);
173+
}
128174
},
129175
},
130176
{
131177
stateShape: harden({
132178
localDenom: M.string(),
133179
poolAccount: M.remotable(),
180+
usdcBrand: BrandShape,
134181
}),
135182
},
136183
);

0 commit comments

Comments
 (0)