Skip to content

Commit 6f0bd04

Browse files
committed
feat: Advancer exo behaviors
- refs: #10390
1 parent a92b0ec commit 6f0bd04

File tree

4 files changed

+369
-141
lines changed

4 files changed

+369
-141
lines changed

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

+88-31
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,57 +69,112 @@ 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),
7984
{
80-
/** @param {CctpTxEvidence} evidence */
81-
handleTransactionEvent(evidence) {
82-
// TODO EventFeed will perform input validation checks.
83-
const { recipientAddress } = evidence.aux;
84-
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
85-
if (!EUD) {
86-
statusManager.observe(evidence);
87-
throw makeError(
88-
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
89-
);
90-
}
85+
/**
86+
* Returns a Promise for a Vow , since we we `await vt.when()`the
87+
* `poolAccount.getBalance()` call. `getBalance()` interfaces with
88+
* `BankManager` and `ChainHub`, so we can expect the calls to resolve
89+
* promptly. We also don't care how many time `.getBalance` is called,
90+
* and watched vows are not dependent on its result.
91+
*
92+
* This also might return `undefined` (unwrapped) if precondition checks
93+
* fail.
94+
*
95+
* We do not expect any callers to depend on the settlement of
96+
* `handleTransactionEvent` - errors caught are communicated to the
97+
* `StatusManager` - so we don't need to concern ourselves with
98+
* preserving the vow chain for callers.
99+
*
100+
* @param {CctpTxEvidence} evidence
101+
*/
102+
async handleTransactionEvent(evidence) {
103+
await null;
104+
try {
105+
const { recipientAddress } = evidence.aux;
106+
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
107+
if (!EUD) {
108+
throw makeError(
109+
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
110+
);
111+
}
91112

92-
// TODO #10391 this can throw, and should make a status update in the catch
93-
const destination = chainHub.makeChainAddress(EUD);
113+
// this will throw if the bech32 prefix is not found, but is handled by the catch
114+
const destination = chainHub.makeChainAddress(EUD);
94115

95-
/** @type {DenomAmount} */
96-
const requestedAmount = harden({
97-
denom: this.state.localDenom,
98-
value: BigInt(evidence.tx.amount),
99-
});
116+
/** @type {DenomAmount} */
117+
const requestedAmount = harden({
118+
denom: this.state.localDenom,
119+
value: BigInt(evidence.tx.amount),
120+
});
121+
/**
122+
* Ensure there's enough funds in poolAccount.
123+
*
124+
* It's safe to await here since we don't care how many
125+
* times we call `getBalance`. Our later Vow call - `transferV` and
126+
* its ctx - are also not reliant on the consistency of this value.
127+
*/
128+
const poolBalance = await when(
129+
E(this.state.poolAccount).getBalance(this.state.localDenom),
130+
);
100131

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

103-
const transferV = E(this.state.poolAccount).transfer(
104-
destination,
105-
requestedAmount,
106-
);
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
152+
// intercept the catch so we don't call .skip which
153+
// also performs this check
154+
log('Advancer error:', q(e).toString());
155+
return;
156+
}
107157

108-
// mark as Advanced since `transferV` initiates the advance
109-
statusManager.advance(evidence);
158+
const transferV = E(this.state.poolAccount).transfer(
159+
destination,
160+
requestedAmount,
161+
);
110162

111-
return watch(transferV, transferHandler, {
112-
destination,
113-
amount: requestedAmount.value,
114-
});
163+
return watch(transferV, transferHandler, {
164+
destination,
165+
amount: requestedAmount.value,
166+
});
167+
} catch (e) {
168+
log(`Advancer error:`, q(e).toString());
169+
statusManager.observe(evidence);
170+
}
115171
},
116172
},
117173
{
118174
stateShape: harden({
119175
localDenom: M.string(),
120176
poolAccount: M.remotable(),
177+
usdcBrand: BrandShape,
121178
}),
122179
},
123180
);

0 commit comments

Comments
 (0)