Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fusdc advancer #10420

Merged
merged 3 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
272 changes: 191 additions & 81 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
@@ -1,124 +1,234 @@
import { AmountMath, AmountShape, PaymentShape } from '@agoric/ertp';
import { assertAllDefined } from '@agoric/internal';
import { ChainAddressShape } from '@agoric/orchestration';
import { pickFacet } from '@agoric/vat-data';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of scope, but this helper should move eventually. It's about Exos, not vat data.

We could move it down to @agoric/internal but that is meant to not be consumed externally and this is valuable to other contract authors.

I wonder if it belongs in Endo.

import { VowShape } from '@agoric/vow';
import { makeError, q } from '@endo/errors';
import { q } from '@endo/errors';
import { E } from '@endo/far';
import { M } from '@endo/patterns';
import { CctpTxEvidenceShape } from '../typeGuards.js';
import { CctpTxEvidenceShape, EudParamShape } 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll assume this @dckc agrees

*
* @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
turadg marked this conversation as resolved.
Show resolved Hide resolved
* `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,
EudParamShape,
);

// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we'll want to log this even to vstorage. But let's come back to it after getting more clarity with product on all the error handling and what the end user needs to see from it. We have that on the schedule.

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()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO pick one

Copy link
Member Author

@0xpatrickdev 0xpatrickdev Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it'll depend on:

  • how we create the LCA (I believe zone.makeOnce() will return a Vow)
  • what incarnation we're in (first time it's a Vow, subsequently, a 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);
5 changes: 2 additions & 3 deletions packages/fast-usdc/src/exos/settler.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ export const prepareSettler = (zone, { statusManager }) => {
return;
}

const { params } = addressTools.getQueryParams(tx.receiver);
// TODO - what's the schema address parameter schema for FUSDC?
if (!params?.EUD) {
const { EUD } = addressTools.getQueryParams(tx.receiver);
if (!EUD) {
// only interested in receivers with EUD parameter
return;
}
Expand Down
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) {
turadg marked this conversation as resolved.
Show resolved Hide resolved
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
Loading