From 25c59e161b1f909cc27c2a7d23b89b33f0c32c3f Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 20 Nov 2024 14:57:07 -0600 Subject: [PATCH] feat(fast-usdc): settler handles OBSERVED tx --- packages/fast-usdc/src/exos/settler.js | 38 ++++++++++-- packages/fast-usdc/test/exos/settler.test.ts | 65 +++++++++++++++++++- packages/fast-usdc/test/mocks.ts | 6 +- 3 files changed, 100 insertions(+), 9 deletions(-) diff --git a/packages/fast-usdc/src/exos/settler.js b/packages/fast-usdc/src/exos/settler.js index a81189837fb..b30e4336f4b 100644 --- a/packages/fast-usdc/src/exos/settler.js +++ b/packages/fast-usdc/src/exos/settler.js @@ -1,15 +1,17 @@ +import { AmountMath } from '@agoric/ertp'; import { assertAllDefined, makeTracer } from '@agoric/internal'; import { atob } from '@endo/base64'; import { makeError, q } from '@endo/errors'; +import { E } from '@endo/far'; import { M } from '@endo/patterns'; -import { AmountMath } from '@agoric/ertp'; +import { PendingTxStatus } from '../constants.js'; import { addressTools } from '../utils/address.js'; import { makeFeeTools } from '../utils/fees.js'; /** * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; - * @import {Denom, OrchestrationAccount, LocalAccountMethods} from '@agoric/orchestration'; + * @import {Denom, OrchestrationAccount, LocalAccountMethods, ChainHub} from '@agoric/orchestration'; * @import {WithdrawToSeat} from '@agoric/orchestration/src/utils/zoe-tools' * @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats'; * @import {Zone} from '@agoric/zone'; @@ -30,10 +32,11 @@ const trace = makeTracer('Settler'); * @param {FeeConfig} caps.feeConfig * @param {HostOf} caps.withdrawToSeat * @param {import('@agoric/vow').VowTools} caps.vowTools + * @param {ChainHub} caps.chainHub */ export const prepareSettler = ( zone, - { statusManager, USDC, zcf, feeConfig, withdrawToSeat, vowTools }, + { statusManager, USDC, zcf, feeConfig, withdrawToSeat, vowTools, chainHub }, ) => { assertAllDefined({ statusManager }); return zone.exoClass( @@ -41,6 +44,9 @@ export const prepareSettler = ( M.interface('SettlerI', { monitorTransfers: M.callWhen().returns(M.any()), receiveUpcall: M.call(M.record()).returns(M.promise()), + settleSansFees: M.call(M.string(), M.string(), M.nat()).returns( + M.promise(), + ), }), /** * @param {{ @@ -107,6 +113,11 @@ export const prepareSettler = ( ); } + const pending = statusManager.lookupPending(sender, amountInt); + if (pending.find(it => it.status === PendingTxStatus.Observed)) { + return this.self.settleSansFees(sender, EUD, amountInt); + } + // Disperse funds const { repayer, settlementAccount } = this.state; @@ -133,10 +144,25 @@ export const prepareSettler = ( repayer.repay(settlingSeat, split); // update status manager, marking tx `SETTLED` - statusManager.settle( - /** @type {NobleAddress} */ (tx.sender), - amountInt, + statusManager.settle(sender, amountInt); + }, + /** + * @param {NobleAddress} sender + * @param {string} EUD + * @param {bigint} amountInt + */ + async settleSansFees(sender, EUD, amountInt) { + const { settlementAccount } = this.state; + + const dest = chainHub.makeChainAddress(EUD); + + const txfrV = E(settlementAccount).transfer( + dest, + AmountMath.make(USDC, amountInt), ); + await vowTools.when(txfrV); // TODO: watch, handle failure + + statusManager.settle(sender, amountInt); }, }, { diff --git a/packages/fast-usdc/test/exos/settler.test.ts b/packages/fast-usdc/test/exos/settler.test.ts index c237496ae34..5aa09ada83b 100644 --- a/packages/fast-usdc/test/exos/settler.test.ts +++ b/packages/fast-usdc/test/exos/settler.test.ts @@ -64,6 +64,9 @@ const makeTestContext = async t => { return vowTools.asVow(() => {}); }; + const { chainHub } = common.facadeServices; + chainHub.registerChain('dydx', fetchedChainInfo.dydx); + chainHub.registerChain('osmosis', fetchedChainInfo.osmosis); const makeSettler = prepareSettler(zone.subZone('settler'), { statusManager, USDC: usdc.brand, @@ -71,6 +74,7 @@ const makeTestContext = async t => { withdrawToSeat: mockWithdrawToSeat, feeConfig: common.commonPrivateArgs.feeConfig, vowTools: common.bootstrap.vowTools, + chainHub, }); const defaultSettlerParams = harden({ @@ -126,7 +130,7 @@ const makeTestContext = async t => { const test = anyTest as TestFn>>; -test.before(async t => (t.context = await makeTestContext(t))); +test.beforeEach(async t => (t.context = await makeTestContext(t))); test('happy path: disburse to LPs; StatusManager removes tx', async t => { const { @@ -218,6 +222,65 @@ test('happy path: disburse to LPs; StatusManager removes tx', async t => { // TODO, confirm vstorage write for TxStatus.SETTLED }); +test('slow path: disburse to LPs; StatusManager removes tx', async t => { + const { + common, + makeSettler, + statusManager, + defaultSettlerParams, + repayer, + simulate, + accounts, + peekCalls, + } = t.context; + const { usdc } = common.brands; + const { feeConfig } = common.commonPrivateArgs; + + const settler = makeSettler({ + repayer, + settlementAccount: accounts.settlement.account, + ...defaultSettlerParams, + }); + + const cctpTxEvidence = simulate.observe(); + t.deepEqual( + statusManager.lookupPending( + cctpTxEvidence.tx.forwardingAddress, + cctpTxEvidence.tx.amount, + ), + [{ ...cctpTxEvidence, status: PendingTxStatus.Observed }], + 'statusManager shows this tx is only observed', + ); + + t.log('Simulate incoming IBC settlement'); + void settler.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO()); + await eventLoopIteration(); + + t.log('review settler interactions with other components'); + t.deepEqual(peekCalls(), []); + t.deepEqual(accounts.settlement.callLog, [ + [ + 'transfer', + { + chainId: 'osmosis-1', + encoding: 'bech32', + value: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', + }, + usdc.units(150), + ], + ]); + + t.deepEqual( + statusManager.lookupPending( + cctpTxEvidence.tx.forwardingAddress, + cctpTxEvidence.tx.amount, + ), + [], + 'SETTLED entry removed from StatusManger', + ); + // TODO, confirm vstorage write for TxStatus.SETTLED +}); + test.todo("StatusManager does not receive update when we can't settle"); test.todo('Observed -> Settle flow'); diff --git a/packages/fast-usdc/test/mocks.ts b/packages/fast-usdc/test/mocks.ts index d453e9e590f..d6196d3b7d5 100644 --- a/packages/fast-usdc/test/mocks.ts +++ b/packages/fast-usdc/test/mocks.ts @@ -44,9 +44,10 @@ export const prepareMockOrchAccounts = ( OrchestrationAccount<{ chainId: 'agoric' }> >; + const settlementCallLog = [] as any[]; const settlementAccountMock = zone.exo('Mock Settlement Account', undefined, { - someMethod() { - throw Error('todo'); + transfer(...args) { + settlementCallLog.push(harden(['transfer', ...args])); }, }); const settlementAccount = settlementAccountMock as unknown as HostInterface< @@ -59,6 +60,7 @@ export const prepareMockOrchAccounts = ( }, settlement: { account: settlementAccount, + callLog: settlementCallLog, }, }; };