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 15, 2024
1 parent d1d7829 commit 4cd2f3f
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 200 deletions.
1 change: 1 addition & 0 deletions packages/fast-usdc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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
271 changes: 192 additions & 79 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
@@ -1,124 +1,237 @@
import { AmountMath, AmountShape, PaymentShape } 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';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../typeGuards.js';
import { addressTools } from '../utils/address.js';

const { isGTE } = AmountMath;

/**
* @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;
* borrow(amount: Amount<"nat">): Promise<Payment<"nat">>;
* repay(payments: PaymentKeywordRecord): Promise<void>
* }} AssetManagerFacet
*/

/**
* @typedef {{
* chainHub: ChainHub;
* log: LogFn;
* statusManager: StatusManager;
* usdc: { brand: Brand<'nat'>; denom: Denom; };
* vowTools: VowTools;
* }} AdvancerKitPowers
*/

/** type guards internal to the AdvancerKit */
const AdvancerKitI = harden({
advancer: M.interface('AdvancerI', {
handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(),
}),
depositHandler: M.interface('DepositHandlerI', {
onFulfilled: M.call(AmountShape, {
destination: ChainAddressShape,
payment: PaymentShape,
}).returns(VowShape),
onRejected: M.call(M.error(), {
destination: ChainAddressShape,
payment: PaymentShape,
}).returns(),
}),
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 {AdvancerKitPowers} caps
*/
export const prepareAdvancer = (
export const prepareAdvancerKit = (
zone,
{ chainHub, log, statusManager, vowTools: { watch } },
{ chainHub, log, statusManager, usdc, 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()),
}),
{
/**
* @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());
},
},
);
/** @param {bigint} value */
const toAmount = value => AmountMath.make(usdc.brand, value);

return zone.exoClass(
return zone.exoClassKit(
'Fast USDC Advancer',
M.interface('AdvancerI', {
handleTransactionEvent: M.call(CctpTxEvidenceShape).returns(VowShape),
}),
AdvancerKitI,
/**
* @param {{
* localDenom: Denom;
* poolAccount: HostInterface<OrchestrationAccount<{ chainId: 'agoric' }>>;
* assetManagerFacet: AssetManagerFacet;
* poolAccount: ERef<HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>>;
* }} 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)}`,
);
}

// TODO #10391 this can throw, and should make a status update in the catch
const destination = chainHub.makeChainAddress(EUD);

/** @type {DenomAmount} */
const requestedAmount = harden({
denom: this.state.localDenom,
value: BigInt(evidence.tx.amount),
});
advancer: {
/**
* Must 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 {
// TODO poolAccount might be a vow we need to unwrap
const { assetManagerFacet, poolAccount } = this.state;
const { recipientAddress } = evidence.aux;
const { EUD } =
addressTools.getQueryParams(recipientAddress).params;
if (!EUD) {
throw makeError(
`recipientAddress does not contain EUD param: ${q(recipientAddress)}`,
);
}

// TODO #10391 ensure there's enough funds in poolAccount
// this will throw if the bech32 prefix is not found, but is handled by the catch
const destination = chainHub.makeChainAddress(EUD);
const requestedAmount = toAmount(evidence.tx.amount);

const transferV = E(this.state.poolAccount).transfer(
destination,
requestedAmount,
);
// TODO: consider skipping and using `borrow()`s internal balance check
const poolBalance = assetManagerFacet.lookupBalance();
if (!isGTE(poolBalance, requestedAmount)) {
log(
`Insufficient pool funds`,
`Requested ${q(requestedAmount)} but only have ${q(poolBalance)}`,
);
statusManager.observe(evidence);
return;
}

// mark as Advanced since `transferV` initiates the advance
statusManager.advance(evidence);
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;
}

return watch(transferV, transferHandler, {
destination,
amount: requestedAmount.value,
});
try {
const payment = await assetManagerFacet.borrow(requestedAmount);
const depositV = E(poolAccount).deposit(payment);
void watch(depositV, this.facets.depositHandler, {
destination,
payment,
});
} catch (e) {
// `.borrow()` might fail if the balance changes since we
// requested it. TODO - how to handle this? change ADVANCED -> OBSERVED?
// Note: `depositHandler` handles the `.deposit()` failure
log('🚨 advance borrow failed', q(e).toString());
}
} catch (e) {
log('Advancer error:', q(e).toString());
statusManager.observe(evidence);
}
},
},
depositHandler: {
/**
* @param {NatAmount} amount amount returned from deposit
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
*/
onFulfilled(amount, { destination }) {
const { poolAccount } = this.state;
const transferV = E(poolAccount).transfer(
destination,
/** @type {DenomAmount} */ ({
denom: usdc.denom,
value: amount.value,
}),
);
return watch(transferV, this.facets.transferHandler, {
destination,
amount,
});
},
/**
* @param {Error} error
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
*/
onRejected(error, { payment }) {
// TODO return live payment from ctx to LP
log('🚨 advance deposit failed', q(error).toString());
log('TODO live payment to return to LP', q(payment).toString());
},
},
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({
localDenom: M.string(),
poolAccount: M.remotable(),
assetManagerFacet: M.remotable(),
poolAccount: M.or(VowShape, M.remotable()),
}),
},
);
};
harden(prepareAdvancerKit);

/**
* @param {Zone} zone
* @param {AdvancerKitPowers} caps
*/
export const prepareAdvancer = (zone, caps) => {
const makeAdvancerKit = prepareAdvancerKit(zone, caps);
return pickFacet(makeAdvancerKit, 'advancer');
};
harden(prepareAdvancer);
12 changes: 10 additions & 2 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { defineInertInvitation } from './utils/zoe.js';
const trace = makeTracer('FastUsdc');

/**
* @import {Denom} from '@agoric/orchestration';
* @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js';
* @import {Zone} from '@agoric/zone';
* @import {OperatorKit} from './exos/operator-kit.js';
Expand All @@ -26,13 +27,15 @@ const trace = makeTracer('FastUsdc');
* @typedef {{
* poolFee: Amount<'nat'>;
* contractFee: Amount<'nat'>;
* usdcDenom: Denom;
* }} FastUsdcTerms
*/
const NatAmountShape = { brand: BrandShape, value: M.nat() };
export const meta = {
customTermsShape: {
contractFee: NatAmountShape,
poolFee: NatAmountShape,
usdcDenom: M.string(),
},
};
harden(meta);
Expand All @@ -49,6 +52,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
assert(tools, 'no tools');
const terms = zcf.getTerms();
assert('USDC' in terms.brands, 'no USDC brand');
assert('usdcDenom' in terms, 'no usdcDenom');

const { makeRecorderKit } = prepareRecorderKitMakers(
zone.mapStore('vstorage'),
Expand All @@ -61,6 +65,10 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
const makeAdvancer = prepareAdvancer(zone, {
chainHub,
log: trace,
usdc: harden({
brand: terms.brands.USDC,
denom: terms.usdcDenom,
}),
statusManager,
vowTools,
});
Expand All @@ -75,7 +83,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
void observeIteration(subscribeEach(feedKit.public.getEvidenceSubscriber()), {
updateState(evidence) {
try {
advancer.handleTransactionEvent(evidence);
void advancer.handleTransactionEvent(evidence);
} catch (err) {
trace('🚨 Error handling transaction event', err);
}
Expand Down Expand Up @@ -117,7 +125,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
* @param {CctpTxEvidence} evidence
*/
makeTestPushInvitation(evidence) {
advancer.handleTransactionEvent(evidence);
void advancer.handleTransactionEvent(evidence);
return makeTestInvitation();
},
makeDepositInvitation() {
Expand Down
Loading

0 comments on commit 4cd2f3f

Please sign in to comment.