diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index 76c5d152c59..0de187aa734 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -378,7 +378,7 @@ test.serial('basic-flows', async t => { [ 'request-coa', { - account: 'published.basicFlows.cosmos1test', + account: 'published.basicFlows.cosmos1test1', }, ], ], @@ -386,11 +386,11 @@ test.serial('basic-flows', async t => { t.like(wd.getLatestUpdateRecord(), { status: { id: 'request-coa', numWantsSatisfied: 1 }, }); - t.deepEqual(readPublished('basicFlows.cosmos1test'), { + t.deepEqual(readPublished('basicFlows.cosmos1test1'), { localAddress: - '/ibc-port/icacontroller-2/ordered/{"version":"ics27-1","controllerConnectionId":"connection-8","hostConnectionId":"connection-649","address":"cosmos1test","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-2', + '/ibc-port/icacontroller-2/ordered/{"version":"ics27-1","controllerConnectionId":"connection-8","hostConnectionId":"connection-649","address":"cosmos1test1","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-2', remoteAddress: - '/ibc-hop/connection-8/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-8","hostConnectionId":"connection-649","address":"cosmos1test","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-2', + '/ibc-hop/connection-8/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-8","hostConnectionId":"connection-649","address":"cosmos1test1","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-2', }); // create a local orchestration account @@ -599,9 +599,8 @@ test.serial('basic-flows - portfolio holder', async t => { 'request-portfolio-acct', { agoric: 'published.basicFlows.agoric1fakeLCAAddress1', - cosmoshub: 'published.basicFlows.cosmos1test', - // XXX support multiple chain addresses in ibc mocks - osmosis: 'published.basicFlows.cosmos1test', + cosmoshub: 'published.basicFlows.cosmos1test2', + osmosis: 'published.basicFlows.cosmos1test3', }, ], ], @@ -609,15 +608,14 @@ test.serial('basic-flows - portfolio holder', async t => { t.like(wd.getLatestUpdateRecord(), { status: { id: 'request-portfolio-acct', numWantsSatisfied: 1 }, }); - // XXX this overrides a previous account, since mocks only provide one address - t.deepEqual(readPublished('basicFlows.cosmos1test'), { + + t.deepEqual(readPublished('basicFlows.cosmos1test3'), { localAddress: - '/ibc-port/icacontroller-4/ordered/{"version":"ics27-1","controllerConnectionId":"connection-1","hostConnectionId":"connection-1649","address":"cosmos1test","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-4', + '/ibc-port/icacontroller-4/ordered/{"version":"ics27-1","controllerConnectionId":"connection-1","hostConnectionId":"connection-1649","address":"cosmos1test3","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-4', remoteAddress: - '/ibc-hop/connection-1/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-1","hostConnectionId":"connection-1649","address":"cosmos1test","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-4', + '/ibc-hop/connection-1/ibc-port/icahost/ordered/{"version":"ics27-1","controllerConnectionId":"connection-1","hostConnectionId":"connection-1649","address":"cosmos1test3","encoding":"proto3","txType":"sdk_multi_msg"}/ibc-channel/channel-4', }); - // XXX this overrides a previous account, since mocks only provide one address - t.is(readPublished('basicFlows.agoric1fakeLCAAddress'), ''); + t.is(readPublished('basicFlows.agoric1fakeLCAAddress1'), ''); const { BLD } = agoricNamesRemotes.brand; BLD || Fail`BLD missing from agoricNames`; diff --git a/packages/boot/test/fast-usdc/fast-usdc.test.ts b/packages/boot/test/fast-usdc/fast-usdc.test.ts index d01a564e49c..60f4e5ccb43 100644 --- a/packages/boot/test/fast-usdc/fast-usdc.test.ts +++ b/packages/boot/test/fast-usdc/fast-usdc.test.ts @@ -8,6 +8,7 @@ import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js'; import { makeMarshal } from '@endo/marshal'; import { defaultMarshaller } from '@agoric/internal/src/storage-test-utils.js'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { BridgeId } from '@agoric/internal'; import { makeWalletFactoryContext, type WalletFactoryTestContext, @@ -15,6 +16,7 @@ import { import { makeSwingsetHarness, insistManagerType, + AckBehavior, } from '../../tools/supports.js'; const test: TestFn< @@ -54,8 +56,9 @@ test.serial( async t => { const { agoricNamesRemotes, - evalProposal, + bridgeUtils, buildProposal, + evalProposal, refreshAgoricNamesRemotes, storage, walletFactoryDriver: wd, @@ -67,6 +70,15 @@ test.serial( wd.provideSmartWallet('agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj'), ]); + // inbound `startChannelOpenInit` responses immediately. + // needed since the Fusdc StartFn relies on an ICA being created + bridgeUtils.setAckBehavior( + BridgeId.DIBC, + 'startChannelOpenInit', + AckBehavior.Immediate, + ); + bridgeUtils.setBech32Prefix('noble'); + const materials = buildProposal( '@agoric/builders/scripts/fast-usdc/init-fast-usdc.js', ['--net', 'MAINNET'], diff --git a/packages/boot/tools/ibc/mocks.ts b/packages/boot/tools/ibc/mocks.ts index 824768df9d6..57385252a77 100644 --- a/packages/boot/tools/ibc/mocks.ts +++ b/packages/boot/tools/ibc/mocks.ts @@ -1,5 +1,6 @@ // @ts-check import { createMockAckMap } from '@agoric/orchestration/tools/ibc-mocks.js'; +import type { IBCChannelID, IBCEvent, IBCMethod } from '@agoric/vats'; /** @import { IBCChannelID, IBCMethod, IBCEvent } from '@agoric/vats'; */ @@ -65,6 +66,11 @@ export const protoMsgMocks = { msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ25zS0tTOXBZbU11WVhCd2JHbGpZWFJwYjI1ekxuUnlZVzV6Wm1WeUxuWXhMazF6WjFSeVlXNXpabVZ5RWs0S0NIUnlZVzV6Wm1WeUVndGphR0Z1Ym1Wc0xUVXpOaG9UQ2cxcFltTXZkWFZ6WkdOb1lYTm9FZ0l4TUNJTFkyOXpiVzl6TVhSbGMzUXFDbTV2WW14bE1YUmxjM1F5QURpQThKTEwzUWc9IiwibWVtbyI6IiJ9', ack: responses.ibcTransfer, }, + // MsgTransfer 10 ibc/uusdchash from cosmos1test1 to noble1test through channel-536 + ibcTransfer2: { + msg: 'eyJ0eXBlIjoxLCJkYXRhIjoiQ253S0tTOXBZbU11WVhCd2JHbGpZWFJwYjI1ekxuUnlZVzV6Wm1WeUxuWXhMazF6WjFSeVlXNXpabVZ5RWs4S0NIUnlZVzV6Wm1WeUVndGphR0Z1Ym1Wc0xUVXpOaG9UQ2cxcFltTXZkWFZ6WkdOb1lYTm9FZ0l4TUNJTVkyOXpiVzl6TVhSbGMzUXhLZ3B1YjJKc1pURjBaWE4wTWdBNGdQQ1N5OTBJIiwibWVtbyI6IiJ9', + ack: responses.ibcTransfer, + }, error: { msg: '', ack: responses.error5, @@ -101,17 +107,18 @@ export const addParamsIfJsonVersion = (version, params) => { export const icaMocks = { /** * ICA Channel Creation - * @param {IBCMethod<'startChannelOpenInit'>} obj - * @returns {IBCEvent<'channelOpenAck'>} + * @param obj + * @param bech32Prefix */ - channelOpenAck: obj => { + channelOpenAck: ( + obj: IBCMethod<'startChannelOpenInit'>, + bech32Prefix: string = 'cosmos', + ): IBCEvent<'channelOpenAck'> => { // Fake a channel IDs from port suffixes. _Ports have no relation to channels, and hosts // and controllers will likely have different channel IDs for the same channel._ const mocklID = Number(obj.packet.source_port.split('-').at(-1)); - /** @type {IBCChannelID} */ - const mockLocalChannelID = `channel-${mocklID}`; - /** @type {IBCChannelID} */ - const mockRemoteChannelID = `channel-${mocklID}`; + const mockLocalChannelID: IBCChannelID = `channel-${mocklID}`; + const mockRemoteChannelID: IBCChannelID = `channel-${mocklID}`; return { type: 'IBC_EVENT', @@ -125,8 +132,8 @@ export const icaMocks = { channel_id: mockRemoteChannelID, }, counterpartyVersion: addParamsIfJsonVersion(obj.version, { - // TODO, parameterize - address: 'cosmos1test', + // mockID expected to increase monotonically since icacontroller ports are sequential + address: `${bech32Prefix}1test${mocklID < 2 ? '' : mocklID - 1}`, }), connectionHops: obj.hops, order: obj.order, diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index 0be39b19fa0..77e1cdc61ca 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -44,7 +44,7 @@ import type { ExecutionContext as AvaT } from 'ava'; import type { CoreEvalSDKType } from '@agoric/cosmic-proto/swingset/swingset.js'; import type { EconomyBootstrapPowers } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; import type { SwingsetController } from '@agoric/swingset-vat/src/controller/controller.js'; -import type { BridgeHandler, IBCMethod } from '@agoric/vats'; +import type { BridgeHandler, IBCDowncallMethod, IBCMethod } from '@agoric/vats'; import type { BootstrapRootObject } from '@agoric/vats/src/core/lib-boot.js'; import type { EProxy } from '@endo/eventual-send'; import type { FastUSDCCorePowers } from '@agoric/fast-usdc/src/fast-usdc.start.js'; @@ -289,6 +289,14 @@ export const matchIter = (t: AvaT, iter, valueRef) => { matchValue(t, iter.value, valueRef); }; +export const AckBehavior = { + /** inbound responses are queued. use `flushInboundQueue()` to simulate the remote response */ + Queued: 'QUEUED', + /** inbound messages are delivered immediately */ + Immediate: 'IMMEDIATE', +} as const; +type AckBehaviorType = (typeof AckBehavior)[keyof typeof AckBehavior]; + /** * Start a SwingSet kernel to be used by tests and benchmarks. * @@ -365,7 +373,30 @@ export const makeSwingsetTestKit = async ( console.log('inbound', ...args); bridgeInbound!(...args); }; + /** + * Config DIBC bridge behavior. + * Defaults to `Queued` unless specified. + * Current only configured for `channelOpenInit` but can be + * extended to support `sendPacket`. + */ + const ackBehaviors: Partial< + Record>> + > = { + [BridgeId.DIBC]: { + startChannelOpenInit: AckBehavior.Queued, + }, + }; + + const shouldAckImmediately = ( + bridgeId: BridgeId, + method: IBCDowncallMethod, + ) => ackBehaviors?.[bridgeId]?.[method] === AckBehavior.Immediate; + /** + * configurable `bech32Prefix` for DIBC bridge + * messages that involve creating an ICA. + */ + let bech32Prefix = 'cosmos'; /** * Adds the sequence so the bridge knows what response to connect it to. * Then queue it send it over the bridge over this returns. @@ -404,7 +435,7 @@ export const makeSwingsetTestKit = async ( * Mock the bridge outbound handler. The real one is implemented in Golang so * changes there will sometimes require changes here. */ - const bridgeOutbound = (bridgeId: string, obj: any) => { + const bridgeOutbound = (bridgeId: BridgeId, obj: any) => { // store all messages for querying by tests if (!outboundMessages.has(bridgeId)) { outboundMessages.set(bridgeId, []); @@ -476,9 +507,17 @@ export const makeSwingsetTestKit = async ( case `${BridgeId.DIBC}:IBC_METHOD`: case `${BridgeId.VTRANSFER}:IBC_METHOD`: { switch (obj.method) { - case 'startChannelOpenInit': - pushInbound(BridgeId.DIBC, icaMocks.channelOpenAck(obj)); + case 'startChannelOpenInit': { + const message = icaMocks.channelOpenAck(obj, bech32Prefix); + const handle = shouldAckImmediately( + bridgeId, + 'startChannelOpenInit', + ) + ? inbound + : pushInbound; + handle(BridgeId.DIBC, message); return undefined; + } case 'sendPacket': { if (protoMsgMockMap[obj.packet.data]) { return ackLater(obj, protoMsgMockMap[obj.packet.data]); @@ -629,6 +668,27 @@ export const makeSwingsetTestKit = async ( getOutboundMessages: (bridgeId: string) => harden([...outboundMessages.get(bridgeId)]), getInboundQueueLength: () => inboundQueue.length, + setAckBehavior( + bridgeId: BridgeId, + method: IBCDowncallMethod, + behavior: AckBehaviorType, + ): void { + if (!ackBehaviors?.[bridgeId]?.[method]) + throw Fail`ack behavior not yet configurable for ${bridgeId} ${method}`; + console.log('setting', bridgeId, method, 'ack behavior to', behavior); + ackBehaviors[bridgeId][method] = behavior; + }, + lookupAckBehavior( + bridgeId: BridgeId, + method: IBCDowncallMethod, + ): AckBehaviorType { + if (!ackBehaviors?.[bridgeId]?.[method]) + throw Fail`ack behavior not yet configurable for ${bridgeId} ${method}`; + return ackBehaviors[bridgeId][method]; + }, + setBech32Prefix(prefix: string): void { + bech32Prefix = prefix; + }, /** * @param {number} max the max number of messages to flush * @returns {Promise} the number of messages flushed diff --git a/packages/builders/scripts/inter-protocol/updatePriceFeeds.js b/packages/builders/scripts/inter-protocol/updatePriceFeeds.js index 6b5f0c2e442..f5e71a6c137 100644 --- a/packages/builders/scripts/inter-protocol/updatePriceFeeds.js +++ b/packages/builders/scripts/inter-protocol/updatePriceFeeds.js @@ -32,7 +32,7 @@ const configurations = { 'agoric1qj07c7vfk3knqdral0sej7fa6eavkdn8vd8etf', // Simply Staking 'agoric10vjkvkmpp9e356xeh6qqlhrny2htyzp8hf88fk', // P2P ], - inBrandNames: ['ATOM', 'stTIA', 'stkATOM'], + inBrandNames: ['ATOM', 'stTIA', 'stkATOM', 'dATOM'], }, EMERYNET: { oracleAddresses: [ diff --git a/packages/builders/test/snapshots/orchestration-imports.test.js.md b/packages/builders/test/snapshots/orchestration-imports.test.js.md index 247866d9e3b..3639fcb2066 100644 --- a/packages/builders/test/snapshots/orchestration-imports.test.js.md +++ b/packages/builders/test/snapshots/orchestration-imports.test.js.md @@ -322,6 +322,17 @@ Generated by [AVA](https://avajs.dev). payload: [ {}, { + intermediateRecipient: { + chainId: Object @match:string { + payload: [], + }, + encoding: Object @match:string { + payload: [], + }, + value: Object @match:string { + payload: [], + }, + }, retries: Object @match:kind { payload: 'number', }, @@ -423,6 +434,17 @@ Generated by [AVA](https://avajs.dev). payload: [ {}, { + intermediateRecipient: { + chainId: Object @match:string { + payload: [], + }, + encoding: Object @match:string { + payload: [], + }, + value: Object @match:string { + payload: [], + }, + }, retries: Object @match:kind { payload: 'number', }, diff --git a/packages/builders/test/snapshots/orchestration-imports.test.js.snap b/packages/builders/test/snapshots/orchestration-imports.test.js.snap index f1588bc482d..dc356ca0ef5 100644 Binary files a/packages/builders/test/snapshots/orchestration-imports.test.js.snap and b/packages/builders/test/snapshots/orchestration-imports.test.js.snap differ diff --git a/packages/fast-usdc/src/cli/operator-commands.js b/packages/fast-usdc/src/cli/operator-commands.js index 06a3fdc01df..5bb19882232 100644 --- a/packages/fast-usdc/src/cli/operator-commands.js +++ b/packages/fast-usdc/src/cli/operator-commands.js @@ -85,7 +85,6 @@ export const addOperatorCommands = ( .requiredOption('--recipientAddress ', 'bech32 address', String) .requiredOption('--blockHash <0xhex>', 'hex hash', parseHex) .requiredOption('--blockNumber ', 'number', parseNat) - .requiredOption('--blockTimestamp ', 'number', parseNat) .requiredOption('--chainId ', 'chain id', Number) .requiredOption('--amount ', 'number', parseNat) .requiredOption('--forwardingAddress ', 'bech32 address', String) diff --git a/packages/fast-usdc/src/exos/advancer.js b/packages/fast-usdc/src/exos/advancer.js index f5a9f8afd09..f244ced3884 100644 --- a/packages/fast-usdc/src/exos/advancer.js +++ b/packages/fast-usdc/src/exos/advancer.js @@ -116,6 +116,7 @@ export const prepareAdvancerKit = ( * notifyFacet: import('./settler.js').SettlerKit['notify']; * borrowerFacet: LiquidityPoolKit['borrower']; * poolAccount: HostInterface>; + * intermediateRecipient: ChainAddress; * }} config */ config => harden(config), @@ -187,12 +188,20 @@ export const prepareAdvancerKit = ( * @param {AdvancerVowCtx & { tmpSeat: ZCFSeat }} ctx */ onFulfilled(result, ctx) { - const { poolAccount } = this.state; + const { poolAccount, intermediateRecipient } = this.state; const { destination, advanceAmount, ...detail } = ctx; - const transferV = E(poolAccount).transfer(destination, { - denom: usdc.denom, - value: advanceAmount.value, - }); + const transferV = E(poolAccount).transfer( + destination, + { + denom: usdc.denom, + value: advanceAmount.value, + }, + { + forwardOpts: { + intermediateRecipient, + }, + }, + ); return watch(transferV, this.facets.transferHandler, { destination, advanceAmount, @@ -250,6 +259,7 @@ export const prepareAdvancerKit = ( notifyFacet: M.remotable(), borrowerFacet: M.remotable(), poolAccount: M.remotable(), + intermediateRecipient: ChainAddressShape, }), }, ); diff --git a/packages/fast-usdc/src/exos/settler.js b/packages/fast-usdc/src/exos/settler.js index 72f1e1f9626..035c288cabd 100644 --- a/packages/fast-usdc/src/exos/settler.js +++ b/packages/fast-usdc/src/exos/settler.js @@ -1,5 +1,6 @@ import { AmountMath } from '@agoric/ertp'; import { assertAllDefined, makeTracer } from '@agoric/internal'; +import { ChainAddressShape } from '@agoric/orchestration'; import { atob } from '@endo/base64'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; @@ -93,6 +94,7 @@ export const prepareSettler = ( * remoteDenom: Denom; * repayer: LiquidityPoolKit['repayer']; * settlementAccount: HostInterface> + * intermediateRecipient: ChainAddress; * }} config */ config => { @@ -255,7 +257,7 @@ export const prepareSettler = ( * @param {string} EUD */ forward(txHash, sender, fullValue, EUD) { - const { settlementAccount } = this.state; + const { settlementAccount, intermediateRecipient } = this.state; const dest = chainHub.makeChainAddress(EUD); @@ -263,6 +265,11 @@ export const prepareSettler = ( const txfrV = E(settlementAccount).transfer( dest, AmountMath.make(USDC, fullValue), + { + forwardOpts: { + intermediateRecipient, + }, + }, ); void vowTools.watch(txfrV, this.facets.transferHandler, { txHash, @@ -305,6 +312,7 @@ export const prepareSettler = ( sourceChannel: M.string(), remoteDenom: M.string(), mintedEarly: M.remotable('mintedEarly'), + intermediateRecipient: ChainAddressShape, }), }, ); diff --git a/packages/fast-usdc/src/fast-usdc.contract.js b/packages/fast-usdc/src/fast-usdc.contract.js index aa799e101d2..1bcdf2b4a4e 100644 --- a/packages/fast-usdc/src/fast-usdc.contract.js +++ b/packages/fast-usdc/src/fast-usdc.contract.js @@ -140,7 +140,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { 'test of forcing evidence', ); - const { makeLocalAccount } = orchestrateAll(flows, {}); + const { makeLocalAccount, makeNobleAccount } = orchestrateAll(flows, {}); const creatorFacet = zone.exo('Fast USDC Creator', undefined, { /** @type {(operatorId: string) => Promise>} */ @@ -214,7 +214,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { privateArgs.assetInfo, ); } - + const nobleAccountV = zone.makeOnce('NobleAccount', () => makeNobleAccount()); const feedKit = zone.makeOnce('Feed Kit', () => makeFeedKit()); const poolAccountV = zone.makeOnce('PoolAccount', () => makeLocalAccount()); @@ -222,10 +222,18 @@ export const contract = async (zcf, privateArgs, zone, tools) => { makeLocalAccount(), ); // when() is OK here since this clearly resolves promptly. - /** @type {HostInterface>[]} */ - const [poolAccount, settlementAccount] = await vowTools.when( - vowTools.all([poolAccountV, settleAccountV]), + /** @type {[HostInterface>, HostInterface>, HostInterface>]} */ + const [nobleAccount, poolAccount, settlementAccount] = await vowTools.when( + vowTools.all([nobleAccountV, poolAccountV, settleAccountV]), + ); + trace('settlementAccount', settlementAccount); + trace('poolAccount', poolAccount); + trace('nobleAccount', nobleAccount); + + const intermediateRecipient = await vowTools.when( + E(nobleAccount).getAddress(), ); + trace('intermediateRecipient', intermediateRecipient); const [_agoric, _noble, agToNoble] = await vowTools.when( chainHub.getChainsAndConnection('agoric', 'noble'), @@ -235,6 +243,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { sourceChannel: agToNoble.transferChannel.counterPartyChannelId, remoteDenom: 'uusdc', settlementAccount, + intermediateRecipient, }); const advancer = zone.makeOnce('Advancer', () => @@ -242,6 +251,7 @@ export const contract = async (zcf, privateArgs, zone, tools) => { borrowerFacet: poolKit.borrower, notifyFacet: settlerKit.notify, poolAccount, + intermediateRecipient, }), ); // Connect evidence stream to advancer diff --git a/packages/fast-usdc/src/fast-usdc.flows.js b/packages/fast-usdc/src/fast-usdc.flows.js index 9f330a4c905..a9aedd6228e 100644 --- a/packages/fast-usdc/src/fast-usdc.flows.js +++ b/packages/fast-usdc/src/fast-usdc.flows.js @@ -11,3 +11,13 @@ export const makeLocalAccount = async orch => { return agoricChain.makeAccount(); }; harden(makeLocalAccount); + +/** + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + */ +export const makeNobleAccount = async orch => { + const nobleChain = await orch.getChain('noble'); + return nobleChain.makeAccount(); +}; +harden(makeNobleAccount); diff --git a/packages/fast-usdc/src/type-guards.js b/packages/fast-usdc/src/type-guards.js index 5561f7b9332..a645f90bae4 100644 --- a/packages/fast-usdc/src/type-guards.js +++ b/packages/fast-usdc/src/type-guards.js @@ -49,11 +49,10 @@ export const CctpTxEvidenceShape = { recipientAddress: M.string(), }, blockHash: EvmHashShape, - blockNumber: M.bigint(), - blockTimestamp: M.bigint(), + blockNumber: M.nat(), chainId: M.number(), tx: { - amount: M.bigint(), + amount: M.nat(), forwardingAddress: M.string(), }, txHash: EvmHashShape, diff --git a/packages/fast-usdc/src/types.ts b/packages/fast-usdc/src/types.ts index e25536ff383..edd182337b7 100644 --- a/packages/fast-usdc/src/types.ts +++ b/packages/fast-usdc/src/types.ts @@ -16,7 +16,6 @@ export interface CctpTxEvidence { }; blockHash: EvmHash; blockNumber: bigint; - blockTimestamp: bigint; chainId: number; /** data covered by signature (aka txHash) */ tx: { diff --git a/packages/fast-usdc/test/exos/advancer.test.ts b/packages/fast-usdc/test/exos/advancer.test.ts index 0921fb4838f..818554dacca 100644 --- a/packages/fast-usdc/test/exos/advancer.test.ts +++ b/packages/fast-usdc/test/exos/advancer.test.ts @@ -14,7 +14,7 @@ import { prepareStatusManager } from '../../src/exos/status-manager.js'; import { makeFeeTools } from '../../src/utils/fees.js'; import { addressTools } from '../../src/utils/address.js'; import { commonSetup } from '../supports.js'; -import { MockCctpTxEvidences } from '../fixtures.js'; +import { MockCctpTxEvidences, intermediateRecipient } from '../fixtures.js'; import { makeTestFeeConfig, makeTestLogger, @@ -113,6 +113,7 @@ const createTestExtensions = (t, common: CommonSetup) => { borrowerFacet: mockBorrowerF, notifyFacet: mockNotifyF, poolAccount: mockAccounts.mockPoolAccount.account, + intermediateRecipient, }); return { @@ -221,6 +222,7 @@ test('updates status to OBSERVED on insufficient pool funds', async t => { borrowerFacet: mockBorrowerErrorF, notifyFacet: mockNotifyF, poolAccount: mockPoolAccount.account, + intermediateRecipient, }); const mockEvidence = MockCctpTxEvidences.AGORIC_PLUS_DYDX(); diff --git a/packages/fast-usdc/test/exos/settler.test.ts b/packages/fast-usdc/test/exos/settler.test.ts index c970f55eaed..f900c7cd191 100644 --- a/packages/fast-usdc/test/exos/settler.test.ts +++ b/packages/fast-usdc/test/exos/settler.test.ts @@ -9,7 +9,11 @@ import { prepareSettler } from '../../src/exos/settler.js'; import { prepareStatusManager } from '../../src/exos/status-manager.js'; import type { CctpTxEvidence } from '../../src/types.js'; import { makeFeeTools } from '../../src/utils/fees.js'; -import { MockCctpTxEvidences, MockVTransferEvents } from '../fixtures.js'; +import { + MockCctpTxEvidences, + MockVTransferEvents, + intermediateRecipient, +} from '../fixtures.js'; import { makeTestLogger, prepareMockOrchAccounts } from '../mocks.js'; import { commonSetup } from '../supports.js'; @@ -87,6 +91,7 @@ const makeTestContext = async t => { fetchedChainInfo.agoric.connections['noble-1'].transferChannel .counterPartyChannelId, remoteDenom: 'uusdc', + intermediateRecipient, }); const simulate = harden({ @@ -280,6 +285,15 @@ test('slow path: forward to EUD; remove pending tx', async t => { value: 'osmo183dejcnmkka5dzcu9xw6mywq0p2m5peks28men', }, usdc.units(150), + { + forwardOpts: { + intermediateRecipient: { + chainId: 'noble-1', + encoding: 'bech32', + value: 'noble1test', + }, + }, + }, ], ]); diff --git a/packages/fast-usdc/test/fast-usdc.contract.test.ts b/packages/fast-usdc/test/fast-usdc.contract.test.ts index 94e79dddbb8..c622acd50a7 100644 --- a/packages/fast-usdc/test/fast-usdc.contract.test.ts +++ b/packages/fast-usdc/test/fast-usdc.contract.test.ts @@ -97,6 +97,7 @@ const startContract = async ( const makeTestContext = async (t: ExecutionContext) => { const common = await commonSetup(t); + await E(common.mocks.ibcBridge).setAddressPrefix('noble'); const startKit = await startContract(common, 2); @@ -328,7 +329,6 @@ const makeEVM = (template = MockCctpTxEvidences.AGORIC_PLUS_OSMO()) => { ...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 }, @@ -406,7 +406,7 @@ const makeCustomer = ( const [ibcTransferMsg] = lm.messages; // support advances to noble + other chains const receiver = - ibcTransferMsg.receiver === 'pfm' + ibcTransferMsg.receiver === 'noble1test' // intermediateRecipient value ? JSON.parse(ibcTransferMsg.memo).forward.receiver : ibcTransferMsg.receiver; return ( diff --git a/packages/fast-usdc/test/fixtures.ts b/packages/fast-usdc/test/fixtures.ts index 0a520918906..cbd83ccb657 100644 --- a/packages/fast-usdc/test/fixtures.ts +++ b/packages/fast-usdc/test/fixtures.ts @@ -1,6 +1,7 @@ import type { VTransferIBCEvent } from '@agoric/vats'; import { buildVTransferEvent } from '@agoric/orchestration/tools/ibc-mocks.js'; import fetchedChainInfo from '@agoric/orchestration/src/fetched-chain-info.js'; +import type { ChainAddress } from '@agoric/orchestration'; import type { CctpTxEvidence } from '../src/types.js'; const mockScenarios = [ @@ -20,7 +21,6 @@ export const MockCctpTxEvidences: Record< blockHash: '0x90d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee665', blockNumber: 21037663n, - blockTimestamp: 1730762090n, txHash: '0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761702', tx: { @@ -39,7 +39,6 @@ export const MockCctpTxEvidences: Record< blockHash: '0x80d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', blockNumber: 21037669n, - blockTimestamp: 1730762099n, txHash: '0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', tx: { @@ -58,7 +57,6 @@ export const MockCctpTxEvidences: Record< blockHash: '0x70d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', blockNumber: 21037669n, - blockTimestamp: 1730762099n, txHash: '0xa81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', tx: { @@ -77,7 +75,6 @@ export const MockCctpTxEvidences: Record< blockHash: '0x70d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699', blockNumber: 21037669n, - blockTimestamp: 1730762099n, txHash: '0xa81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799', tx: { @@ -145,3 +142,9 @@ export const MockVTransferEvents: Record< MockCctpTxEvidences.AGORIC_UNKNOWN_EUD().aux.recipientAddress, }), }; + +export const intermediateRecipient: ChainAddress = harden({ + chainId: 'noble-1', + value: 'noble1test', + encoding: 'bech32', +}); 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 72890a09ece..1c6fe4d0433 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 @@ -588,6 +588,7 @@ Generated by [AVA](https://avajs.dev). withdrawHandler: Object @Alleged: Liquidity Pool withdrawHandler {}, }, 'Liquidity Pool_kindHandle': 'Alleged: kind', + NobleAccount: 'Vow', 'Operator Kit_kindHandle': 'Alleged: kind', PendingTxs: {}, PoolAccount: 'Vow', @@ -601,6 +602,9 @@ Generated by [AVA](https://avajs.dev). makeLocalAccount: { asyncFlow_kindHandle: 'Alleged: kind', }, + makeNobleAccount: { + asyncFlow_kindHandle: 'Alleged: kind', + }, }, pending: {}, vstorage: { @@ -620,6 +624,10 @@ Generated by [AVA](https://avajs.dev). pending: false, value: Object @Alleged: LocalChainFacade public {}, }, + noble: { + pending: false, + value: Object @Alleged: RemoteChainFacade public {}, + }, }, ibcTools: { IBCTransferSenderKit_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 c289cbdd0bb..09943c2591d 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/orchestration/src/cosmos-api.ts b/packages/orchestration/src/cosmos-api.ts index dad627c1382..a4934c2006c 100644 --- a/packages/orchestration/src/cosmos-api.ts +++ b/packages/orchestration/src/cosmos-api.ts @@ -334,6 +334,8 @@ export interface IBCMsgTransferOptions { timeoutTimestamp?: MsgTransfer['timeoutTimestamp']; memo?: string; forwardOpts?: { + /** The recipient address for the intermediate transfer. Defaults to 'pfm' unless specified */ + intermediateRecipient?: ChainAddress; timeout?: ForwardInfo['forward']['timeout']; retries?: ForwardInfo['forward']['retries']; }; @@ -395,7 +397,7 @@ export type TransferRoute = { token: Coin; } & ( | { - receiver: typeof PFM_RECEIVER; + receiver: typeof PFM_RECEIVER | ChainAddress['value']; /** contains PFM forwarding info */ forwardInfo: ForwardInfo; } diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index 8f40d34b563..557d25e0585 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -606,7 +606,7 @@ export const makeChainHub = (zone, agoricNames, vowTools) => { * purposely using invalid bech32 * {@link https://github.com/cosmos/ibc-apps/blob/26f3ad8f58e4ffc7769c6766cb42b954181dc100/middleware/packet-forward-middleware/README.md#minimal-example---chain-forward-a-b-c} */ - receiver: PFM_RECEIVER, + receiver: forwardOpts?.intermediateRecipient?.value || PFM_RECEIVER, forwardInfo, }); }, diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 843bb10fbb5..a787ff8e39d 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -82,7 +82,7 @@ import { makeTimestampHelper } from '../utils/time.js'; * @import {LocalIbcAddress, RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; */ -const trace = makeTracer('ComosOrchestrationAccountHolder'); +const trace = makeTracer('CosmosOrchAccount'); const { Vow$ } = NetworkShape; // TODO #9611 diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 20285c1c9b2..362ac8950d7 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -43,7 +43,7 @@ import { TransferRouteShape } from './chain-hub.js'; * @import {ZoeTools} from '../utils/zoe-tools.js'; */ -const trace = makeTracer('LOA'); +const trace = makeTracer('LocalOrchAccount'); const { Vow$ } = NetworkShape; // TODO #9611 @@ -678,7 +678,7 @@ export const prepareLocalOrchestrationAccountKit = ( */ transfer(destination, amount, opts) { return asVow(() => { - trace('Transferring funds from LCA over IBC'); + trace('Transferring funds over IBC'); const denomAmount = coerceDenomAmount(chainHub, amount); const { forwardOpts, ...rest } = opts ?? {}; @@ -690,6 +690,7 @@ export const prepareLocalOrchestrationAccountKit = ( 'agoric', forwardOpts, ); + trace('got transfer route', q(route).toString()); // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` // TODO #9324 what's a reasonable default? currently 5 minutes diff --git a/packages/orchestration/src/exos/remote-chain-facade.js b/packages/orchestration/src/exos/remote-chain-facade.js index 3efebfff11d..3349eb39c5d 100644 --- a/packages/orchestration/src/exos/remote-chain-facade.js +++ b/packages/orchestration/src/exos/remote-chain-facade.js @@ -125,8 +125,6 @@ const prepareRemoteChainFacadeKit = ( makeAccount() { return asVow(() => { const { remoteChainInfo, connectionInfo } = this.state; - const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom; - if (!stakingDenom) throw Fail`chain info lacks staking denom`; // icqConnection is ultimately retrieved from state, but let's // create a connection if it doesn't exist diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index 4496e245643..3f83257ab32 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -245,6 +245,7 @@ export const ForwardOptsShape = M.splitRecord( { timeout: M.string(), retries: M.number(), + intermediateRecipient: ChainAddressShape, }, {}, ); diff --git a/packages/orchestration/test/exos/chain-hub.test.ts b/packages/orchestration/test/exos/chain-hub.test.ts index 870d5c9672d..1505c6e0321 100644 --- a/packages/orchestration/test/exos/chain-hub.test.ts +++ b/packages/orchestration/test/exos/chain-hub.test.ts @@ -377,6 +377,7 @@ test('makeTransferRoute - takes forwardOpts', t => { token: { denom: uusdcOnOsmosis, }, + receiver: 'pfm', forwardInfo: { forward: { channel: 'channel-21', @@ -385,9 +386,19 @@ test('makeTransferRoute - takes forwardOpts', t => { }, }); + const nobleAddr = harden({ + value: 'noble1234', + encoding: 'bech32' as const, + chainId: 'noble-1', + }); + t.like( - chainHub.makeTransferRoute(dest, amt, 'osmosis', { timeout: '99m' }), + chainHub.makeTransferRoute(dest, amt, 'osmosis', { + timeout: '99m', + intermediateRecipient: nobleAddr, + }), { + receiver: nobleAddr.value, forwardInfo: { forward: { timeout: '99m' as const, diff --git a/packages/orchestration/test/facade-durability.test.ts b/packages/orchestration/test/facade-durability.test.ts index dff35064c26..011ccc98276 100644 --- a/packages/orchestration/test/facade-durability.test.ts +++ b/packages/orchestration/test/facade-durability.test.ts @@ -80,7 +80,7 @@ test('chain info', async t => { t.deepEqual(await vt.when(result.getChainInfo()), mockChainInfo); }); -test('faulty chain info', async t => { +test('missing chain info', async t => { const { facadeServices, commonPrivateArgs } = await commonSetup(t); const zone = provideFreshRootZone(); @@ -99,16 +99,7 @@ test('faulty chain info', async t => { commonPrivateArgs.marshaller, ); - const { chainHub, orchestrate } = orchKit; - - const { stakingTokens, ...sansStakingTokens } = mockChainInfo; - - chainHub.registerChain('mock', sansStakingTokens); - chainHub.registerConnection( - 'agoric-3', - mockChainInfo.chainId, - mockChainConnection, - ); + const { orchestrate } = orchKit; const handle = orchestrate('mock', {}, async orc => { const chain = await orc.getChain('mock'); @@ -117,7 +108,7 @@ test('faulty chain info', async t => { }); await t.throwsAsync(vt.when(handle()), { - message: 'chain info lacks staking denom', + message: 'chain not found:mock', }); });