Skip to content

Commit

Permalink
feat: Advancer exo behaviors
Browse files Browse the repository at this point in the history
- refs: #10390
  • Loading branch information
0xpatrickdev committed Nov 13, 2024
1 parent b18b817 commit 37d455f
Show file tree
Hide file tree
Showing 8 changed files with 518 additions and 198 deletions.
1 change: 1 addition & 0 deletions packages/fast-usdc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@agoric/notifier": "^0.6.2",
"@agoric/orchestration": "^0.1.0",
"@agoric/store": "^0.9.2",
"@agoric/vat-data": "^0.5.2",
"@agoric/vow": "^0.1.0",
"@agoric/zoe": "^0.26.2",
"@endo/base64": "^1.0.8",
Expand Down
256 changes: 181 additions & 75 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { AmountMath, AmountShape, BrandShape } from '@agoric/ertp';
import { assertAllDefined } from '@agoric/internal';
import { ChainAddressShape } from '@agoric/orchestration';
import { pickFacet } from '@agoric/vat-data';
import { VowShape } from '@agoric/vow';
import { makeError, q } from '@endo/errors';
import { E } from '@endo/far';
Expand All @@ -9,116 +11,220 @@ import { addressTools } from '../utils/address.js';

/**
* @import {HostInterface} from '@agoric/async-flow';
* @import {NatAmount} from '@agoric/ertp';
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
* @import {VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence, LogFn} from '../types.js';
* @import {StatusManager} from './status-manager.js';
* @import {TransactionFeedKit} from './transaction-feed.js';
*/

/**
* Expected interface from LiquidityPool
*
* @typedef {{
* lookupBalance(): NatAmount;
* borrowUnderlying(amount: Amount<"nat">): Promise<PaymentPKeywordRecord>;
* returnUnderlying(principalPayment: Payment<"nat">, feePayment: Payment<"nat">): Promise<void>
* }} AssetManagerFacet
*/

/**
* @typedef {{
* chainHub: ChainHub;
* log: LogFn;
* statusManager: StatusManager;
* vowTools: VowTools;
* }} AdvancerKitCaps
*/

/** type guards internal to the AdvancerKit */
const WatcherHandlersShape = {
depositHandler: M.interface('DepositHandlerI', {
onFulfilled: M.call(AmountShape, ChainAddressShape).returns(VowShape),
}),
transferHandler: M.interface('TransferHandlerI', {
// TODO confirm undefined, and not bigint (sequence)
onFulfilled: M.call(M.undefined(), {
amount: AmountShape,
destination: ChainAddressShape,
}).returns(M.undefined()),
onRejected: M.call(M.error(), {
amount: AmountShape,
destination: ChainAddressShape,
}).returns(M.undefined()),
}),
};

/**
* @param {Zone} zone
* @param {object} caps
* @param {ChainHub} caps.chainHub
* @param {LogFn} caps.log
* @param {StatusManager} caps.statusManager
* @param {VowTools} caps.vowTools
* @param {AdvancerKitCaps} caps
*/
export const prepareAdvancer = (
export const prepareAdvancerKit = (
zone,
{ chainHub, log, statusManager, vowTools: { watch } },
{ chainHub, log, statusManager, vowTools: { watch, when } },
) => {
assertAllDefined({ statusManager, watch });
assertAllDefined({
chainHub,
statusManager,
watch,
when,
});

const transferHandler = zone.exo(
'Fast USDC Advance Transfer Handler',
M.interface('TransferHandlerI', {
// TODO confirm undefined, and not bigint (sequence)
onFulfilled: M.call(M.undefined(), {
amount: M.bigint(),
destination: ChainAddressShape,
}).returns(M.undefined()),
onRejected: M.call(M.error(), {
amount: M.bigint(),
destination: ChainAddressShape,
}).returns(M.undefined()),
}),
return zone.exoClassKit(
'Fast USDC Advancer',
{
/**
* @param {undefined} result TODO confirm this is not a bigint (sequence)
* @param {{ destination: ChainAddress; amount: bigint; }} ctx
*/
onFulfilled(result, { destination, amount }) {
log(
'Advance transfer fulfilled',
q({ amount, destination, result }).toString(),
);
},
onRejected(error) {
// XXX retry logic?
// What do we do if we fail, should we keep a Status?
log('Advance transfer rejected', q(error).toString());
},
advancer: M.interface('AdvancerI', {
handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(
M.or(M.undefined(), VowShape),
),
}),
...WatcherHandlersShape,
},
);

return zone.exoClass(
'Fast USDC Advancer',
M.interface('AdvancerI', {
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
}),
/**
* @param {{
* assetManagerFacet: AssetManagerFacet;
* localDenom: Denom;
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
* poolAccount: ERef<HostInterface<OrchestrationAccount<{chainId: 'agoric';}>>>
* usdcBrand: Brand<'nat'>;
* }} config
*/
config => harden(config),
{
/** @param {CctpTxEvidence} evidence */
handleTransactionEvent(evidence) {
// TODO EventFeed will perform input validation checks.
const { recipientAddress } = evidence.aux;
const { EUD } = addressTools.getQueryParams(recipientAddress).params;
if (!EUD) {
statusManager.observe(evidence);
throw makeError(
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
);
}
advancer: {
/**
* Returns a Promise for a Vow in the happy path, or undefined
* when conditions are not met. Aims to perform a status update for
* every observed transaction.
*
* We do not expect any callers to depend on the settlement of
* `handleTransactionEvent` - errors caught are communicated to the
* `StatusManager` - so we don't need to concern ourselves with
* preserving the vow chain for callers.
*
* @param {CctpTxEvidence} evidence
*/
async handleTransactionEvent(evidence) {
await null;
try {
const { assetManagerFacet, usdcBrand } = this.state;
// XXX better way?
const poolAccount = await when(this.state.poolAccount);
const { recipientAddress } = evidence.aux;
const { EUD } =
addressTools.getQueryParams(recipientAddress).params;
if (!EUD) {
throw makeError(
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
);
}

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

/** @type {DenomAmount} */
const requestedAmount = harden({
denom: this.state.localDenom,
value: BigInt(evidence.tx.amount),
});
const requestedValue = BigInt(evidence.tx.amount);
const requestedAmount = AmountMath.make(usdcBrand, requestedValue);
const poolBalance = assetManagerFacet.lookupBalance();

// TODO #10391 ensure there's enough funds in poolAccount
if (!AmountMath.isGTE(poolBalance, requestedAmount)) {
log(
`Insufficient pool funds`,
`Requested ${q(requestedAmount)} but only have ${q(poolBalance)}`,
);
statusManager.observe(evidence);
return;
}

const transferV = E(this.state.poolAccount).transfer(
destination,
requestedAmount,
);
try {
// mark as Advanced since `transferV` initiates the advance
// will throw if we've already .skipped or .advanced this evidence
statusManager.advance(evidence);
} catch (e) {
// only anticipated error is `assertNotSeen`, so
// intercept the catch so we don't call .skip which
// also performs this check
log('Advancer error:', q(e).toString());
return;
}

// mark as Advanced since `transferV` initiates the advance
statusManager.advance(evidence);
try {
// should LiquidityPool return a vow here?
const { USDC: advancePmtP } =
await assetManagerFacet.borrowUnderlying(requestedAmount);

return watch(transferV, transferHandler, {
destination,
amount: requestedAmount.value,
});
// do we actually need to await here?
const advancePmt = await advancePmtP;
const depositV = E(poolAccount).deposit(advancePmt);
return watch(depositV, this.facets.depositHandler, destination);
} catch (e) {
// TODO how should we think about failure here?
log('Ruh roh', q(e).toString());
throw e;
}
} catch (e) {
log('Advancer error:', q(e).toString());
statusManager.observe(evidence);
}
},
},
depositHandler: {
/**
* @param {NatAmount} amount amount returned from deposit
* @param {ChainAddress} destination
*/
onFulfilled(amount, destination) {
const { localDenom, poolAccount } = this.state;
const transferV = E(poolAccount).transfer(
destination,
/** @type {DenomAmount} */ ({
denom: localDenom,
value: amount.value,
}),
);
return watch(transferV, this.facets.transferHandler, {
destination,
amount,
});
},
// xxx return payment on rejected
},
transferHandler: {
/**
* @param {undefined} result TODO confirm this is not a bigint (sequence)
* @param {{ destination: ChainAddress; amount: NatAmount; }} ctx
*/
onFulfilled(result, { destination, amount }) {
// TODO vstorage update?
log(
'Advance transfer fulfilled',
q({ amount, destination, result }).toString(),
);
},
onRejected(error) {
// XXX retry logic?
// What do we do if we fail, should we keep a Status?
log('Advance transfer rejected', q(error).toString());
},
},
},
{
stateShape: harden({
assetManagerFacet: M.remotable(),
localDenom: M.string(),
poolAccount: M.remotable(),
poolAccount: M.or(VowShape, M.remotable()),
usdcBrand: BrandShape,
}),
},
);
};
harden(prepareAdvancerKit);

/**
* @param {Zone} zone
* @param {AdvancerKitCaps} caps
*/
export const prepareAdvancer = (zone, caps) => {
const makeAdvancerKit = prepareAdvancerKit(zone, caps);
return pickFacet(makeAdvancerKit, 'advancer');
};
harden(prepareAdvancer);
6 changes: 1 addition & 5 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
// Connect evidence stream to advancer
void observeIteration(subscribeEach(feedKit.public.getEvidenceStream()), {
updateState(evidence) {
try {
advancer.handleTransactionEvent(evidence);
} catch (err) {
trace('🚨 Error handling transaction event', err);
}
void advancer.handleTransactionEvent(evidence);
},
});
const makeLiquidityPoolKit = prepareLiquidityPoolKit(
Expand Down
Loading

0 comments on commit 37d455f

Please sign in to comment.