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: advancer integrates with LP and contract #10518

Merged
merged 4 commits into from
Nov 20, 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
121 changes: 68 additions & 53 deletions packages/fast-usdc/src/exos/advancer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AmountMath, AmountShape, PaymentShape } from '@agoric/ertp';
import { assertAllDefined } from '@agoric/internal';
import { AmountMath, AmountShape } from '@agoric/ertp';
import { assertAllDefined, makeTracer } from '@agoric/internal';
import { ChainAddressShape } from '@agoric/orchestration';
import { pickFacet } from '@agoric/vat-data';
import { VowShape } from '@agoric/vow';
Expand All @@ -15,31 +15,25 @@ const { isGTE } = AmountMath;
/**
* @import {HostInterface} from '@agoric/async-flow';
* @import {NatAmount} from '@agoric/ertp';
* @import {ChainAddress, ChainHub, Denom, DenomAmount, OrchestrationAccount} from '@agoric/orchestration';
* @import {ChainAddress, ChainHub, Denom, OrchestrationAccount} from '@agoric/orchestration';
* @import {ZoeTools} from '@agoric/orchestration/src/utils/zoe-tools.js';
* @import {VowTools} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {CctpTxEvidence, FeeConfig, LogFn} from '../types.js';
* @import {StatusManager} from './status-manager.js';
*/

/**
* Expected interface from LiquidityPool
*
* @typedef {{
* lookupBalance(): NatAmount;
* borrow(amount: Amount<"nat">): Promise<Payment<"nat">>;
* repay(payments: PaymentKeywordRecord): Promise<void>
* }} AssetManagerFacet
* @import {LiquidityPoolKit} from './liquidity-pool.js';
*/

/**
* @typedef {{
* chainHub: ChainHub;
* feeConfig: FeeConfig;
* log: LogFn;
* localTransfer: ZoeTools['localTransfer'];
* log?: LogFn;
* statusManager: StatusManager;
* usdc: { brand: Brand<'nat'>; denom: Denom; };
* vowTools: VowTools;
* zcf: ZCF;
* }} AdvancerKitPowers
*/

Expand All @@ -49,13 +43,15 @@ const AdvancerKitI = harden({
handleTransactionEvent: M.callWhen(CctpTxEvidenceShape).returns(),
}),
depositHandler: M.interface('DepositHandlerI', {
onFulfilled: M.call(AmountShape, {
onFulfilled: M.call(M.undefined(), {
amount: AmountShape,
destination: ChainAddressShape,
payment: PaymentShape,
tmpSeat: M.remotable(),
}).returns(VowShape),
onRejected: M.call(M.error(), {
amount: AmountShape,
destination: ChainAddressShape,
payment: PaymentShape,
tmpSeat: M.remotable(),
}).returns(),
}),
transferHandler: M.interface('TransferHandlerI', {
Expand All @@ -77,7 +73,16 @@ const AdvancerKitI = harden({
*/
export const prepareAdvancerKit = (
zone,
{ chainHub, feeConfig, log, statusManager, usdc, vowTools: { watch, when } },
{
chainHub,
feeConfig,
localTransfer,
log = makeTracer('Advancer', true),
statusManager,
usdc,
vowTools: { watch, when },
zcf,
} = /** @type {AdvancerKitPowers} */ ({}),
Copy link
Member

Choose a reason for hiding this comment

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

I only see zcf.makeEmptySeatKit used. Narrow this to that 1 method?

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree this would be helpful towards limiting authority, but I don't think we can do this without making a new exo with something like prepareGuardedAttenuator or the like.

Would you still like to see this in this PR? I also suspect Settler will need the same - I can add a TODO if you're planning to tackle this there?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can do this without making a new exo ...

Right. Never mind.

) => {
assertAllDefined({
chainHub,
Expand All @@ -95,8 +100,8 @@ export const prepareAdvancerKit = (
AdvancerKitI,
/**
* @param {{
* assetManagerFacet: AssetManagerFacet;
* poolAccount: ERef<HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>>;
* borrowerFacet: LiquidityPoolKit['borrower'];
* poolAccount: HostInterface<OrchestrationAccount<{chainId: 'agoric'}>>;
* }} config
*/
config => harden(config),
Expand All @@ -115,8 +120,7 @@ export const prepareAdvancerKit = (
async handleTransactionEvent(evidence) {
await null;
try {
// TODO poolAccount might be a vow we need to unwrap
const { assetManagerFacet, poolAccount } = this.state;
const { borrowerFacet, poolAccount } = this.state;
const { recipientAddress } = evidence.aux;
const { EUD } = addressTools.getQueryParams(
recipientAddress,
Expand All @@ -129,14 +133,12 @@ export const prepareAdvancerKit = (
const advanceAmount = feeTools.calculateAdvance(requestedAmount);

// TODO: consider skipping and using `borrow()`s internal balance check
const poolBalance = assetManagerFacet.lookupBalance();
const poolBalance = borrowerFacet.getBalance();
if (!isGTE(poolBalance, requestedAmount)) {
log(
`Insufficient pool funds`,
`Requested ${q(advanceAmount)} but only have ${q(poolBalance)}`,
);
Comment on lines 138 to 140
Copy link
Member

Choose a reason for hiding this comment

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

note: I wonder if this makes use of the ses console as it should.

There's nothing to censor here. But "a more powerful distributed causal console" could be really handy.

Not for this PR, but I wonder when.

// report `requestedAmount`, not `advancedAmount`... do we need to
// communicate net to `StatusManger` in case fees change in between?
statusManager.observe(evidence);
Comment on lines -138 to -139
Copy link
Member

Choose a reason for hiding this comment

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

That's an interesting idea: compute the split at advancement time.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, something to keep in our back packet. When .settle is called, it could return the split amounts. But it's still not entirely clear to me if we should wait to .settle() until the payments have actually returned back to the LP, or settling on intent is acceptable.

return;
}
Expand All @@ -152,19 +154,29 @@ export const prepareAdvancerKit = (
return;
}

const { zcfSeat: tmpSeat } = zcf.makeEmptySeatKit();
const amountKWR = harden({ USDC: advanceAmount });
try {
const payment = await assetManagerFacet.borrow(advanceAmount);
const depositV = E(poolAccount).deposit(payment);
void watch(depositV, this.facets.depositHandler, {
destination,
payment,
});
borrowerFacet.borrow(tmpSeat, amountKWR);
} 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
// We do not expect this to fail since there are no turn boundaries
// between .getBalance() and .borrow().
// We catch to report outside of the normal error flow since this is
// not expected.
log('🚨 advance borrow failed', q(e).toString());
}

const depositV = localTransfer(
tmpSeat,
// @ts-expect-error LocalAccountMethods vs OrchestrationAccount
poolAccount,
amountKWR,
);
void watch(depositV, this.facets.depositHandler, {
amount: advanceAmount,
destination,
tmpSeat,
});
} catch (e) {
log('Advancer error:', q(e).toString());
statusManager.observe(evidence);
Expand All @@ -173,31 +185,33 @@ export const prepareAdvancerKit = (
},
depositHandler: {
/**
* @param {NatAmount} amount amount returned from deposit
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
* @param {undefined} result
* @param {{ amount: Amount<'nat'>; destination: ChainAddress; tmpSeat: ZCFSeat }} ctx
*/
onFulfilled(amount, { destination }) {
onFulfilled(result, { amount, destination }) {
const { poolAccount } = this.state;
const transferV = E(poolAccount).transfer(
destination,
/** @type {DenomAmount} */ ({
denom: usdc.denom,
value: amount.value,
}),
);
const transferV = E(poolAccount).transfer(destination, {
denom: usdc.denom,
value: amount.value,
});
return watch(transferV, this.facets.transferHandler, {
destination,
amount,
});
},
/**
* @param {Error} error
* @param {{ destination: ChainAddress; payment: Payment<'nat'> }} ctx
* @param {{ amount: Amount<'nat'>; destination: ChainAddress; tmpSeat: ZCFSeat }} ctx
*/
onRejected(error, { payment }) {
// TODO return live payment from ctx to LP
onRejected(error, { tmpSeat }) {
// TODO return seat allocation from ctx to LP?
log('🚨 advance deposit failed', q(error).toString());
log('TODO live payment to return to LP', q(payment).toString());
// TODO #10510 (comprehensive error testing) determine
// course of action here
Comment on lines +207 to +209
Copy link
Member

Choose a reason for hiding this comment

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

this stuff is why #10510 looks to me like "the other 80% of the work".

The local deposit would only fail due to things in our control at design-time (i.e. bugs), but the remote transfer can fail for runtime reasons (timeout).

Copy link
Member Author

Choose a reason for hiding this comment

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

The local deposit would only fail due to things in our control at design-time (i.e. bugs), but the remote transfer can fail for runtime reasons (timeout).

The is well-put - I should include this verbiage as a code comment.

Agree we can almost guarantee .deposit() will never fail, so I decided not to add more noise to this PR with a repay() method on the borrower facet. Also agree .transfer() failing is the more important failure path to be concerned with.

log(
'TODO live payment on seat to return to LP',
q(tmpSeat).toString(),
);
},
},
transferHandler: {
Expand All @@ -206,23 +220,24 @@ export const prepareAdvancerKit = (
* @param {{ destination: ChainAddress; amount: NatAmount; }} ctx
*/
onFulfilled(result, { destination, amount }) {
// TODO vstorage update?
// TODO vstorage update? We don't currently have a status for
// Advanced + transferV settled
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?
// TODO #10510 (comprehensive error testing) determine
// course of action here. This might fail due to timeout.
log('Advance transfer rejected', q(error).toString());
},
},
},
{
stateShape: harden({
assetManagerFacet: M.remotable(),
poolAccount: M.or(VowShape, M.remotable()),
borrowerFacet: M.remotable(),
poolAccount: M.remotable(),
}),
},
);
Expand Down
64 changes: 44 additions & 20 deletions packages/fast-usdc/src/fast-usdc.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,27 @@ import {
} from '@agoric/orchestration';
import { provideSingleton } from '@agoric/zoe/src/contractSupport/durability.js';
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
import { makeZoeTools } from '@agoric/orchestration/src/utils/zoe-tools.js';
import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import { E } from '@endo/far';
import { M, objectMap } from '@endo/patterns';
import { depositToSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js';
import { prepareAdvancer } from './exos/advancer.js';
import { prepareLiquidityPoolKit } from './exos/liquidity-pool.js';
import { prepareSettler } from './exos/settler.js';
import { prepareStatusManager } from './exos/status-manager.js';
import { prepareTransactionFeedKit } from './exos/transaction-feed.js';
import { defineInertInvitation } from './utils/zoe.js';
import { FastUSDCTermsShape, FeeConfigShape } from './type-guards.js';
import * as flows from './fast-usdc.flows.js';

const trace = makeTracer('FastUsdc');

/**
* @import {Denom} from '@agoric/orchestration';
* @import {HostInterface} from '@agoric/async-flow';
* @import {OrchestrationAccount} from '@agoric/orchestration';
* @import {OrchestrationPowers, OrchestrationTools} from '@agoric/orchestration/src/utils/start-helper.js';
* @import {Vow} from '@agoric/vow';
* @import {Zone} from '@agoric/zone';
* @import {OperatorKit} from './exos/operator-kit.js';
* @import {CctpTxEvidence, FeeConfig} from './types.js';
Expand Down Expand Up @@ -73,35 +78,22 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
);
const statusManager = prepareStatusManager(zone);
const makeSettler = prepareSettler(zone, { statusManager });
const { chainHub, vowTools } = tools;
const { chainHub, orchestrateAll, vowTools } = tools;
const { localTransfer } = makeZoeTools(zcf, vowTools);
const makeAdvancer = prepareAdvancer(zone, {
chainHub,
feeConfig,
log: trace,
localTransfer,
usdc: harden({
brand: terms.brands.USDC,
denom: terms.usdcDenom,
}),
statusManager,
vowTools,
zcf,
});
const makeFeedKit = prepareTransactionFeedKit(zone, zcf);
assertAllDefined({ makeFeedKit, makeAdvancer, makeSettler, statusManager });
const feedKit = makeFeedKit();
const advancer = makeAdvancer(
// @ts-expect-error FIXME
{},
);
// Connect evidence stream to advancer
void observeIteration(subscribeEach(feedKit.public.getEvidenceSubscriber()), {
updateState(evidence) {
try {
void advancer.handleTransactionEvent(evidence);
} catch (err) {
trace('🚨 Error handling transaction event', err);
}
},
});
const makeLiquidityPoolKit = prepareLiquidityPoolKit(
zone,
zcf,
Expand All @@ -114,16 +106,19 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
'test of forcing evidence',
);

const { makeLocalAccount } = orchestrateAll(flows, {});

const creatorFacet = zone.exo('Fast USDC Creator', undefined, {
/** @type {(operatorId: string) => Promise<Invitation<OperatorKit>>} */
async makeOperatorInvitation(operatorId) {
// eslint-disable-next-line no-use-before-define
Copy link
Member

Choose a reason for hiding this comment

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

I didn't realize that #9361 hadn't landed. I'll try to get that in now.

return feedKit.creator.makeOperatorInvitation(operatorId);
},
/**
* @param {{ USDC: Amount<'nat'>}} amounts
*/
testBorrow(amounts) {
console.log('🚧🚧 UNTIL: borrow is integrated 🚧🚧', amounts);
console.log('🚧🚧 UNTIL: borrow is integrated (#10388) 🚧🚧', amounts);
const { zcfSeat: tmpAssetManagerSeat } = zcf.makeEmptySeatKit();
// eslint-disable-next-line no-use-before-define
poolKit.borrower.borrow(tmpAssetManagerSeat, amounts);
Expand All @@ -136,7 +131,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
* @returns {Promise<AmountKeywordRecord>}
*/
async testRepay(amounts, payments) {
console.log('🚧🚧 UNTIL: repay is integrated 🚧🚧', amounts);
console.log('🚧🚧 UNTIL: repay is integrated (#10388) 🚧🚧', amounts);
const { zcfSeat: tmpAssetManagerSeat } = zcf.makeEmptySeatKit();
await depositToSeat(
zcf,
Expand Down Expand Up @@ -164,6 +159,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
* @param {CctpTxEvidence} evidence
*/
makeTestPushInvitation(evidence) {
// eslint-disable-next-line no-use-before-define
void advancer.handleTransactionEvent(evidence);
return makeTestInvitation();
},
Expand Down Expand Up @@ -207,6 +203,34 @@ export const contract = async (zcf, privateArgs, zone, tools) => {
makeLiquidityPoolKit(shareMint, privateArgs.storageNode),
);

const feedKit = zone.makeOnce('Feed Kit', () => makeFeedKit());

const poolAccountV =
// cast to HostInterface
/** @type { Vow<HostInterface<OrchestrationAccount<{chainId: 'agoric';}>>>} */ (
/** @type {unknown}*/ (
zone.makeOnce('Pool Local Orch Account', () => makeLocalAccount())
)
);
const poolAccount = await vowTools.when(poolAccountV);
0xpatrickdev marked this conversation as resolved.
Show resolved Hide resolved

const advancer = zone.makeOnce('Advancer', () =>
makeAdvancer({
borrowerFacet: poolKit.borrower,
poolAccount,
}),
);
// Connect evidence stream to advancer
void observeIteration(subscribeEach(feedKit.public.getEvidenceSubscriber()), {
updateState(evidence) {
try {
void advancer.handleTransactionEvent(evidence);
} catch (err) {
trace('🚨 Error handling transaction event', err);
}
},
});

return harden({ creatorFacet, publicFacet });
};
harden(contract);
Expand Down
13 changes: 13 additions & 0 deletions packages/fast-usdc/src/fast-usdc.flows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @import {Orchestrator, OrchestrationFlow} from '@agoric/orchestration';
*/

/**
* @satisfies {OrchestrationFlow}
* @param {Orchestrator} orch
*/
export const makeLocalAccount = async orch => {
Copy link
Member

Choose a reason for hiding this comment

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

is this our first flow? I thought we would be able to ship this without async-flow.

This is fine since it runs once but I'm curious what it would take to do this without a flow.

Copy link
Member Author

Choose a reason for hiding this comment

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

curious what it would take to do this without a flow

Less complicated than a CosmosOrchAccount Maybe something like:

const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit(
  zone,
  {
    makeRecorderKit,
    zcf,
    timerService,
    vowTools,
    chainHub,
    localchain,
    zoeTools,
  },
);

const account = await when(E(localchain).makeAccount());
const address = await when(E(account).getAddress());

const { holder } = makeLocalOrchestrationAccountKit({
  account,
  address: harden({
    value: address,
    encoding: 'bech32',
    chainId: localChainInfo.chainId,
  }),
  // FIXME storage path https://github.com/Agoric/agoric-sdk/issues/9066
  storageNode: childNode,
});

Something we can revisit . Also - I'm reminded that #9066 might need some love as we are getting to launch this FUSDC.

const agoricChain = await orch.getChain('agoric');
return agoricChain.makeAccount();
};
harden(makeLocalAccount);
Loading
Loading