diff --git a/packages/fast-usdc/src/exos/advancer.js b/packages/fast-usdc/src/exos/advancer.js index e772f2ecb4a..ceb4bc09ef8 100644 --- a/packages/fast-usdc/src/exos/advancer.js +++ b/packages/fast-usdc/src/exos/advancer.js @@ -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'; @@ -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>; - * repay(payments: PaymentKeywordRecord): Promise - * }} 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 */ @@ -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', { @@ -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} */ ({}), ) => { assertAllDefined({ chainHub, @@ -95,8 +100,8 @@ export const prepareAdvancerKit = ( AdvancerKitI, /** * @param {{ - * assetManagerFacet: AssetManagerFacet; - * poolAccount: ERef>>; + * borrowerFacet: LiquidityPoolKit['borrower']; + * poolAccount: HostInterface>; * }} config */ config => harden(config), @@ -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, @@ -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)}`, ); - // report `requestedAmount`, not `advancedAmount`... do we need to - // communicate net to `StatusManger` in case fees change in between? statusManager.observe(evidence); return; } @@ -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); @@ -173,18 +185,15 @@ 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, @@ -192,12 +201,17 @@ export const prepareAdvancerKit = ( }, /** * @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 + log( + 'TODO live payment on seat to return to LP', + q(tmpSeat).toString(), + ); }, }, transferHandler: { @@ -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(), }), }, ); diff --git a/packages/fast-usdc/src/fast-usdc.contract.js b/packages/fast-usdc/src/fast-usdc.contract.js index 80b6602e35c..5e5a0466f5a 100644 --- a/packages/fast-usdc/src/fast-usdc.contract.js +++ b/packages/fast-usdc/src/fast-usdc.contract.js @@ -11,9 +11,10 @@ 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'; @@ -21,12 +22,16 @@ 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'; @@ -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, @@ -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>} */ async makeOperatorInvitation(operatorId) { + // eslint-disable-next-line no-use-before-define 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); @@ -136,7 +131,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { * @returns {Promise} */ 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, @@ -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(); }, @@ -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>>} */ ( + /** @type {unknown}*/ ( + zone.makeOnce('Pool Local Orch Account', () => makeLocalAccount()) + ) + ); + const poolAccount = await vowTools.when(poolAccountV); + + 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); diff --git a/packages/fast-usdc/src/fast-usdc.flows.js b/packages/fast-usdc/src/fast-usdc.flows.js new file mode 100644 index 00000000000..9f330a4c905 --- /dev/null +++ b/packages/fast-usdc/src/fast-usdc.flows.js @@ -0,0 +1,13 @@ +/** + * @import {Orchestrator, OrchestrationFlow} from '@agoric/orchestration'; + */ + +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + */ +export const makeLocalAccount = async orch => { + const agoricChain = await orch.getChain('agoric'); + return agoricChain.makeAccount(); +}; +harden(makeLocalAccount); diff --git a/packages/fast-usdc/test/exos/advancer.test.ts b/packages/fast-usdc/test/exos/advancer.test.ts index dc23436af50..fb21deabf86 100644 --- a/packages/fast-usdc/test/exos/advancer.test.ts +++ b/packages/fast-usdc/test/exos/advancer.test.ts @@ -7,6 +7,7 @@ import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; import { Far } from '@endo/pass-style'; import { makePromiseKit } from '@endo/promise-kit'; import type { NatAmount } from '@agoric/ertp'; +import { type ZoeTools } from '@agoric/orchestration/src/utils/zoe-tools.js'; import { PendingTxStatus } from '../../src/constants.js'; import { prepareAdvancer } from '../../src/exos/advancer.js'; import { prepareStatusManager } from '../../src/exos/status-manager.js'; @@ -50,10 +51,27 @@ const createTestExtensions = (t, common: CommonSetup) => { usdc, }); + const mockZCF = Far('MockZCF', { + makeEmptySeatKit: () => ({ zcfSeat: Far('MockZCFSeat', {}) }), + }); + + const localTransferVK = vowTools.makeVowKit(); + const resolveLocalTransferV = () => { + // pretend funds move from tmpSeat to poolAccount + localTransferVK.resolver.resolve(); + }; + const mockZoeTools = Far('MockZoeTools', { + localTransfer(...args: Parameters) { + console.log('ZoeTools.localTransfer called with', args); + return localTransferVK.vow; + }, + }); + const feeConfig = makeTestFeeConfig(usdc); const makeAdvancer = prepareAdvancer(rootZone.subZone('advancer'), { chainHub, feeConfig, + localTransfer: mockZoeTools.localTransfer, log, statusManager, usdc: harden({ @@ -61,6 +79,8 @@ const createTestExtensions = (t, common: CommonSetup) => { denom: LOCAL_DENOM, }), vowTools, + // @ts-expect-error mocked zcf + zcf: mockZCF, }); /** pretend we have 1M USDC in pool deposits */ @@ -73,19 +93,19 @@ const createTestExtensions = (t, common: CommonSetup) => { mockPoolBalance = usdc.make(value); }; - const borrowUnderlyingPK = makePromiseKit>(); - const resolveBorrowUnderlyingP = async (amount: Amount<'nat'>) => { - const pmt = await pourPayment(amount); - return borrowUnderlyingPK.resolve(pmt); + const borrowUnderlyingPK = makePromiseKit(); + const resolveBorrowUnderlyingP = () => { + // pretend funds are allocated to tmpSeat provided to borrow + return borrowUnderlyingPK.resolve(); }; const rejectBorrowUnderlyingP = () => borrowUnderlyingPK.reject('Mock unable to borrow.'); const advancer = makeAdvancer({ - assetManagerFacet: Far('AssetManager', { - lookupBalance: () => mockPoolBalance, - borrow: (amount: NatAmount) => { - t.log('borrowUnderlying called with', amount); + borrowerFacet: Far('LiquidityPool Borrow Facet', { + getBalance: () => mockPoolBalance, + borrow: (seat: ZCFSeat, amounts: { USDC: NatAmount }) => { + t.log('borrowUnderlying called with', amounts); return borrowUnderlyingPK.promise; }, repay: () => Promise.resolve(), @@ -106,6 +126,7 @@ const createTestExtensions = (t, common: CommonSetup) => { setMockPoolBalance, resolveBorrowUnderlyingP, rejectBorrowUnderlyingP, + resolveLocalTransferV, }, services: { advancer, @@ -134,7 +155,11 @@ test('updates status to ADVANCED in happy path', async t => { extensions: { services: { advancer, statusManager }, helpers: { inspectLogs }, - mocks: { mockPoolAccount, resolveBorrowUnderlyingP }, + mocks: { + mockPoolAccount, + resolveBorrowUnderlyingP, + resolveLocalTransferV, + }, }, brands: { usdc }, } = t.context; @@ -142,7 +167,8 @@ test('updates status to ADVANCED in happy path', async t => { const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); const handleTxP = advancer.handleTransactionEvent(mockEvidence); - await resolveBorrowUnderlyingP(usdc.make(mockEvidence.tx.amount)); + resolveBorrowUnderlyingP(); + resolveLocalTransferV(); await eventLoopIteration(); mockPoolAccount.transferVResolver.resolve(); @@ -162,7 +188,7 @@ test('updates status to ADVANCED in happy path', async t => { t.deepEqual(inspectLogs(0), [ 'Advance transfer fulfilled', - '{"amount":{"brand":"[Alleged: USDC brand]","value":"[150000000n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', + '{"amount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', ]); }); @@ -194,7 +220,7 @@ test('updates status to OBSERVED on insufficient pool funds', async t => { t.deepEqual(inspectLogs(0), [ 'Insufficient pool funds', - 'Requested {"brand":"[Alleged: USDC brand]","value":"[199999899n]"} but only have {"brand":"[Alleged: USDC brand]","value":"[1n]"}', + 'Requested {"brand":"[Alleged: USDC brand]","value":"[294999999n]"} but only have {"brand":"[Alleged: USDC brand]","value":"[1n]"}', ]); }); @@ -211,8 +237,8 @@ test('updates status to OBSERVED if balance query fails', async t => { // make a new advancer that intentionally throws const advancer = makeAdvancer({ // @ts-expect-error mock - assetManagerFacet: Far('AssetManager', { - lookupBalance: () => { + borrowerFacet: Far('LiquidityPool Borrow Facet', { + getBalance: () => { throw new Error('lookupBalance failed'); }, }), @@ -267,13 +293,17 @@ test('updates status to OBSERVED if makeChainAddress fails', async t => { ]); }); -// TODO, this failure should be handled differently +// TODO #10510 this failure should be handled differently test('does not update status on failed transfer', async t => { const { extensions: { services: { advancer, statusManager }, helpers: { inspectLogs }, - mocks: { mockPoolAccount, resolveBorrowUnderlyingP }, + mocks: { + mockPoolAccount, + resolveBorrowUnderlyingP, + resolveLocalTransferV, + }, }, brands: { usdc }, } = t.context; @@ -281,7 +311,8 @@ test('does not update status on failed transfer', async t => { const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_DYDX(); const handleTxP = advancer.handleTransactionEvent(mockEvidence); - await resolveBorrowUnderlyingP(usdc.make(mockEvidence.tx.amount)); + resolveBorrowUnderlyingP(); + resolveLocalTransferV(); mockPoolAccount.transferVResolver.reject(new Error('simulated error')); await handleTxP; @@ -340,23 +371,27 @@ test('will not advance same txHash:chainId evidence twice', async t => { extensions: { services: { advancer }, helpers: { inspectLogs }, - mocks: { mockPoolAccount, resolveBorrowUnderlyingP }, + mocks: { + mockPoolAccount, + resolveBorrowUnderlyingP, + resolveLocalTransferV, + }, }, - brands: { usdc }, } = t.context; const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO(); // First attempt const handleTxP = advancer.handleTransactionEvent(mockEvidence); - await resolveBorrowUnderlyingP(usdc.make(mockEvidence.tx.amount)); + resolveBorrowUnderlyingP(); + resolveLocalTransferV(); mockPoolAccount.transferVResolver.resolve(); await handleTxP; await eventLoopIteration(); t.deepEqual(inspectLogs(0), [ 'Advance transfer fulfilled', - '{"amount":{"brand":"[Alleged: USDC brand]","value":"[150000000n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', + '{"amount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', ]); // Second attempt @@ -367,3 +402,5 @@ test('will not advance same txHash:chainId evidence twice', async t => { '"[Error: Transaction already seen: \\"seenTx:[\\\\\\"0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702\\\\\\",1]\\"]"', ]); }); + +test.todo('zoeTools.localTransfer fails to deposit borrowed USDC to LOA'); diff --git a/packages/fast-usdc/test/fast-usdc.contract.test.ts b/packages/fast-usdc/test/fast-usdc.contract.test.ts index 427ca2cf9d2..e697b9118da 100644 --- a/packages/fast-usdc/test/fast-usdc.contract.test.ts +++ b/packages/fast-usdc/test/fast-usdc.contract.test.ts @@ -23,6 +23,7 @@ import { commonSetup } from './supports.js'; import type { FastUsdcTerms } from '../src/fast-usdc.contract.js'; import { makeFeeTools } from '../src/utils/fees.js'; import type { PoolMetrics } from '../src/types.js'; +import { addressTools } from '../src/utils/address.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); @@ -52,6 +53,14 @@ const startContract = async ( const { zoe, bundleAndInstall } = await setUpZoeForTest({ setJig: jig => { jig.chainHub.registerChain('osmosis', fetchedChainInfo.osmosis); + jig.chainHub.registerChain('agoric', fetchedChainInfo.agoric); + // TODO #10445 register noble<>agoric and noble<>osmosis instead + // for PFM routing. also will need to call `registerAsset` + jig.chainHub.registerConnection( + fetchedChainInfo.agoric.chainId, + fetchedChainInfo.osmosis.chainId, + fetchedChainInfo.agoric.connections['osmosis-1'], + ); }, }); const installation: Installation = @@ -69,26 +78,6 @@ const startContract = async ( return { ...startKit, zoe }; }; -// FIXME this makeTestPushInvitation forces evidence, which triggers advancing, -// which doesn't yet work -test.skip('advancing', async t => { - const common = await commonSetup(t); - - const { publicFacet, zoe } = await startContract(common); - - const e1 = await E(MockCctpTxEvidences.AGORIC_PLUS_DYDX)(); - - const inv = await E(publicFacet).makeTestPushInvitation(e1); - // the invitation maker itself pushes the evidence - - // the offer is still safe to make - const seat = await E(zoe).offer(inv); - t.is( - await E(seat).getOfferResult(), - 'inert; nothing should be expected from this offer', - ); -}); - test('oracle operators have closely-held rights to submit evidence of CCTP transactions', async t => { const common = await commonSetup(t); const { creatorFacet, zoe } = await startContract(common); @@ -551,3 +540,85 @@ test('baggage', async t => { const tree = inspectMapStore(contractBaggage); t.snapshot(tree, 'contract baggage after start'); }); + +test('advancing happy path', async t => { + const common = await commonSetup(t); + const { + brands: { usdc }, + commonPrivateArgs, + utils: { inspectLocalBridge, inspectBankBridge, transmitTransferAck }, + } = common; + + const { instance, publicFacet, zoe } = await startContract(common); + const terms = await E(zoe).getTerms(instance); + const { subscriber } = E.get( + E.get(E(publicFacet).getPublicTopics()).poolMetrics, + ); + const feeTools = makeFeeTools(commonPrivateArgs.feeConfig); + const { makeLP, purseOf } = makeLpTools(t, common, { + publicFacet, + subscriber, + terms, + zoe, + }); + + const evidence = await E(MockCctpTxEvidences.AGORIC_PLUS_OSMO)(); + + // seed pool with funds + const alice = makeLP('Alice', purseOf(evidence.tx.amount)); + await alice.deposit(evidence.tx.amount); + + // the invitation maker itself pushes the evidence + const inv = await E(publicFacet).makeTestPushInvitation(evidence); + const seat = await E(zoe).offer(inv); + t.is( + await E(seat).getOfferResult(), + 'inert; nothing should be expected from this offer', + ); + + // calculate advance net of fees + const expectedAdvance = feeTools.calculateAdvance( + usdc.make(evidence.tx.amount), + ); + t.log('Expecting to observe advance of', expectedAdvance); + + await eventLoopIteration(); // let Advancer do work + + // advance sent from PoolSeat to PoolAccount + t.deepEqual(inspectBankBridge().at(-1), { + amount: String(expectedAdvance.value), + denom: 'ibc/usdconagoric', + recipient: 'agoric1fakeLCAAddress', + type: 'VBANK_GIVE', + }); + + // ibc transfer sent over localChain bridge + const localBridgeMsg = inspectLocalBridge().at(-1); + const ibcTransferMsg = localBridgeMsg.messages[0]; + t.is(ibcTransferMsg['@type'], '/ibc.applications.transfer.v1.MsgTransfer'); + + const expectedReceiver = addressTools.getQueryParams( + evidence.aux.recipientAddress, + ).EUD; + t.is(ibcTransferMsg.receiver, expectedReceiver, 'sent to correct address'); + t.deepEqual(ibcTransferMsg.token, { + amount: String(expectedAdvance.value), + denom: 'ibc/usdconagoric', + }); + + // TODO #10445 expect PFM memo + t.is(ibcTransferMsg.memo, '', 'TODO expecting PFM memo'); + + // TODO #10445 expect routing through noble, not osmosis + t.is( + ibcTransferMsg.sourceChannel, + fetchedChainInfo.agoric.connections['osmosis-1'].transferChannel.channelId, + 'TODO expecting routing through Noble', + ); + + await transmitTransferAck(); + // Nothing we can check here, unless we want to inspect calls to `trace`. + // `test/exos/advancer.test.ts` covers calls to `log: LogFn` with mocks. + // This is still helpful to call, so we can observe "Advance transfer + // fulfilled" in the test output. +}); diff --git a/packages/fast-usdc/test/fixtures.ts b/packages/fast-usdc/test/fixtures.ts index fff1f942d96..0a520918906 100644 --- a/packages/fast-usdc/test/fixtures.ts +++ b/packages/fast-usdc/test/fixtures.ts @@ -43,7 +43,7 @@ export const MockCctpTxEvidences: Record< txHash: '0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', tx: { - amount: 200000000n, + amount: 300000000n, forwardingAddress: 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelktz', }, aux: { diff --git a/packages/fast-usdc/test/mocks.ts b/packages/fast-usdc/test/mocks.ts index 16db2a5d125..ad82a2665cf 100644 --- a/packages/fast-usdc/test/mocks.ts +++ b/packages/fast-usdc/test/mocks.ts @@ -69,6 +69,6 @@ export const makeTestFeeConfig = (usdc: Omit): FeeConfig => harden({ flat: usdc.make(1n), variableRate: makeRatio(2n, usdc.brand), - maxVariable: usdc.make(100n), + maxVariable: usdc.units(5), contractRate: makeRatio(20n, usdc.brand), }); diff --git a/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.md b/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.md index 8421aa9cc3c..382c6399d37 100644 --- a/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.md +++ b/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.md @@ -30,9 +30,22 @@ Generated by [AVA](https://avajs.dev). chainHub: { ChainHub_kindHandle: 'Alleged: kind', ChainHub_singleton: 'Alleged: ChainHub', - bech32PrefixToChainName: {}, + bech32PrefixToChainName: { + agoric: 'agoric', + }, brandDenom: {}, - chainInfos: {}, + chainInfos: { + agoric: { + bech32Prefix: 'agoric', + chainId: 'agoric-3', + icqEnabled: false, + stakingTokens: [ + { + denom: 'ubld', + }, + ], + }, + }, connectionInfos: {}, denom: {}, lookupChainInfo_kindHandle: 'Alleged: kind', @@ -40,6 +53,7 @@ Generated by [AVA](https://avajs.dev). lookupConnectionInfo_kindHandle: 'Alleged: kind', }, contract: { + Advancer: 'Alleged: Fast USDC Advancer advancer', 'Fast USDC Advancer_kindHandle': 'Alleged: kind', 'Fast USDC Creator_kindHandle': 'Alleged: kind', 'Fast USDC Creator_singleton': 'Alleged: Fast USDC Creator', @@ -49,6 +63,11 @@ Generated by [AVA](https://avajs.dev). 'Fast USDC Settler_kindHandle': 'Alleged: kind', 'Fast USDC Status Manager_kindHandle': 'Alleged: kind', 'Fast USDC Status Manager_singleton': 'Alleged: Fast USDC Status Manager', + 'Feed Kit': { + creator: Object @Alleged: Fast USDC Feed creator {}, + operatorPowers: Object @Alleged: Fast USDC Feed operatorPowers {}, + public: Object @Alleged: Fast USDC Feed public {}, + }, Kinds: { 'Transaction Feed_kindHandle': 'Alleged: kind', }, @@ -63,12 +82,17 @@ Generated by [AVA](https://avajs.dev). 'Liquidity Pool_kindHandle': 'Alleged: kind', 'Operator Kit_kindHandle': 'Alleged: kind', PendingTxs: {}, + 'Pool Local Orch Account': 'Vow', SeenTxs: [], mint: { PoolShare: 'Alleged: zcfMint', }, operators: {}, - orchestration: {}, + orchestration: { + makeLocalAccount: { + asyncFlow_kindHandle: 'Alleged: kind', + }, + }, pending: {}, vstorage: { 'Durable Publish Kit_kindHandle': 'Alleged: kind', @@ -81,7 +105,12 @@ Generated by [AVA](https://avajs.dev). LocalChainFacade_kindHandle: 'Alleged: kind', Orchestrator_kindHandle: 'Alleged: kind', RemoteChainFacade_kindHandle: 'Alleged: kind', - chainName: {}, + chainName: { + agoric: { + pending: false, + value: Object @Alleged: LocalChainFacade public {}, + }, + }, ibcTools: { IBCTransferSenderKit_kindHandle: 'Alleged: kind', ibcResultWatcher_kindHandle: 'Alleged: kind', diff --git a/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap b/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap index 7658bda5e41..de372333f82 100644 Binary files a/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap and b/packages/fast-usdc/test/snapshots/fast-usdc.contract.test.ts.snap differ diff --git a/packages/fast-usdc/test/supports.ts b/packages/fast-usdc/test/supports.ts index 82e8955c4bc..8ce9d7b12ea 100644 --- a/packages/fast-usdc/test/supports.ts +++ b/packages/fast-usdc/test/supports.ts @@ -52,7 +52,7 @@ export const commonSetup = async (t: ExecutionContext) => { onToBridge: obj => bankBridgeMessages.push(obj), }); await E(bankManager).addAsset( - 'uusdc', + 'ibc/usdconagoric', 'USDC', 'USD Circle Stablecoin', usdc.issuerKit, @@ -64,13 +64,13 @@ export const commonSetup = async (t: ExecutionContext) => { // TODO https://github.com/Agoric/agoric-sdk/issues/9966 await makeWellKnownSpaces(agoricNamesAdmin, t.log, ['vbankAsset']); await E(E(agoricNamesAdmin).lookupAdmin('vbankAsset')).update( - 'uusdc', + 'ibc/usdconagoric', /** @type {AssetInfo} */ harden({ brand: usdc.brand, issuer: usdc.issuer, - issuerName: 'IST', + issuerName: 'USDC', denom: 'uusdc', - proposedName: 'IST', + proposedName: 'USDC', displayInfo: { IOU: true }, }), ); diff --git a/packages/fast-usdc/test/utils/fees.test.ts b/packages/fast-usdc/test/utils/fees.test.ts index 5da214a8e26..07004b3b125 100644 --- a/packages/fast-usdc/test/utils/fees.test.ts +++ b/packages/fast-usdc/test/utils/fees.test.ts @@ -1,8 +1,12 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { makeIssuerKit, AmountMath } from '@agoric/ertp'; import { makeRatioFromAmounts } from '@agoric/zoe/src/contractSupport/ratio.js'; +import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; +import { q } from '@endo/errors'; import { makeFeeTools } from '../../src/utils/fees.js'; import type { FeeConfig } from '../../src/types.js'; +import { makeTestFeeConfig } from '../mocks.js'; +import { MockCctpTxEvidences } from '../fixtures.js'; const { add, isEqual } = AmountMath; @@ -44,17 +48,18 @@ const feeToolsScenario = test.macro({ { config = aFeeConfig, requested, expected }: FeelToolsScenario, ) => { const { totalFee, advance, split } = expected; + const feeTools = makeFeeTools(harden(config)); + const debugString = `expected:\n${q(feeTools.calculateSplit(requested)).toString()}`; t.true( isEqual(totalFee, add(split.ContractFee, split.PoolFee)), - 'sanity check: total fee equals sum of splits', + `sanity check: total fee equals sum of splits. ${debugString}`, ); t.true( isEqual(requested, add(totalFee, advance)), - 'sanity check: requested equals advance plus fee', + `sanity check: requested equals advance plus fee. ${debugString}`, ); - const feeTools = makeFeeTools(harden(config)); t.deepEqual(feeTools.calculateAdvanceFee(requested), totalFee); t.deepEqual(feeTools.calculateAdvance(requested), advance); t.deepEqual(feeTools.calculateSplit(requested), { @@ -161,7 +166,39 @@ test(feeToolsScenario, { }, }); -test.only('request must exceed fees', t => { +test(feeToolsScenario, { + name: 'AGORIC_PLUS_OSMO with commonPrivateArgs.feeConfig', + // 150_000_000n + requested: USDC(MockCctpTxEvidences.AGORIC_PLUS_OSMO().tx.amount), + // same as commonPrivateArgs.feeConfig from `CommonSetup` + config: makeTestFeeConfig(withAmountUtils(issuerKits.USDC)), + expected: { + totalFee: USDC(3000001n), // 1n + min(2% of 150USDC, 5USDC) + advance: USDC(146999999n), + split: { + ContractFee: USDC(600000n), // 20% of fee + PoolFee: USDC(2400001n), + }, + }, +}); + +test(feeToolsScenario, { + name: 'AGORIC_PLUS_DYDX with commonPrivateArgs.feeConfig', + // 300_000_000n + requested: USDC(MockCctpTxEvidences.AGORIC_PLUS_DYDX().tx.amount), + // same as commonPrivateArgs.feeConfig from `CommonSetup` + config: makeTestFeeConfig(withAmountUtils(issuerKits.USDC)), + expected: { + totalFee: USDC(5000001n), // 1n + min(2% of 300USDC, 5USDC) + advance: USDC(294999999n), + split: { + ContractFee: USDC(1000000n), // 20% of fee + PoolFee: USDC(4000001n), + }, + }, +}); + +test('request must exceed fees', t => { const feeTools = makeFeeTools(aFeeConfig); const expectedError = { message: 'Request must exceed fees.' };