diff --git a/packages/fast-usdc/src/exos/advancer.js b/packages/fast-usdc/src/exos/advancer.js index 902cc890283..f5a9f8afd09 100644 --- a/packages/fast-usdc/src/exos/advancer.js +++ b/packages/fast-usdc/src/exos/advancer.js @@ -1,6 +1,6 @@ -import { AmountMath, AmountShape } from '@agoric/ertp'; +import { AmountMath } from '@agoric/ertp'; import { assertAllDefined, makeTracer } from '@agoric/internal'; -import { ChainAddressShape } from '@agoric/orchestration'; +import { AnyNatAmountShape, ChainAddressShape } from '@agoric/orchestration'; import { pickFacet } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; import { q } from '@endo/errors'; @@ -16,6 +16,7 @@ import { makeFeeTools } from '../utils/fees.js'; /** * @import {HostInterface} from '@agoric/async-flow'; + * @import {TypedPattern} from '@agoric/internal' * @import {NatAmount} from '@agoric/ertp'; * @import {ChainAddress, ChainHub, Denom, OrchestrationAccount} from '@agoric/orchestration'; * @import {ZoeTools} from '@agoric/orchestration/src/utils/zoe-tools.js'; @@ -39,47 +40,40 @@ import { makeFeeTools } from '../utils/fees.js'; * }} AdvancerKitPowers */ +/** @type {TypedPattern} */ +const AdvancerVowCtxShape = M.splitRecord( + { + fullAmount: AnyNatAmountShape, + advanceAmount: AnyNatAmountShape, + destination: ChainAddressShape, + forwardingAddress: M.string(), + txHash: EvmHashShape, + }, + { tmpSeat: M.remotable() }, +); + /** 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(M.undefined(), { - amount: AmountShape, - destination: ChainAddressShape, - forwardingAddress: M.string(), - tmpSeat: M.remotable(), - txHash: EvmHashShape, - }).returns(VowShape), - onRejected: M.call(M.error(), { - amount: AmountShape, - destination: ChainAddressShape, - forwardingAddress: M.string(), - tmpSeat: M.remotable(), - txHash: EvmHashShape, - }).returns(), + onFulfilled: M.call(M.undefined(), AdvancerVowCtxShape).returns(VowShape), + onRejected: M.call(M.error(), AdvancerVowCtxShape).returns(), }), transferHandler: M.interface('TransferHandlerI', { // TODO confirm undefined, and not bigint (sequence) - onFulfilled: M.call(M.undefined(), { - amount: AmountShape, - destination: ChainAddressShape, - forwardingAddress: M.string(), - txHash: EvmHashShape, - }).returns(M.undefined()), - onRejected: M.call(M.error(), { - amount: AmountShape, - destination: ChainAddressShape, - forwardingAddress: M.string(), - txHash: EvmHashShape, - }).returns(M.undefined()), + onFulfilled: M.call(M.undefined(), AdvancerVowCtxShape).returns( + M.undefined(), + ), + onRejected: M.call(M.error(), AdvancerVowCtxShape).returns(M.undefined()), }), }); /** * @typedef {{ - * amount: NatAmount; + * fullAmount: NatAmount; + * advanceAmount: NatAmount; * destination: ChainAddress; * forwardingAddress: NobleAddress; * txHash: EvmHash; @@ -155,9 +149,9 @@ export const prepareAdvancerKit = ( // throws if the bech32 prefix is not found const destination = chainHub.makeChainAddress(EUD); - const requestedAmount = toAmount(evidence.tx.amount); + const fullAmount = toAmount(evidence.tx.amount); // throws if requested does not exceed fees - const advanceAmount = feeTools.calculateAdvance(requestedAmount); + const advanceAmount = feeTools.calculateAdvance(fullAmount); const { zcfSeat: tmpSeat } = zcf.makeEmptySeatKit(); const amountKWR = harden({ USDC: advanceAmount }); @@ -174,7 +168,8 @@ export const prepareAdvancerKit = ( amountKWR, ); void watch(depositV, this.facets.depositHandler, { - amount: advanceAmount, + fullAmount, + advanceAmount, destination, forwardingAddress: evidence.tx.forwardingAddress, tmpSeat, @@ -193,16 +188,15 @@ export const prepareAdvancerKit = ( */ onFulfilled(result, ctx) { const { poolAccount } = this.state; - const { amount, destination, forwardingAddress, txHash } = ctx; + const { destination, advanceAmount, ...detail } = ctx; const transferV = E(poolAccount).transfer(destination, { denom: usdc.denom, - value: amount.value, + value: advanceAmount.value, }); return watch(transferV, this.facets.transferHandler, { destination, - amount, - forwardingAddress, - txHash, + advanceAmount, + ...detail, }); }, /** @@ -222,23 +216,23 @@ export const prepareAdvancerKit = ( }, transferHandler: { /** - * @param {undefined} result TODO confirm this is not a bigint (sequence) + * @param {unknown} result TODO confirm this is not a bigint (sequence) * @param {AdvancerVowCtx} ctx */ onFulfilled(result, ctx) { const { notifyFacet } = this.state; - const { amount, destination, forwardingAddress, txHash } = ctx; + const { advanceAmount, destination, ...detail } = ctx; log( 'Advance transfer fulfilled', - q({ amount, destination, result }).toString(), - ); - notifyFacet.notifyAdvancingResult( - txHash, - forwardingAddress, - amount.value, - destination.value, - true, + q({ advanceAmount, destination, result }).toString(), ); + // During development, due to a bug, this call threw. + // The failure was silent (no diagnostics) due to: + // - #10576 Vows do not report unhandled rejections + // For now, the advancer kit relies on consistency between + // notifyFacet, statusManager, and callers of handleTransactionEvent(). + // TODO: revisit #10576 during #10510 + notifyFacet.notifyAdvancingResult({ destination, ...detail }, true); }, /** * @param {Error} error @@ -246,15 +240,8 @@ export const prepareAdvancerKit = ( */ onRejected(error, ctx) { const { notifyFacet } = this.state; - const { amount, destination, forwardingAddress, txHash } = ctx; log('Advance transfer rejected', q(error).toString()); - notifyFacet.notifyAdvancingResult( - txHash, - forwardingAddress, - amount.value, - destination.value, - false, - ); + notifyFacet.notifyAdvancingResult(ctx, false); }, }, }, diff --git a/packages/fast-usdc/src/exos/settler.js b/packages/fast-usdc/src/exos/settler.js index 9083a80b420..72f1e1f9626 100644 --- a/packages/fast-usdc/src/exos/settler.js +++ b/packages/fast-usdc/src/exos/settler.js @@ -11,18 +11,16 @@ import { EvmHashShape } from '../type-guards.js'; /** * @import {FungibleTokenPacketData} from '@agoric/cosmic-proto/ibc/applications/transfer/v2/packet.js'; - * @import {Denom, OrchestrationAccount, ChainHub} from '@agoric/orchestration'; + * @import {Denom, OrchestrationAccount, ChainHub, ChainAddress} from '@agoric/orchestration'; * @import {WithdrawToSeat} from '@agoric/orchestration/src/utils/zoe-tools' * @import {IBCChannelID, VTransferIBCEvent} from '@agoric/vats'; * @import {Zone} from '@agoric/zone'; * @import {HostOf, HostInterface} from '@agoric/async-flow'; * @import {TargetRegistration} from '@agoric/vats/src/bridge-target.js'; - * @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash} from '../types.js'; + * @import {NobleAddress, LiquidityPoolKit, FeeConfig, EvmHash, LogFn} from '../types.js'; * @import {StatusManager} from './status-manager.js'; */ -const trace = makeTracer('Settler'); - /** * NOTE: not meant to be parsable. * @@ -42,10 +40,20 @@ const makeMintedEarlyKey = (addr, amount) => * @param {HostOf} caps.withdrawToSeat * @param {import('@agoric/vow').VowTools} caps.vowTools * @param {ChainHub} caps.chainHub + * @param {LogFn} [caps.log] */ export const prepareSettler = ( zone, - { statusManager, USDC, zcf, feeConfig, withdrawToSeat, vowTools, chainHub }, + { + chainHub, + feeConfig, + log = makeTracer('Settler', true), + statusManager, + USDC, + vowTools, + withdrawToSeat, + zcf, + }, ) => { assertAllDefined({ statusManager }); return zone.exoClassKit( @@ -59,8 +67,7 @@ export const prepareSettler = ( }), notify: M.interface('SettlerNotifyI', { notifyAdvancingResult: M.call( - M.string(), - M.nat(), + M.record(), // XXX fill in details TODO M.boolean(), ).returns(), }), @@ -89,6 +96,7 @@ export const prepareSettler = ( * }} config */ config => { + log('config', config); return { ...config, /** @type {HostInterface|undefined} */ @@ -111,11 +119,12 @@ export const prepareSettler = ( tap: { /** @param {VTransferIBCEvent} event */ async receiveUpcall(event) { + log('upcall event', event.packet.sequence, event.blockTime); const { sourceChannel, remoteDenom } = this.state; const { packet } = event; if (packet.source_channel !== sourceChannel) { const { source_channel: actual } = packet; - trace('unexpected channel', { actual, expected: sourceChannel }); + log('unexpected channel', { actual, expected: sourceChannel }); return; } @@ -129,7 +138,7 @@ export const prepareSettler = ( if (tx.denom !== remoteDenom) { const { denom: actual } = tx; - trace('unexpected denom', { actual, expected: remoteDenom }); + log('unexpected denom', { actual, expected: remoteDenom }); return; } @@ -148,7 +157,7 @@ export const prepareSettler = ( const { self } = this.facets; const found = statusManager.dequeueStatus(sender, amount); - trace('dequeued', found, 'for', sender, amount); + log('dequeued', found, 'for', sender, amount); switch (found?.status) { case PendingTxStatus.Advanced: return self.disburse(found.txHash, sender, amount); @@ -157,35 +166,51 @@ export const prepareSettler = ( this.state.mintedEarly.add(makeMintedEarlyKey(sender, amount)); return; - case undefined: case PendingTxStatus.Observed: case PendingTxStatus.AdvanceFailed: + return self.forward(found.txHash, sender, amount, EUD); + + case undefined: default: - return self.forward(found?.txHash, sender, amount, EUD); + log('⚠️ tap: no status for ', sender, amount); } }, }, notify: { /** - * @param {EvmHash} txHash - * @param {NobleAddress} sender - * @param {NatValue} amount - * @param {string} EUD + * @param {object} ctx + * @param {EvmHash} ctx.txHash + * @param {NobleAddress} ctx.forwardingAddress + * @param {Amount<'nat'>} ctx.fullAmount + * @param {ChainAddress} ctx.destination * @param {boolean} success * @returns {void} */ - notifyAdvancingResult(txHash, sender, amount, EUD, success) { + notifyAdvancingResult( + { txHash, forwardingAddress, fullAmount, destination }, + success, + ) { const { mintedEarly } = this.state; - const key = makeMintedEarlyKey(sender, amount); + const { value: fullValue } = fullAmount; + const key = makeMintedEarlyKey(forwardingAddress, fullValue); if (mintedEarly.has(key)) { mintedEarly.delete(key); if (success) { - void this.facets.self.disburse(txHash, sender, amount); + void this.facets.self.disburse( + txHash, + forwardingAddress, + fullValue, + ); } else { - void this.facets.self.forward(txHash, sender, amount, EUD); + void this.facets.self.forward( + txHash, + forwardingAddress, + fullValue, + destination.value, + ); } } else { - statusManager.advanceOutcome(sender, amount, success); + statusManager.advanceOutcome(forwardingAddress, fullValue, success); } }, }, @@ -193,15 +218,15 @@ export const prepareSettler = ( /** * @param {EvmHash} txHash * @param {NobleAddress} sender - * @param {NatValue} amount + * @param {NatValue} fullValue */ - async disburse(txHash, sender, amount) { + async disburse(txHash, sender, fullValue) { const { repayer, settlementAccount } = this.state; - const received = AmountMath.make(USDC, amount); + const received = AmountMath.make(USDC, fullValue); const { zcfSeat: settlingSeat } = zcf.makeEmptySeatKit(); const { calculateSplit } = makeFeeTools(feeConfig); const split = calculateSplit(received); - trace('disbursing', split); + log('disbursing', split); // TODO: what if this throws? // arguably, it cannot. Even if deposits @@ -224,12 +249,12 @@ export const prepareSettler = ( statusManager.disbursed(txHash); }, /** - * @param {EvmHash | undefined} txHash + * @param {EvmHash} txHash * @param {NobleAddress} sender - * @param {NatValue} amount + * @param {NatValue} fullValue * @param {string} EUD */ - forward(txHash, sender, amount, EUD) { + forward(txHash, sender, fullValue, EUD) { const { settlementAccount } = this.state; const dest = chainHub.makeChainAddress(EUD); @@ -237,12 +262,12 @@ export const prepareSettler = ( // TODO? statusManager.forwarding(txHash, sender, amount); const txfrV = E(settlementAccount).transfer( dest, - AmountMath.make(USDC, amount), + AmountMath.make(USDC, fullValue), ); void vowTools.watch(txfrV, this.facets.transferHandler, { txHash, sender, - amount, + fullValue, }); }, }, @@ -254,20 +279,21 @@ export const prepareSettler = ( * @typedef {{ * txHash: EvmHash; * sender: NobleAddress; - * amount: NatValue; + * fullValue: NatValue; * }} SettlerTransferCtx */ onFulfilled(_result, ctx) { - const { txHash, sender, amount } = ctx; - statusManager.forwarded(txHash, sender, amount); + const { txHash, sender, fullValue } = ctx; + statusManager.forwarded(txHash, sender, fullValue); }, /** - * @param {unknown} _result - * @param {SettlerTransferCtx} _ctx + * @param {unknown} reason + * @param {SettlerTransferCtx} ctx */ - onRejected(_result, _ctx) { + onRejected(reason, ctx) { + log('⚠️ transfer rejected!', reason, ctx); // const { txHash, sender, amount } = ctx; - // TODO: statusManager.forwardFailed(txHash, sender, amount); + // TODO(#10510): statusManager.forwardFailed(txHash, sender, amount); }, }, }, diff --git a/packages/fast-usdc/src/fast-usdc.contract.js b/packages/fast-usdc/src/fast-usdc.contract.js index 0863dcdc210..d7f886cec7a 100644 --- a/packages/fast-usdc/src/fast-usdc.contract.js +++ b/packages/fast-usdc/src/fast-usdc.contract.js @@ -246,9 +246,12 @@ export const contract = async (zcf, privateArgs, zone, tools) => { vowTools.all([poolAccountV, settleAccountV]), ); + const [_agoric, _noble, agToNoble] = await vowTools.when( + chainHub.getChainsAndConnection('agoric', 'noble'), + ); const settlerKit = makeSettler({ repayer: poolKit.repayer, - sourceChannel: 'channel-1234', // TODO: fix this as soon as testing needs it', + sourceChannel: agToNoble.transferChannel.counterPartyChannelId, remoteDenom: 'uusdc', settlementAccount, }); diff --git a/packages/fast-usdc/test/exos/advancer.test.ts b/packages/fast-usdc/test/exos/advancer.test.ts index 14f1fe4b3ea..0921fb4838f 100644 --- a/packages/fast-usdc/test/exos/advancer.test.ts +++ b/packages/fast-usdc/test/exos/advancer.test.ts @@ -186,17 +186,22 @@ test('updates status to ADVANCING in happy path', async t => { t.deepEqual(inspectLogs(0), [ 'Advance transfer fulfilled', - '{"amount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', + '{"advanceAmount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', ]); // We expect to see an `Advanced` update, but that is now Settler's job. // but we can ensure it's called - t.deepEqual(inspectNotifyCalls(), [ + t.like(inspectNotifyCalls(), [ [ - mockEvidence.txHash, - mockEvidence.tx.forwardingAddress, - feeTools.calculateAdvance(usdc.make(mockEvidence.tx.amount)).value, - addressTools.getQueryParams(mockEvidence.aux.recipientAddress).EUD, + { + txHash: mockEvidence.txHash, + forwardingAddress: mockEvidence.tx.forwardingAddress, + fullAmount: usdc.make(mockEvidence.tx.amount), + destination: { + value: addressTools.getQueryParams(mockEvidence.aux.recipientAddress) + .EUD, + }, + }, true, // indicates transfer succeeded ], ]); @@ -305,13 +310,21 @@ test('calls notifyAdvancingResult (AdvancedFailed) on failed transfer', async t // We expect to see an `AdvancedFailed` update, but that is now Settler's job. // but we can ensure it's called - t.deepEqual(inspectNotifyCalls(), [ + t.like(inspectNotifyCalls(), [ [ - mockEvidence.txHash, - mockEvidence.tx.forwardingAddress, - feeTools.calculateAdvance(usdc.make(mockEvidence.tx.amount)).value, - addressTools.getQueryParams(mockEvidence.aux.recipientAddress).EUD, - false, // this indicates transfer succeeded + { + txHash: mockEvidence.txHash, + forwardingAddress: mockEvidence.tx.forwardingAddress, + fullAmount: usdc.make(mockEvidence.tx.amount), + advanceAmount: feeTools.calculateAdvance( + usdc.make(mockEvidence.tx.amount), + ), + destination: { + value: addressTools.getQueryParams(mockEvidence.aux.recipientAddress) + .EUD, + }, + }, + false, // this indicates transfer failed ], ]); }); @@ -364,7 +377,7 @@ test('will not advance same txHash:chainId evidence twice', async t => { t.deepEqual(inspectLogs(0), [ 'Advance transfer fulfilled', - '{"amount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', + '{"advanceAmount":{"brand":"[Alleged: USDC brand]","value":"[146999999n]"},"destination":{"chainId":"osmosis-1","encoding":"bech32","value":"osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men"},"result":"[undefined]"}', ]); // Second attempt diff --git a/packages/fast-usdc/test/exos/settler.test.ts b/packages/fast-usdc/test/exos/settler.test.ts index 6ca8491db2f..c970f55eaed 100644 --- a/packages/fast-usdc/test/exos/settler.test.ts +++ b/packages/fast-usdc/test/exos/settler.test.ts @@ -1,17 +1,17 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import type { TestFn } from 'ava'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; import type { Zone } from '@agoric/zone'; -import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { PendingTxStatus } from '../../src/constants.js'; import { prepareSettler } from '../../src/exos/settler.js'; import { prepareStatusManager } from '../../src/exos/status-manager.js'; -import { commonSetup } from '../supports.js'; -import { MockCctpTxEvidences, MockVTransferEvents } from '../fixtures.js'; import type { CctpTxEvidence } from '../../src/types.js'; -import { makeTestLogger, prepareMockOrchAccounts } from '../mocks.js'; import { makeFeeTools } from '../../src/utils/fees.js'; +import { MockCctpTxEvidences, MockVTransferEvents } from '../fixtures.js'; +import { makeTestLogger, prepareMockOrchAccounts } from '../mocks.js'; +import { commonSetup } from '../supports.js'; const mockZcf = (zone: Zone) => { const callLog = [] as any[]; @@ -79,6 +79,7 @@ const makeTestContext = async t => { feeConfig: common.commonPrivateArgs.feeConfig, vowTools: common.bootstrap.vowTools, chainHub, + log, }); const defaultSettlerParams = harden({ @@ -307,7 +308,6 @@ test('Settlement for unknown transaction', async t => { peekCalls, inspectLogs, } = t.context; - const { usdc } = common.brands; const settler = makeSettler({ repayer, @@ -319,22 +319,19 @@ test('Settlement for unknown transaction', async t => { void settler.tap.receiveUpcall(MockVTransferEvents.AGORIC_PLUS_OSMO()); await eventLoopIteration(); - t.log('USDC was forwarded'); + t.log('Nothing was transferrred'); t.deepEqual(peekCalls(), []); - t.deepEqual(accounts.settlement.callLog, [ + t.deepEqual(accounts.settlement.callLog, []); + t.like(inspectLogs(), [ + ['config', { sourceChannel: 'channel-21' }], + ['upcall event'], + ['dequeued', undefined], [ - 'transfer', - { - chainId: 'osmosis-1', - encoding: 'bech32', - value: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', - }, - usdc.units(150), + '⚠️ tap: no status for ', + 'noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd', + 150000000n, ], ]); - t.deepEqual(inspectLogs(0), [ - '⚠️ Forwarded minted amount 150000000 from account noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelqkd before it was observed.', - ]); }); test.todo("StatusManager does not receive update when we can't settle"); diff --git a/packages/fast-usdc/test/fast-usdc.contract.test.ts b/packages/fast-usdc/test/fast-usdc.contract.test.ts index e697b9118da..38e80e76e71 100644 --- a/packages/fast-usdc/test/fast-usdc.contract.test.ts +++ b/packages/fast-usdc/test/fast-usdc.contract.test.ts @@ -1,36 +1,48 @@ -import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import type { ExecutionContext } from 'ava'; +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import type { ExecutionContext, TestFn } from 'ava'; import { AmountMath } from '@agoric/ertp/src/amountMath.js'; +import { deeplyFulfilledObject } from '@agoric/internal'; import { eventLoopIteration, inspectMapStore, } from '@agoric/internal/src/testing-utils.js'; +import { + makePublishKit, + observeIteration, + subscribeEach, + type Subscriber, +} from '@agoric/notifier'; +import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; +import { buildVTransferEvent } from '@agoric/orchestration/tools/ibc-mocks.js'; +import { heapVowE as VE } from '@agoric/vow/vat.js'; import { divideBy, multiplyBy, parseRatio, } from '@agoric/zoe/src/contractSupport/ratio.js'; +import type { Instance } from '@agoric/zoe/src/zoeService/utils.js'; import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import { E } from '@endo/far'; +import { matches, objectMap } from '@endo/patterns'; +import { makePromiseKit } from '@endo/promise-kit'; import path from 'path'; -import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; -import { objectMap } from '@endo/patterns'; -import { deeplyFulfilledObject } from '@agoric/internal'; -import type { Subscriber } from '@agoric/notifier'; -import { MockCctpTxEvidences } from './fixtures.js'; -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 type { OperatorKit } from '../src/exos/operator-kit.js'; +import type { FastUsdcSF } from '../src/fast-usdc.contract.js'; +import { PoolMetricsShape } from '../src/type-guards.js'; +import type { CctpTxEvidence, FeeConfig, PoolMetrics } from '../src/types.js'; import { addressTools } from '../src/utils/address.js'; +import { makeFeeTools } from '../src/utils/fees.js'; +import { MockCctpTxEvidences } from './fixtures.js'; +import { commonSetup, uusdcOnAgoric } from './supports.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); const contractFile = `${dirname}/../src/fast-usdc.contract.js`; -type StartFn = typeof import('../src/fast-usdc.contract.js').start; -const { add, isGTE, subtract } = AmountMath; +const agToNoble = fetchedChainInfo.agoric.connections['noble-1']; + +const { add, isGTE, make, subtract, min } = AmountMath; const getInvitationProperties = async ( zoe: ZoeService, @@ -43,76 +55,184 @@ const getInvitationProperties = async ( type CommonSetup = Awaited>; const startContract = async ( - common: Pick, + common: Pick, + operatorQty = 1, ) => { const { brands: { usdc }, commonPrivateArgs, } = common; - 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 = + const { zoe, bundleAndInstall } = await setUpZoeForTest(); + const installation: Installation = await bundleAndInstall(contractFile); const startKit = await E(zoe).startInstance( installation, { USDC: usdc.issuer }, - { - usdcDenom: 'ibc/usdconagoric', - }, + { usdcDenom: uusdcOnAgoric }, commonPrivateArgs, ); - return { ...startKit, zoe }; + const terms = await E(zoe).getTerms(startKit.instance); + + const { subscriber: metricsSub } = E.get( + E.get(E(startKit.publicFacet).getPublicTopics()).poolMetrics, + ); + + const opInvs = await Promise.all( + [...Array(operatorQty).keys()].map(opIx => + E(startKit.creatorFacet).makeOperatorInvitation(`operator-${opIx}`), + ), + ); + + return { + ...startKit, + terms, + zoe, + metricsSub, + invitations: { operator: opInvs }, + }; }; -test('oracle operators have closely-held rights to submit evidence of CCTP transactions', async t => { +const makeTestContext = async (t: ExecutionContext) => { const common = await commonSetup(t); - const { creatorFacet, zoe } = await startContract(common); - const operatorId = 'operator-1'; + const startKit = await startContract(common, 2); + + const { transferBridge } = common.mocks; + const evm = makeEVM(); + + const { inspectBankBridge, inspectLocalBridge } = common.utils; + const snapshot = () => ({ + bank: inspectBankBridge().length, + local: inspectLocalBridge().length, + }); + const since = ix => ({ + bank: inspectBankBridge().slice(ix.bank), + local: inspectLocalBridge().slice(ix.local), + }); + + const sync = { + ocw: makePromiseKit>[]>(), + lp: makePromiseKit>>(), + }; + + const { brands, utils } = common; + const { bankManager } = common.bootstrap; + const receiveUSDCAt = async (addr: string, amount: NatValue) => { + const pmt = await utils.pourPayment(make(brands.usdc.brand, amount)); + const purse = E(E(bankManager).getBankForAddress(addr)).getPurse( + brands.usdc.brand, + ); + return E(purse).deposit(pmt); + }; + + const mint = async (e: CctpTxEvidence) => { + const settlerAddr = 'agoric1fakeLCAAddress1'; // TODO: get from contract + const rxd = await receiveUSDCAt(settlerAddr, e.tx.amount); + await VE(transferBridge).fromBridge( + buildVTransferEvent({ + receiver: e.aux.recipientAddress, + target: settlerAddr, + sourceChannel: agToNoble.transferChannel.counterPartyChannelId, + denom: 'uusdc', + amount: e.tx.amount, + sender: e.tx.forwardingAddress, + }), + ); + await eventLoopIteration(); // let settler do work + return rxd; + }; + + return { bridges: { snapshot, since }, common, evm, mint, startKit, sync }; +}; + +const test = anyTest as TestFn>>; +test.before(async t => (t.context = await makeTestContext(t))); + +test('baggage', async t => { + const { + brands: { usdc }, + commonPrivateArgs, + } = await commonSetup(t); + + let contractBaggage; + const setJig = ({ baggage }) => { + contractBaggage = baggage; + }; + + const { zoe, bundleAndInstall } = await setUpZoeForTest({ setJig }); + const installation: Installation = + await bundleAndInstall(contractFile); + + await E(zoe).startInstance( + installation, + { USDC: usdc.issuer }, + { usdcDenom: uusdcOnAgoric }, + commonPrivateArgs, + ); + + const tree = inspectMapStore(contractBaggage); + t.snapshot(tree, 'contract baggage after start'); +}); - const opInv = await E(creatorFacet).makeOperatorInvitation(operatorId); +const purseOf = + (issuer: Issuer, { pourPayment }) => + async (value: bigint) => { + const brand = await E(issuer).getBrand(); + const purse = E(issuer).makeEmptyPurse(); + const pmt = await pourPayment(make(brand, value)); + await E(purse).deposit(pmt); + return purse; + }; +const makeOracleOperator = async ( + opInv: Invitation, + txSubscriber: Subscriber, + zoe: ZoeService, + t: ExecutionContext, +) => { + let done = 0; + const failures = [] as any[]; t.like(await getInvitationProperties(zoe, opInv), { description: 'oracle operator invitation', }); + // operator only gets `.invitationMakers` + // but for testing, we need `.admin` too. UNTIL #????? const operatorKit = await E(E(zoe).offer(opInv)).getOfferResult(); - t.deepEqual(Object.keys(operatorKit), [ 'admin', 'invitationMakers', 'operator', ]); - - const e1 = MockCctpTxEvidences.AGORIC_NO_PARAMS(); - - { - const inv = await E(operatorKit.invitationMakers).SubmitEvidence(e1); - const res = await E(E(zoe).offer(inv)).getOfferResult(); - t.is(res, 'inert; nothing should be expected from this offer'); - } - - // what removeOperator will do - await E(operatorKit.admin).disable(); - - await t.throwsAsync(E(operatorKit.invitationMakers).SubmitEvidence(e1), { - message: 'submitEvidence for disabled operator', + const { invitationMakers } = operatorKit; + + return harden({ + watch: () => { + void observeIteration(subscribeEach(txSubscriber), { + updateState: tx => + // KLUDGE: tx wouldn't include aux. OCW looks it up + E.when( + E(invitationMakers).SubmitEvidence(tx), + inv => + E.when(E(E(zoe).offer(inv)).getOfferResult(), res => { + t.is(res, 'inert; nothing should be expected from this offer'); + done += 1; + }), + reason => { + failures.push(reason.message); + }, + ), + }); + }, + getDone: () => done, + getFailures: () => harden([...failures]), + // operator only gets .invitationMakers + getKit: () => operatorKit, }); -}); +}; const logAmt = amt => [ Number(amt.value), @@ -127,170 +247,94 @@ const scaleAmount = (frac: number, amount: Amount<'nat'>) => { return multiplyBy(amount, asRatio); }; -const makeLpTools = ( - t: ExecutionContext, - common: Pick, - { - zoe, - terms, - subscriber, - publicFacet, - }: { - zoe: ZoeService; - subscriber: ERef>; - publicFacet: StartedInstanceKit['publicFacet']; - terms: StandardTerms & FastUsdcTerms; - }, +const makeLP = async ( + name: string, + usdcPurse: ERef, + zoe: ZoeService, + instance: Instance, ) => { - const { - brands: { usdc }, - utils, - } = common; - const makeLP = (name, usdcPurse: ERef) => { - const sharePurse = E(terms.issuers.PoolShares).makeEmptyPurse(); - let deposited = AmountMath.makeEmpty(usdc.brand); - const me = harden({ - deposit: async (qty: bigint) => { - const { - value: { shareWorth }, - } = await E(subscriber).getUpdateSince(); - const give = { USDC: usdc.make(qty) }; - const proposal = harden({ - give, - want: { PoolShare: divideBy(give.USDC, shareWorth) }, - }); - t.log(name, 'deposits', ...logAmt(proposal.give.USDC)); - const toDeposit = await E(publicFacet).makeDepositInvitation(); - const payments = { USDC: await E(usdcPurse).withdraw(give.USDC) }; - const payout = await E(zoe) - .offer(toDeposit, proposal, payments) - .then(seat => E(seat).getPayout('PoolShare')) - .then(pmt => E(sharePurse).deposit(pmt)) - .then(a => a as Amount<'nat'>); - t.log(name, 'deposit payout', ...logAmt(payout)); - t.true(isGTE(payout, proposal.want.PoolShare)); - deposited = add(deposited, give.USDC); - }, - - withdraw: async (portion: number) => { - const myShares = await E(sharePurse) - .getCurrentAmount() - .then(a => a as Amount<'nat'>); - const give = { PoolShare: scaleAmount(portion, myShares) }; - const { - value: { shareWorth }, - } = await E(subscriber).getUpdateSince(); - const myUSDC = multiplyBy(myShares, shareWorth); - const myFees = subtract(myUSDC, deposited); - t.log(name, 'sees fees earned', ...logAmt(myFees)); - const proposal = harden({ - give, - want: { USDC: multiplyBy(give.PoolShare, shareWorth) }, - }); - const pct = portion * 100; - t.log(name, 'withdraws', pct, '%:', ...logAmt(proposal.give.PoolShare)); - const toWithdraw = await E(publicFacet).makeWithdrawInvitation(); - const usdcPmt = await E(sharePurse) - .withdraw(proposal.give.PoolShare) - .then(pmt => E(zoe).offer(toWithdraw, proposal, { PoolShare: pmt })) - .then(seat => E(seat).getPayout('USDC')); - const amt = await E(usdcPurse).deposit(usdcPmt); - t.log(name, 'withdraw payout', ...logAmt(amt)); - t.true(isGTE(amt, proposal.want.USDC)); - }, - }); - return me; - }; - const purseOf = (value: bigint) => - E(terms.issuers.USDC) - .makeEmptyPurse() - .then(async p => { - const pmt = await utils.pourPayment(usdc.make(value)); - await p.deposit(pmt); - return p; - }); - return { makeLP, purseOf }; -}; - -test('LP deposits, earns fees, withdraws', async t => { - const common = await commonSetup(t); - const { - commonPrivateArgs, - brands: { usdc }, - utils, - } = common; - - const { instance, creatorFacet, publicFacet, zoe } = - await startContract(common); - const terms = await E(zoe).getTerms(instance); - + const publicFacet = E(zoe).getPublicFacet(instance); const { subscriber } = E.get( E.get(E(publicFacet).getPublicTopics()).poolMetrics, ); + const terms = await E(zoe).getTerms(instance); + const { USDC } = terms.brands; + const sharePurse = E(terms.issuers.PoolShares).makeEmptyPurse(); + let investment = AmountMath.makeEmpty(USDC); + const me = harden({ + deposit: async (t: ExecutionContext, qty: bigint) => { + const { + value: { shareWorth }, + } = await E(subscriber).getUpdateSince(); + const give = { USDC: make(USDC, qty) }; + const proposal = harden({ + give, + want: { PoolShare: divideBy(give.USDC, shareWorth) }, + }); + t.log(name, 'deposits', ...logAmt(proposal.give.USDC)); + const toDeposit = await E(publicFacet).makeDepositInvitation(); + const payments = { USDC: await E(usdcPurse).withdraw(give.USDC) }; + const payout = await E(zoe) + .offer(toDeposit, proposal, payments) + .then(seat => E(seat).getPayout('PoolShare')) + .then(pmt => E(sharePurse).deposit(pmt)) + .then(a => a as Amount<'nat'>); + t.log(name, 'deposit payout', ...logAmt(payout)); + t.true(isGTE(payout, proposal.want.PoolShare)); + investment = add(investment, give.USDC); + }, - const { makeLP, purseOf } = makeLpTools(t, common, { - publicFacet, - subscriber, - terms, - zoe, + withdraw: async (t: ExecutionContext, portion: number) => { + const myShares = await E(sharePurse) + .getCurrentAmount() + .then(a => a as Amount<'nat'>); + const give = { PoolShare: scaleAmount(portion, myShares) }; + const { + value: { shareWorth }, + } = await E(subscriber).getUpdateSince(); + const myUSDC = multiplyBy(myShares, shareWorth); + const myFees = subtract(myUSDC, investment); + t.log(name, 'sees fees earned', ...logAmt(myFees)); + const proposal = harden({ + give, + want: { USDC: multiplyBy(give.PoolShare, shareWorth) }, + }); + const pct = portion * 100; + t.log(name, 'withdraws', pct, '%:', ...logAmt(proposal.give.PoolShare)); + const toWithdraw = await E(publicFacet).makeWithdrawInvitation(); + const usdcPmt = await E(sharePurse) + .withdraw(proposal.give.PoolShare) + .then(pmt => E(zoe).offer(toWithdraw, proposal, { PoolShare: pmt })) + .then(seat => E(seat).getPayout('USDC')); + const amt = await E(usdcPurse).deposit(usdcPmt); + t.log(name, 'withdraw payout', ...logAmt(amt)); + t.true(isGTE(amt, proposal.want.USDC)); + // min() in case things changed between checking metrics and withdrawing + investment = subtract(investment, min(amt, investment)); + return amt; + }, }); - const lps = { - alice: makeLP('Alice', purseOf(60n)), - bob: makeLP('Bob', purseOf(50n)), - }; - - await Promise.all([lps.alice.deposit(60n), lps.bob.deposit(40n)]); - - { - t.log('simulate borrow and repay so pool accrues fees'); - const feeTools = makeFeeTools(commonPrivateArgs.feeConfig); - const requestedAmount = usdc.make(50n); - const splits = feeTools.calculateSplit(requestedAmount); - - const amt = await E(creatorFacet).testBorrow({ USDC: splits.Principal }); - t.deepEqual( - amt.USDC, - splits.Principal, - 'testBorrow returns requested amount', - ); - const repayPayments = await deeplyFulfilledObject( - objectMap(splits, utils.pourPayment), - ); - const remaining = await E(creatorFacet).testRepay(splits, repayPayments); - for (const r of Object.values(remaining)) { - t.is(r.value, 0n, 'testRepay consumes all payments'); - } - } - await Promise.all([lps.alice.withdraw(0.2), lps.bob.withdraw(0.8)]); -}); + return me; +}; -test('LP borrow', async t => { +test.skip('LP borrow - TODO: move to exo test', async t => { const common = await commonSetup(t); const { brands: { usdc }, + utils, } = common; - const { instance, creatorFacet, publicFacet, zoe } = + const { instance, creatorFacet, zoe, metricsSub, terms } = await startContract(common); - const terms = await E(zoe).getTerms(instance); - const { subscriber } = E.get( - E.get(E(publicFacet).getPublicTopics()).poolMetrics, - ); - - const { makeLP, purseOf } = makeLpTools(t, common, { - publicFacet, - subscriber, - terms, - zoe, - }); + const usdcPurse = purseOf(terms.issuers.USDC, utils); const lps = { - alice: makeLP('Alice', purseOf(100n)), + alice: makeLP('Alice', usdcPurse(100n), zoe, instance), }; // seed pool with funds - await lps.alice.deposit(100n); + await E(lps.alice).deposit(t, 100n); - const { value } = await E(subscriber).getUpdateSince(); + const { value } = await E(metricsSub).getUpdateSince(); const { shareWorth, encumberedBalance } = value; const poolSeatAllocation = subtract( subtract(shareWorth.numerator, encumberedBalance), @@ -328,13 +372,13 @@ test('LP borrow', async t => { ); // LPs can still withdraw (contract did not shutdown) - await lps.alice.withdraw(0.5); + await E(lps.alice).withdraw(t, 0.5); const amt = await E(creatorFacet).testBorrow({ USDC: usdc.make(30n) }); t.deepEqual(amt, { USDC: usdc.make(30n) }, 'borrow succeeds'); await eventLoopIteration(); - t.like(await E(subscriber).getUpdateSince(), { + t.like(await E(metricsSub).getUpdateSince(), { value: { encumberedBalance: { value: 30n, @@ -349,7 +393,7 @@ test('LP borrow', async t => { }); }); -test('LP repay', async t => { +test.skip('LP repay - TODO: move to exo test', async t => { const common = await commonSetup(t); const { commonPrivateArgs, @@ -357,29 +401,18 @@ test('LP repay', async t => { utils, } = common; - const { instance, creatorFacet, publicFacet, zoe } = + const { instance, creatorFacet, zoe, metricsSub, terms } = 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 usdcPurse = purseOf(terms.issuers.USDC, utils); const lps = { - alice: makeLP('Alice', purseOf(100n)), + alice: makeLP('Alice', usdcPurse(100n), zoe, instance), }; // seed pool with funds - await lps.alice.deposit(100n); + await E(lps.alice).deposit(t, 100n); // borrow funds from pool to increase encumbered balance await E(creatorFacet).testBorrow({ USDC: usdc.make(50n) }); - + const feeTools = makeFeeTools(commonPrivateArgs.feeConfig); { t.log('cannot repay more than encumbered balance'); const repayAmounts = feeTools.calculateSplit(usdc.make(100n)); @@ -484,7 +517,7 @@ test('LP repay', async t => { } await eventLoopIteration(); - t.like(await E(subscriber).getUpdateSince(), { + t.like(await E(metricsSub).getUpdateSince(), { value: { encumberedBalance: { value: 0n, @@ -510,115 +543,458 @@ test('LP repay', async t => { }); // LPs can still withdraw (contract did not shutdown) - await lps.alice.withdraw(1); + await E(lps.alice).withdraw(t, 1); }); -test('baggage', async t => { - const { - brands: { usdc }, - commonPrivateArgs, - } = await commonSetup(t); - - let contractBaggage; - const setJig = ({ baggage }) => { - contractBaggage = baggage; +const makeEVM = (template = MockCctpTxEvidences.AGORIC_PLUS_OSMO()) => { + const [settleAddr] = template.aux.recipientAddress.split('?'); + let nonce = 0; + + const makeTx = (amount: bigint, recipientAddress: string): CctpTxEvidence => { + nonce += 1; + + const tx: CctpTxEvidence = harden({ + ...template, + txHash: `0x00000${nonce}`, + blockNumber: template.blockNumber + BigInt(nonce), + blockTimestamp: template.blockTimestamp + BigInt(nonce * 3), + tx: { ...template.tx, amount }, + // KLUDGE: CCTP doesn't know about aux; it would be added by OCW + aux: { ...template.aux, recipientAddress }, + }); + return tx; }; - const { zoe, bundleAndInstall } = await setUpZoeForTest({ setJig }); - const installation: Installation = - await bundleAndInstall(contractFile); + const txPub = makePublishKit(); - await E(zoe).startInstance( - installation, - { USDC: usdc.issuer }, - { - usdcDenom: 'ibc/usdconagoric', + return harden({ cctp: { makeTx }, txPub }); +}; + +const makeCustomer = ( + who: string, + cctp: ReturnType['cctp'], + txPublisher: Publisher, + feeConfig: FeeConfig, // TODO: get from vstorage (or at least: a subscriber) +) => { + const USDC = feeConfig.flat.brand; + const feeTools = makeFeeTools(feeConfig); + const sent = [] as CctpTxEvidence[]; + + // TODO: get settlerAddr from vstorage + const [settleAddr] = + MockCctpTxEvidences.AGORIC_PLUS_OSMO().aux.recipientAddress.split('?'); + + const me = harden({ + checkPoolAvailable: async ( + t: ExecutionContext, + want: NatValue, + metricsSub: ERef>, + ) => { + const { value: m } = await E(metricsSub).getUpdateSince(); + const { numerator: poolBalance } = m.shareWorth; // XXX awkward API? + const enough = poolBalance.value > want; + t.log(who, 'sees', poolBalance.value, enough ? '>' : 'NOT >', want); + return enough; }, - commonPrivateArgs, - ); + sendFast: async (t: ExecutionContext, amount: bigint, EUD: string) => { + const recipientAddress = `${settleAddr}?EUD=${EUD}`; + // KLUDGE: UI would ask noble for a forwardingAddress + // "cctp" here has some noble stuff mixed in. + const tx = cctp.makeTx(amount, recipientAddress); + t.log(who, 'signs CCTP for', amount, 'uusdc w/EUD:', EUD); + txPublisher.publish(tx); + sent.push(tx); + await eventLoopIteration(); + return tx; + }, + checkSent: ( + t: ExecutionContext, + { bank = [] as any[], local = [] as any[] } = {}, + forward?: unknown, + ) => { + const evidence = sent.shift(); + if (!evidence) throw t.fail('nothing sent'); + + // C3 - Contract MUST calculate AdvanceAmount by ... + // Mostly, see unit tests for calculateAdvance, calculateSplit + const toReceive = forward + ? { value: evidence.tx.amount } + : feeTools.calculateAdvance(AmountMath.make(USDC, evidence.tx.amount)); + + if (forward) { + t.log(who, 'waits for fallback / forward'); + t.deepEqual(bank, []); // no vbank GIVE / GRAB + } + + const { EUD } = addressTools.getQueryParams( + evidence.aux.recipientAddress, + ); - const tree = inspectMapStore(contractBaggage); - t.snapshot(tree, 'contract baggage after start'); -}); + const myMsg = local.find(lm => { + if (lm.type !== 'VLOCALCHAIN_EXECUTE_TX') return false; + const [ibcTransferMsg] = lm.messages; + return ( + ibcTransferMsg['@type'] === + '/ibc.applications.transfer.v1.MsgTransfer' && + ibcTransferMsg.receiver === EUD + ); + }); + if (!myMsg) { + if (forward) return; + throw t.fail(`no MsgTransfer to ${EUD}`); + } + const [ibcTransferMsg] = myMsg.messages; + // C4 - Contract MUST release funds to the end user destination address + // in response to invocation by the off-chain watcher that + // an acceptable Fast USDC Transaction has been initiated. + t.deepEqual( + ibcTransferMsg.token, + { amount: String(toReceive.value), denom: uusdcOnAgoric }, + 'C4', + ); -test('advancing happy path', async t => { - const common = await commonSetup(t); - const { - brands: { usdc }, - commonPrivateArgs, - utils: { inspectLocalBridge, inspectBankBridge, transmitTransferAck }, - } = common; + t.log(who, 'sees', ibcTransferMsg.token, 'sent to', EUD); + // TODO #10445 expect PFM memo + t.is(ibcTransferMsg.memo, '', 'TODO expecting PFM memo'); - 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, + // TODO #10445 expect routing through noble, not osmosis + t.is( + ibcTransferMsg.sourceChannel, + fetchedChainInfo.agoric.connections['osmosis-1'].transferChannel + .channelId, + 'TODO expecting routing through Noble', + ); + }, }); + return me; +}; - 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', +test.serial('OCW operators redeem invitations and start watching', async t => { + const { + startKit: { zoe, invitations }, + evm: { txPub }, + sync, + } = t.context; + const operators = await Promise.all( + invitations.operator.map(async opInv => { + const op = makeOracleOperator(opInv, txPub.subscriber, zoe, t); + await E(op).watch(); + return op; + }), ); + sync.ocw.resolve(operators); +}); - // calculate advance net of fees - const expectedAdvance = feeTools.calculateAdvance( - usdc.make(evidence.tx.amount), - ); - t.log('Expecting to observe advance of', expectedAdvance); +// XXX: replace test.serial() with promise synchronization? + +test.serial('C25 - LPs can deposit USDC', async t => { + const { + startKit: { zoe, instance, metricsSub }, + common: { + utils, + brands: { usdc }, + }, + sync, + } = t.context; + const usdcPurse = purseOf(usdc.issuer, utils); + // C25 - MUST support multiple liquidity providers + const lp = { + lp50: makeLP('Logan', usdcPurse(50_000_000n), zoe, instance), + lp200: makeLP('Larry', usdcPurse(200_000_000n), zoe, instance), + }; - await eventLoopIteration(); // let Advancer do work + const { + value: { + shareWorth: { numerator: balance0 }, + }, + } = await E(metricsSub).getUpdateSince(); + + await Promise.all([ + E(lp.lp200).deposit(t, 200_000_000n), + E(lp.lp50).deposit(t, 50_000_000n), + ]); + + sync.lp.resolve(lp); + const { + value: { + shareWorth: { numerator: poolBalance }, + }, + } = await E(metricsSub).getUpdateSince(); + t.deepEqual(poolBalance, make(usdc.brand, 250_000_000n + balance0.value)); +}); - // advance sent from PoolSeat to PoolAccount +test.serial('STORY01: advancing happy path for 100 USDC', async t => { + const { + common: { + brands: { usdc }, + commonPrivateArgs: { feeConfig }, + utils: { inspectBankBridge, transmitTransferAck }, + }, + evm: { cctp, txPub }, + startKit: { metricsSub }, + bridges: { snapshot, since }, + mint, + } = t.context; + const cust1 = makeCustomer('Carl', cctp, txPub.publisher, feeConfig); + + const bridgePos = snapshot(); + const sent1 = await cust1.sendFast(t, 108_000_000n, 'osmo1234advanceHappy'); + await transmitTransferAck(); // ack IBC transfer for advance + // 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. + + const { calculateAdvance, calculateSplit } = makeFeeTools(feeConfig); + const expectedAdvance = calculateAdvance(usdc.make(sent1.tx.amount)); + t.log('advancer sent to PoolAccount', expectedAdvance); t.deepEqual(inspectBankBridge().at(-1), { amount: String(expectedAdvance.value), - denom: 'ibc/usdconagoric', + denom: uusdcOnAgoric, 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'); + cust1.checkSent(t, since(bridgePos)); - 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', - }); + const emptyMetrics = { + encumberedBalance: usdc.makeEmpty(), + shareWorth: { + numerator: usdc.make(1n), + denominator: { value: 1n }, + }, + totalBorrows: usdc.makeEmpty(), + totalContractFees: usdc.makeEmpty(), + totalPoolFees: usdc.makeEmpty(), + totalRepays: usdc.makeEmpty(), + }; + const par250 = { + numerator: usdc.make(250_000_001n), + denominator: { value: 250_000_001n }, + }; - // TODO #10445 expect PFM memo - t.is(ibcTransferMsg.memo, '', 'TODO expecting PFM memo'); + t.like( + await E(metricsSub) + .getUpdateSince() + .then(r => r.value), + { + ...emptyMetrics, + encumberedBalance: expectedAdvance, + shareWorth: par250, + totalBorrows: expectedAdvance, + }, + 'metrics while advancing', + ); - // 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 mint(sent1); + + // C8 - "Contract MUST be able to initialize settlement process when Noble mints USDC." + // The metrics are a useful proxy, but the contract could lie. + // The real test of whether the contract turns minted funds into liquidity is + // the ability to advance the funds (in later tests). + const split = calculateSplit(usdc.make(sent1.tx.amount)); + t.like( + await E(metricsSub) + .getUpdateSince() + .then(r => r.value), + { + ...emptyMetrics, + shareWorth: { + ...par250, + numerator: add(par250.numerator, split.PoolFee), + }, + totalBorrows: { value: 105839999n }, + totalContractFees: { value: 432000n }, + totalPoolFees: { value: 1728001n }, + totalRepays: { value: 105839999n }, + }, + 'metrics after advancing', ); +}); + +// most likely in exo unit tests +test.todo( + 'C21 - Contract MUST log / timestamp each step in the transaction flow', +); + +test.serial('STORY03: see accounting metrics', async t => { + const { + common: { + brands: { usdc }, + }, + startKit: { metricsSub }, + } = t.context; + const { value: metrics } = await E(metricsSub).getUpdateSince(); + t.log(metrics); + t.true(matches(metrics, PoolMetricsShape)); +}); +test.todo('document metrics storage schema'); +test.todo('get metrics from vstorage'); + +test.serial('STORY05: LP collects fees on 100 USDC', async t => { + const { + sync, + common: { + brands: { usdc }, + }, + } = t.context; + + const lp = await sync.lp.promise; + const got = await E(lp.lp200).withdraw(t, 0.5); // redeem 1/2 my shares + + // C3 - Contract MUST calculate ... + // Mostly, see unit tests for calculateAdvance, calculateSplit + // TODO: add a feeTools unit test for the magic number below. + t.deepEqual(got, add(usdc.units(100), usdc.make(691_200n))); + + await E(lp.lp200).deposit(t, 100_000_000n); // put all but the fees back in +}); + +test.serial('With 250 available, 3 race to get ~100', async t => { + const { + bridges: { snapshot, since }, + evm: { cctp, txPub }, + common: { + commonPrivateArgs: { feeConfig }, + utils: { transmitTransferAck }, + }, + startKit: { metricsSub }, + mint, + } = t.context; + + const cust = { + racer1: makeCustomer('Racer1', cctp, txPub.publisher, feeConfig), + racer2: makeCustomer('Racer2', cctp, txPub.publisher, feeConfig), + racer3: makeCustomer('Racer3', cctp, txPub.publisher, feeConfig), + }; + + await cust.racer3.checkPoolAvailable(t, 125_000_000n, metricsSub); + + const bridgePos = snapshot(); + const [sent1, sent2, sent3] = await Promise.all([ + cust.racer1.sendFast(t, 110_000_000n, 'osmo1234a'), + cust.racer2.sendFast(t, 120_000_000n, 'osmo1234b'), + cust.racer3.sendFast(t, 125_000_000n, 'osmo1234c'), + ]); + cust.racer1.checkSent(t, since(bridgePos)); + cust.racer2.checkSent(t, since(bridgePos)); + // TODO/WIP: cust.racer3.checkSent(t, since(bridgePos), 'forward - LP depleted'); 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. + await transmitTransferAck(); + await transmitTransferAck(); + await Promise.all([mint(sent1), mint(sent2), mint(sent3)]); +}); + +test.serial('STORY05(cont): LPs withdraw all liquidity', async t => { + const { + sync, + common: { + brands: { usdc }, + }, + } = t.context; + + const lp = await sync.lp.promise; + const [a, b] = await Promise.all([ + E(lp.lp200).withdraw(t, 1), + E(lp.lp50).withdraw(t, 1), + ]); + t.log({ a, b, sum: add(a, b) }); + t.truthy(a); + t.truthy(b); }); + +test.serial('STORY09: insufficient liquidity: no FastUSDC option', async t => { + // STORY09 - As the Fast USDC end user, + // I should see the option to use Fast USDC unavailable + // on the UI (and unusable) if there are not funds in the + // MarketMaker’s account + const { + common: { + commonPrivateArgs: { feeConfig }, + }, + evm: { cctp, txPub }, + startKit: { metricsSub }, + } = t.context; + const early = makeCustomer('Unice', cctp, txPub.publisher, feeConfig); + const available = await early.checkPoolAvailable(t, 5_000_000n, metricsSub); + t.false(available); +}); + +test.serial('C20 - Contract MUST function with an empty pool', async t => { + const { + common: { + commonPrivateArgs: { feeConfig }, + utils: { transmitTransferAck }, + }, + evm: { cctp, txPub }, + startKit: { metricsSub }, + bridges: { snapshot, since }, + mint, + } = t.context; + const custEmpty = makeCustomer('Earl', cctp, txPub.publisher, feeConfig); + const bridgePos = snapshot(); + const sent = await custEmpty.sendFast(t, 150_000_000n, 'osmo123'); + const bridgeTraffic = since(bridgePos); + await mint(sent); + custEmpty.checkSent(t, bridgeTraffic, 'forward'); + t.log('No advancement, just settlement'); + await transmitTransferAck(); // ack IBC transfer for forward +}); + +// advancedEarly stuff +test.todo( + 'C12 - Contract MUST only pay back the Pool only if they started the advance before USDC is minted', +); + +test.todo('C18 - forward - MUST log and alert these incidents'); + +test.serial('Settlement for unknown transaction (operator down)', async t => { + const { + sync, + bridges: { snapshot, since }, + evm: { cctp, txPub }, + common: { + commonPrivateArgs: { feeConfig }, + utils: { transmitTransferAck }, + }, + mint, + } = t.context; + const operators = await sync.ocw.promise; + + const opDown = makeCustomer('Otto', cctp, txPub.publisher, feeConfig); + + // what removeOperator will do + await E(E.get(E(operators[1]).getKit()).admin).disable(); + const bridgePos = snapshot(); + const sent = await opDown.sendFast(t, 20_000_000n, 'osmo12345'); + await mint(sent); + const bridgeTraffic = since(bridgePos); + + t.like( + bridgeTraffic.bank, + [ + { + amount: '20000000', + sender: 'faucet', + type: 'VBANK_GRAB', + }, + { + amount: '20000000', + recipient: 'agoric1fakeLCAAddress1', + type: 'VBANK_GIVE', + }, + ], + '20 USDC arrive at the settlement account', + ); + t.deepEqual(bridgeTraffic.local, [], 'no IBC transfers'); + + await transmitTransferAck(); + t.deepEqual(await E(operators[1]).getFailures(), [ + 'submitEvidence for disabled operator', + ]); +}); + +test.todo( + 'fee levels MUST be visible to external parties - i.e., written to public storage', +); 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 ec82b58c800..42cfa097928 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 @@ -32,8 +32,12 @@ Generated by [AVA](https://avajs.dev). ChainHub_singleton: 'Alleged: ChainHub', bech32PrefixToChainName: { agoric: 'agoric', + noble: 'noble', + osmo: 'osmosis', + }, + brandDenom: { + 'Alleged: USDC brand': 'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9', }, - brandDenom: {}, chainInfos: { agoric: { bech32Prefix: 'agoric', @@ -45,9 +49,512 @@ Generated by [AVA](https://avajs.dev). }, ], }, + noble: { + bech32Prefix: 'noble', + chainId: 'noble-1', + icqEnabled: false, + }, + osmosis: { + bech32Prefix: 'osmo', + chainId: 'osmosis-1', + icqEnabled: true, + stakingTokens: [ + { + denom: 'uosmo', + }, + ], + }, + }, + connectionInfos: { + 'agoric-3_cosmoshub-4': { + client_id: '07-tendermint-6', + counterparty: { + client_id: '07-tendermint-927', + connection_id: 'connection-649', + }, + id: 'connection-8', + state: 3, + transferChannel: { + channelId: 'channel-5', + counterPartyChannelId: 'channel-405', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'agoric-3_noble-1': { + client_id: '07-tendermint-77', + counterparty: { + client_id: '07-tendermint-32', + connection_id: 'connection-40', + }, + id: 'connection-72', + state: 3, + transferChannel: { + channelId: 'channel-62', + counterPartyChannelId: 'channel-21', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'agoric-3_omniflixhub-1': { + client_id: '07-tendermint-73', + counterparty: { + client_id: '07-tendermint-47', + connection_id: 'connection-40', + }, + id: 'connection-67', + state: 3, + transferChannel: { + channelId: 'channel-58', + counterPartyChannelId: 'channel-30', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'agoric-3_osmosis-1': { + client_id: '07-tendermint-1', + counterparty: { + client_id: '07-tendermint-2109', + connection_id: 'connection-1649', + }, + id: 'connection-1', + state: 3, + transferChannel: { + channelId: 'channel-1', + counterPartyChannelId: 'channel-320', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'agoric-3_secret-4': { + client_id: '07-tendermint-17', + counterparty: { + client_id: '07-tendermint-111', + connection_id: 'connection-80', + }, + id: 'connection-17', + state: 3, + transferChannel: { + channelId: 'channel-10', + counterPartyChannelId: 'channel-51', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'agoric-3_stride-1': { + client_id: '07-tendermint-74', + counterparty: { + client_id: '07-tendermint-129', + connection_id: 'connection-118', + }, + id: 'connection-68', + state: 3, + transferChannel: { + channelId: 'channel-59', + counterPartyChannelId: 'channel-148', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'agoric-3_umee-1': { + client_id: '07-tendermint-18', + counterparty: { + client_id: '07-tendermint-152', + connection_id: 'connection-101', + }, + id: 'connection-18', + state: 3, + transferChannel: { + channelId: 'channel-11', + counterPartyChannelId: 'channel-42', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'celestia_osmosis-1': { + client_id: '07-tendermint-10', + counterparty: { + client_id: '07-tendermint-3012', + connection_id: 'connection-2503', + }, + id: 'connection-2', + state: 3, + transferChannel: { + channelId: 'channel-2', + counterPartyChannelId: 'channel-6994', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'cosmoshub-4_noble-1': { + client_id: '07-tendermint-1116', + counterparty: { + client_id: '07-tendermint-4', + connection_id: 'connection-12', + }, + id: 'connection-790', + state: 3, + transferChannel: { + channelId: 'channel-536', + counterPartyChannelId: 'channel-4', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'cosmoshub-4_osmosis-1': { + client_id: '07-tendermint-259', + counterparty: { + client_id: '07-tendermint-1', + connection_id: 'connection-1', + }, + id: 'connection-257', + state: 3, + transferChannel: { + channelId: 'channel-141', + counterPartyChannelId: 'channel-0', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'dydx-mainnet-1_noble-1': { + client_id: '07-tendermint-0', + counterparty: { + client_id: '07-tendermint-59', + connection_id: 'connection-57', + }, + id: 'connection-0', + state: 3, + transferChannel: { + channelId: 'channel-0', + counterPartyChannelId: 'channel-33', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'dydx-mainnet-1_osmosis-1': { + client_id: '07-tendermint-3', + counterparty: { + client_id: '07-tendermint-3009', + connection_id: 'connection-2500', + }, + id: 'connection-7', + state: 3, + transferChannel: { + channelId: 'channel-3', + counterPartyChannelId: 'channel-6787', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'juno-1_noble-1': { + client_id: '07-tendermint-334', + counterparty: { + client_id: '07-tendermint-3', + connection_id: 'connection-8', + }, + id: 'connection-322', + state: 3, + transferChannel: { + channelId: 'channel-224', + counterPartyChannelId: 'channel-3', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'juno-1_osmosis-1': { + client_id: '07-tendermint-0', + counterparty: { + client_id: '07-tendermint-1457', + connection_id: 'connection-1142', + }, + id: 'connection-0', + state: 3, + transferChannel: { + channelId: 'channel-0', + counterPartyChannelId: 'channel-42', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'neutron-1_noble-1': { + client_id: '07-tendermint-40', + counterparty: { + client_id: '07-tendermint-25', + connection_id: 'connection-34', + }, + id: 'connection-31', + state: 3, + transferChannel: { + channelId: 'channel-30', + counterPartyChannelId: 'channel-18', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'neutron-1_osmosis-1': { + client_id: '07-tendermint-19', + counterparty: { + client_id: '07-tendermint-2823', + connection_id: 'connection-2338', + }, + id: 'connection-18', + state: 3, + transferChannel: { + channelId: 'channel-10', + counterPartyChannelId: 'channel-874', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'noble-1_omniflixhub-1': { + client_id: '07-tendermint-68', + counterparty: { + client_id: '07-tendermint-51', + connection_id: 'connection-49', + }, + id: 'connection-65', + state: 3, + transferChannel: { + channelId: 'channel-44', + counterPartyChannelId: 'channel-38', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'noble-1_osmosis-1': { + client_id: '07-tendermint-0', + counterparty: { + client_id: '07-tendermint-2704', + connection_id: 'connection-2241', + }, + id: 'connection-2', + state: 3, + transferChannel: { + channelId: 'channel-1', + counterPartyChannelId: 'channel-750', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'noble-1_secret-4': { + client_id: '07-tendermint-24', + counterparty: { + client_id: '07-tendermint-170', + connection_id: 'connection-127', + }, + id: 'connection-33', + state: 3, + transferChannel: { + channelId: 'channel-17', + counterPartyChannelId: 'channel-88', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'noble-1_stargaze-1': { + client_id: '07-tendermint-16', + counterparty: { + client_id: '07-tendermint-287', + connection_id: 'connection-214', + }, + id: 'connection-25', + state: 3, + transferChannel: { + channelId: 'channel-11', + counterPartyChannelId: 'channel-204', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'noble-1_umee-1': { + client_id: '07-tendermint-73', + counterparty: { + client_id: '07-tendermint-248', + connection_id: 'connection-210', + }, + id: 'connection-74', + state: 3, + transferChannel: { + channelId: 'channel-51', + counterPartyChannelId: 'channel-120', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'omniflixhub-1_osmosis-1': { + client_id: '07-tendermint-8', + counterparty: { + client_id: '07-tendermint-1829', + connection_id: 'connection-1431', + }, + id: 'connection-8', + state: 3, + transferChannel: { + channelId: 'channel-1', + counterPartyChannelId: 'channel-199', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'osmosis-1_secret-4': { + client_id: '07-tendermint-1588', + counterparty: { + client_id: '07-tendermint-2', + connection_id: 'connection-1', + }, + id: 'connection-1244', + state: 3, + transferChannel: { + channelId: 'channel-88', + counterPartyChannelId: 'channel-1', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'osmosis-1_stargaze-1': { + client_id: '07-tendermint-1562', + counterparty: { + client_id: '07-tendermint-0', + connection_id: 'connection-0', + }, + id: 'connection-1223', + state: 3, + transferChannel: { + channelId: 'channel-75', + counterPartyChannelId: 'channel-0', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'osmosis-1_stride-1': { + client_id: '07-tendermint-2119', + counterparty: { + client_id: '07-tendermint-1', + connection_id: 'connection-2', + }, + id: 'connection-1657', + state: 3, + transferChannel: { + channelId: 'channel-326', + counterPartyChannelId: 'channel-5', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + 'osmosis-1_umee-1': { + client_id: '07-tendermint-1805', + counterparty: { + client_id: '07-tendermint-6', + connection_id: 'connection-0', + }, + id: 'connection-1410', + state: 3, + transferChannel: { + channelId: 'channel-184', + counterPartyChannelId: 'channel-0', + counterPartyPortId: 'transfer', + ordering: 0, + portId: 'transfer', + state: 3, + version: 'ics20-1', + }, + }, + }, + denom: { + 'ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4': { + baseDenom: 'uusdc', + baseName: 'noble', + brandKey: undefined, + chainName: 'osmosis', + }, + 'ibc/FE98AAD68F02F03565E9FA39A5E627946699B2B07115889ED812D8BA639576A9': { + baseDenom: 'uusdc', + baseName: 'noble', + brand: Object @Alleged: USDC brand {}, + brandKey: 'USDC', + chainName: 'agoric', + }, + uusdc: { + baseDenom: 'uusdc', + baseName: 'noble', + chainName: 'noble', + }, }, - connectionInfos: {}, - denom: {}, lookupChainInfo_kindHandle: 'Alleged: kind', lookupChainsAndConnection_kindHandle: 'Alleged: kind', lookupConnectionInfo_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 5e8c6efc2ee..dcb9a05cb25 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 e7977de6963..174a28ceed9 100644 --- a/packages/fast-usdc/test/supports.ts +++ b/packages/fast-usdc/test/supports.ts @@ -2,8 +2,16 @@ import { makeIssuerKit } from '@agoric/ertp'; import { VTRANSFER_IBC_EVENT } from '@agoric/internal/src/action-types.js'; import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { + denomHash, + type CosmosChainInfo, + type Denom, +} from '@agoric/orchestration'; import { registerKnownChains } from '@agoric/orchestration/src/chain-info.js'; -import { makeChainHub } from '@agoric/orchestration/src/exos/chain-hub.js'; +import { + makeChainHub, + type DenomDetail, +} from '@agoric/orchestration/src/exos/chain-hub.js'; import { prepareCosmosInterchainService } from '@agoric/orchestration/src/exos/cosmos-interchain-service.js'; import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; import { setupFakeNetwork } from '@agoric/orchestration/test/network-fakes.js'; @@ -35,6 +43,36 @@ export { makeFakeTransferBridge, } from '@agoric/vats/tools/fake-bridge.js'; +const assetOn = ( + baseDenom: Denom, + baseName: string, + chainName?: string, + infoOf?: Record, + brandKey?: string, +): [string, DenomDetail & { brandKey?: string }] => { + if (!chainName) { + return [baseDenom, { baseName, chainName: baseName, baseDenom }]; + } + if (!infoOf) throw Error(`must provide infoOf`); + const issuerInfo = infoOf[baseName]; + const holdingInfo = infoOf[chainName]; + if (!holdingInfo) throw Error(`${chainName} missing`); + if (!holdingInfo.connections) + throw Error(`connections missing for ${chainName}`); + const { channelId } = + holdingInfo.connections[issuerInfo.chainId].transferChannel; + const denom = `ibc/${denomHash({ denom: baseDenom, channelId })}`; + return [denom, { baseName, chainName, baseDenom, brandKey }]; +}; + +export const [uusdcOnAgoric, agUSDCDetail] = assetOn( + 'uusdc', + 'noble', + 'agoric', + fetchedChainInfo, + 'USDC', +); + export const commonSetup = async (t: ExecutionContext) => { t.log('bootstrap vat dependencies'); // The common setup cannot support a durable zone because many of the fakes are not durable. @@ -52,7 +90,7 @@ export const commonSetup = async (t: ExecutionContext) => { onToBridge: obj => bankBridgeMessages.push(obj), }); await E(bankManager).addAsset( - 'ibc/usdconagoric', + uusdcOnAgoric, 'USDC', 'USD Circle Stablecoin', usdc.issuerKit, @@ -64,7 +102,7 @@ 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( - 'ibc/usdconagoric', + uusdcOnAgoric, /** @type {AssetInfo} */ harden({ brand: usdc.brand, issuer: usdc.issuer, @@ -157,6 +195,19 @@ export const commonSetup = async (t: ExecutionContext) => { vowTools, ); + const chainInfo = harden(() => { + const { agoric, osmosis, noble } = fetchedChainInfo; + return { agoric, osmosis, noble }; + })(); + + const assetInfo = harden( + Object.fromEntries([ + assetOn('uusdc', 'noble'), + [uusdcOnAgoric, agUSDCDetail], + assetOn('uusdc', 'noble', 'osmosis', fetchedChainInfo), + ]), + ); + return { bootstrap: { agoricNames, @@ -186,8 +237,8 @@ export const commonSetup = async (t: ExecutionContext) => { marshaller, timerService: timer, feeConfig: makeTestFeeConfig(usdc), - chainInfo: {}, - assetInfo: {}, + chainInfo, + assetInfo, }, facadeServices: { agoricNames, diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index a782294256c..39cd3626d83 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -98,7 +98,7 @@ export const connectionKey = (chainId1, chainId2) => { */ const reverseConnInfo = connInfo => { const { transferChannel } = connInfo; - return { + return harden({ id: connInfo.counterparty.connection_id, client_id: connInfo.counterparty.client_id, counterparty: { @@ -113,7 +113,7 @@ const reverseConnInfo = connInfo => { portId: transferChannel.counterPartyPortId, counterPartyPortId: transferChannel.portId, }, - }; + }); }; /** diff --git a/packages/orchestration/src/utils/chain-hub-helper.js b/packages/orchestration/src/utils/chain-hub-helper.js index be01c8b3ef7..9e75248c6e3 100644 --- a/packages/orchestration/src/utils/chain-hub-helper.js +++ b/packages/orchestration/src/utils/chain-hub-helper.js @@ -18,8 +18,8 @@ export const registerChainsAndAssets = ( chainInfo, assetInfo, ) => { + console.log('chainHub: registering chains', Object.keys(chainInfo || {})); if (!chainInfo) { - console.log('No chain info provided, returning early.'); return; } @@ -32,16 +32,17 @@ export const registerChainsAndAssets = ( const registeredPairs = new Set(); for (const [pChainId, connInfos] of Object.entries(conns)) { for (const [cChainId, connInfo] of Object.entries(connInfos)) { - const pair = [pChainId, cChainId].sort().join(''); + const pair = [pChainId, cChainId].sort().join('<->'); if (!registeredPairs.has(pair)) { chainHub.registerConnection(pChainId, cChainId, connInfo); registeredPairs.add(pair); } } } + console.log('chainHub: registered connections', [...registeredPairs].sort()); + console.log('chainHub: registering assets', Object.keys(assetInfo || {})); if (!assetInfo) { - console.log('No asset info provided, returning early.'); return; } for (const [denom, info] of Object.entries(assetInfo)) {