From 8a2f60e63c14453da32e7fafc81dcb036318be16 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 19 Aug 2024 14:29:54 -0400 Subject: [PATCH 1/6] feat: icqConnection.query returns a vow --- .../src/exos/icq-connection-kit.js | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/orchestration/src/exos/icq-connection-kit.js b/packages/orchestration/src/exos/icq-connection-kit.js index cfe78cc8ae5..755950f4b96 100644 --- a/packages/orchestration/src/exos/icq-connection-kit.js +++ b/packages/orchestration/src/exos/icq-connection-kit.js @@ -2,6 +2,7 @@ import { Fail } from '@endo/errors'; import { E } from '@endo/far'; import { M } from '@endo/patterns'; +import { VowShape } from '@agoric/vow'; import { NonNullish, makeTracer } from '@agoric/internal'; import { makeQueryPacket, parseQueryPacket } from '../utils/packet.js'; import { OutboundConnectionHandlerI } from '../typeGuards.js'; @@ -9,7 +10,7 @@ import { OutboundConnectionHandlerI } from '../typeGuards.js'; /** * @import {Zone} from '@agoric/base-zone'; * @import {Connection, Port} from '@agoric/network'; - * @import {Remote, VowTools} from '@agoric/vow'; + * @import {Remote, Vow, VowTools} from '@agoric/vow'; * @import {JsonSafe} from '@agoric/cosmic-proto'; * @import {RequestQuery, ResponseQuery} from '@agoric/cosmic-proto/tendermint/abci/types.js'; * @import {LocalIbcAddress, RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; @@ -25,7 +26,7 @@ export const ICQMsgShape = M.splitRecord( export const ICQConnectionI = M.interface('ICQConnection', { getLocalAddress: M.call().returns(M.string()), getRemoteAddress: M.call().returns(M.string()), - query: M.call(M.arrayOf(ICQMsgShape)).returns(M.promise()), + query: M.call(M.arrayOf(ICQMsgShape)).returns(VowShape), }); /** @@ -53,7 +54,7 @@ export const ICQConnectionI = M.interface('ICQConnection', { * @param {Zone} zone * @param {VowTools} vowTools */ -export const prepareICQConnectionKit = (zone, { watch, when }) => +export const prepareICQConnectionKit = (zone, { watch, asVow }) => zone.exoClassKit( 'ICQConnectionKit', { @@ -89,20 +90,17 @@ export const prepareICQConnectionKit = (zone, { watch, when }) => }, /** * @param {JsonSafe[]} msgs - * @returns {Promise[]>} - * @throws {Error} if packet fails to send or an error is returned + * @returns {Vow[]>} */ query(msgs) { - const { connection } = this.state; - // TODO #9281 do not throw synchronously when returning a promise; return a rejected Vow - /// see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 - if (!connection) throw Fail`connection not available`; - return when( - watch( + return asVow(() => { + const { connection } = this.state; + if (!connection) throw Fail`connection not available`; + return watch( E(connection).send(makeQueryPacket(msgs)), this.facets.parseQueryPacketWatcher, - ), - ); + ); + }); }, }, parseQueryPacketWatcher: { From 1cd5ca698a7627fe40ff7eccae8d7154b5b1282b Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 19 Aug 2024 22:43:50 -0400 Subject: [PATCH 2/6] test: track ica accounts separately from ibc channels - with the refactor of remote-chain-facade.js, sometimes the icqConn resolves before the account. this change ensures the first account in each test context is cosmos1test and not cosmos1test1 --- packages/orchestration/test/network-fakes.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/orchestration/test/network-fakes.ts b/packages/orchestration/test/network-fakes.ts index c9b0304903c..d7360331ab9 100644 --- a/packages/orchestration/test/network-fakes.ts +++ b/packages/orchestration/test/network-fakes.ts @@ -179,6 +179,7 @@ export const makeFakeIBCBridge = ( * @type {nubmer} */ let channelCount = 0; + let icaAccountCount = 0; let bech32Prefix = 'cosmos'; /** @@ -205,13 +206,16 @@ export const makeFakeIBCBridge = ( const connectionChannelCount = remoteChannelMap[obj.hops[0]] || 0; const ackEvent = ibcBridgeMocks.channelOpenAck(obj, { bech32Prefix, - sequence: channelCount, + sequence: icaAccountCount, channelID: `channel-${channelCount}`, counterpartyChannelID: `channel-${connectionChannelCount}`, }); bridgeHandler?.fromBridge(ackEvent); bridgeEvents = bridgeEvents.concat(ackEvent); channelCount += 1; + if (obj.packet.source_port.includes('icacontroller')) { + icaAccountCount += 1; + } remoteChannelMap[obj.hops[0]] = connectionChannelCount + 1; return undefined; } From 27aaf9e7f48bb844d973e225e013ca028e25eae5 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 19 Aug 2024 23:01:06 -0400 Subject: [PATCH 3/6] chore: IBCMethod<'bindPort'> type --- packages/vats/src/types.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vats/src/types.d.ts b/packages/vats/src/types.d.ts index a2f901dd144..c49d17709d3 100644 --- a/packages/vats/src/types.d.ts +++ b/packages/vats/src/types.d.ts @@ -223,7 +223,7 @@ type IBCMethodEvents = { receiveExecuted: {}; // TODO update startChannelOpenInit: ChannelOpenInitDowncall; startChannelCloseInit: {}; // TODO update - bindPort: {}; // TODO update + bindPort: { packet: { source_port: IBCPortID } }; timeoutExecuted: {}; // TODO update // XXX why isn't this in receiver.go? initOpenExecuted: ChannelOpenAckDowncall; From 4b176caec4aa245d992d646726e87f9e3ea36788 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 19 Aug 2024 18:11:27 -0400 Subject: [PATCH 4/6] feat: plumb ICQConnection through RemoteChainFacade - ensure CosmosOrchestrationAccountKit is given icqConnection if chainConfig has icqEnabled - ensure Chain.query() sends queries if chainConfig has icqEnabled - lazily provideICQConnection on first request and cache result in state - refs: #9890 --- .../src/examples/basic-flows.contract.js | 15 ++ .../src/examples/basic-flows.flows.js | 59 ++++++- .../src/exos/icq-connection-kit.js | 9 +- .../src/exos/local-chain-facade.js | 5 + .../src/exos/remote-chain-facade.js | 149 ++++++++++++----- packages/orchestration/src/typeGuards.js | 6 + .../examples/basic-flows.contract.test.ts | 150 +++++++++++++++++- 7 files changed, 338 insertions(+), 55 deletions(-) diff --git a/packages/orchestration/src/examples/basic-flows.contract.js b/packages/orchestration/src/examples/basic-flows.contract.js index b1ff6b8613b..010247fd231 100644 --- a/packages/orchestration/src/examples/basic-flows.contract.js +++ b/packages/orchestration/src/examples/basic-flows.contract.js @@ -40,6 +40,9 @@ const contract = async ( M.interface('Basic Flows PF', { makeOrchAccountInvitation: M.callWhen().returns(InvitationShape), makePortfolioAccountInvitation: M.callWhen().returns(InvitationShape), + makeSendICQQueryInvitation: M.callWhen().returns(InvitationShape), + makeAccountAndSendBalanceQueryInvitation: + M.callWhen().returns(InvitationShape), }), { makeOrchAccountInvitation() { @@ -54,6 +57,18 @@ const contract = async ( 'Make an Orchestration Account', ); }, + makeSendICQQueryInvitation() { + return zcf.makeInvitation( + orchFns.sendQuery, + 'Submit a query to a remote chain', + ); + }, + makeAccountAndSendBalanceQueryInvitation() { + return zcf.makeInvitation( + orchFns.makeAccountAndSendBalanceQuery, + 'Make an account and submit a balance query', + ); + }, }, ); diff --git a/packages/orchestration/src/examples/basic-flows.flows.js b/packages/orchestration/src/examples/basic-flows.flows.js index 2e6e9369ad0..d0bf98c9d9a 100644 --- a/packages/orchestration/src/examples/basic-flows.flows.js +++ b/packages/orchestration/src/examples/basic-flows.flows.js @@ -2,12 +2,14 @@ * @file Primarily a testing fixture, but also serves as an example of how to * leverage basic functionality of the Orchestration API with async-flow. */ +import { Fail } from '@endo/errors'; import { M, mustMatch } from '@endo/patterns'; /** - * @import {Zone} from '@agoric/zone'; - * @import {OrchestrationAccount, OrchestrationFlow, Orchestrator} from '@agoric/orchestration'; + * @import {DenomArg, OrchestrationAccount, OrchestrationFlow, Orchestrator} from '@agoric/orchestration'; * @import {ResolvedPublicTopic} from '@agoric/zoe/src/contractSupport/topics.js'; + * @import {JsonSafe} from '@agoric/cosmic-proto'; + * @import {RequestQuery} from '@agoric/cosmic-proto/tendermint/abci/types.js'; * @import {OrchestrationPowers} from '../utils/start-helper.js'; * @import {MakePortfolioHolder} from '../exos/portfolio-holder-kit.js'; * @import {OrchestrationTools} from '../utils/start-helper.js'; @@ -79,3 +81,56 @@ export const makePortfolioAccount = async ( return portfolioHolder.asContinuingOffer(); }; harden(makePortfolioAccount); + +/** + * Send a query and get the response back in an offer result. This invitation is + * for testing only. In a real scenario it's better to use an RPC or API client + * and vstorage to retrieve data for a frontend. Queries should only be + * leveraged if contract logic requires it. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {any} _ctx + * @param {ZCFSeat} seat + * @param {{ chainName: string; msgs: JsonSafe[] }} offerArgs + */ +export const sendQuery = async (orch, _ctx, seat, { chainName, msgs }) => { + seat.exit(); // no funds exchanged + mustMatch(chainName, M.string()); + if (chainName === 'agoric') throw Fail`ICQ not supported on local chain`; + const remoteChain = await orch.getChain(chainName); + // @ts-expect-error FIXME implement Chain.query + const queryResponse = await remoteChain.query(msgs); + console.debug('sendQuery response:', queryResponse); + return queryResponse; +}; +harden(sendQuery); + +/** + * Create an account and send a query and get the response back in an offer + * result. Like `sendQuery`, this invitation is for testing only. In a real + * scenario it doesn't make much sense to send a query immediately after the + * account is created - it won't have any funds. + * + * @satisfies {OrchestrationFlow} + * @param {Orchestrator} orch + * @param {any} _ctx + * @param {ZCFSeat} seat + * @param {{ chainName: string; denom: DenomArg }} offerArgs + */ +export const makeAccountAndSendBalanceQuery = async ( + orch, + _ctx, + seat, + { chainName, denom }, +) => { + seat.exit(); // no funds exchanged + mustMatch(chainName, M.string()); + if (chainName === 'agoric') throw Fail`ICQ not supported on local chain`; + const remoteChain = await orch.getChain(chainName); + const orchAccount = await remoteChain.makeAccount(); + const queryResponse = await orchAccount.getBalance(denom); + console.debug('getBalance response:', queryResponse); + return queryResponse; +}; +harden(makeAccountAndSendBalanceQuery); diff --git a/packages/orchestration/src/exos/icq-connection-kit.js b/packages/orchestration/src/exos/icq-connection-kit.js index 755950f4b96..a2ac5bba4aa 100644 --- a/packages/orchestration/src/exos/icq-connection-kit.js +++ b/packages/orchestration/src/exos/icq-connection-kit.js @@ -5,7 +5,7 @@ import { M } from '@endo/patterns'; import { VowShape } from '@agoric/vow'; import { NonNullish, makeTracer } from '@agoric/internal'; import { makeQueryPacket, parseQueryPacket } from '../utils/packet.js'; -import { OutboundConnectionHandlerI } from '../typeGuards.js'; +import { ICQMsgShape, OutboundConnectionHandlerI } from '../typeGuards.js'; /** * @import {Zone} from '@agoric/base-zone'; @@ -18,11 +18,6 @@ import { OutboundConnectionHandlerI } from '../typeGuards.js'; const trace = makeTracer('Orchestration:ICQConnection'); -export const ICQMsgShape = M.splitRecord( - { path: M.string(), data: M.string() }, - { height: M.string(), prove: M.boolean() }, -); - export const ICQConnectionI = M.interface('ICQConnection', { getLocalAddress: M.call().returns(M.string()), getRemoteAddress: M.call().returns(M.string()), @@ -89,6 +84,8 @@ export const prepareICQConnectionKit = (zone, { watch, asVow }) => ); }, /** + * Vow rejects if packet fails to send or an error is returned + * * @param {JsonSafe[]} msgs * @returns {Vow[]>} */ diff --git a/packages/orchestration/src/exos/local-chain-facade.js b/packages/orchestration/src/exos/local-chain-facade.js index 766afb110d9..65bff5c71bc 100644 --- a/packages/orchestration/src/exos/local-chain-facade.js +++ b/packages/orchestration/src/exos/local-chain-facade.js @@ -6,6 +6,7 @@ import { M } from '@endo/patterns'; import { pickFacet } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; +import { Fail } from '@endo/errors'; import { chainFacadeMethods } from '../typeGuards.js'; /** @@ -107,6 +108,10 @@ const prepareLocalChainFacadeKit = ( this.facets.makeAccountWatcher, ); }, + query() { + // TODO https://github.com/Agoric/agoric-sdk/pull/9935 + return asVow(() => Fail`not yet implemented`); + }, /** @type {HostOf} */ getVBankAssetInfo() { return asVow(() => { diff --git a/packages/orchestration/src/exos/remote-chain-facade.js b/packages/orchestration/src/exos/remote-chain-facade.js index c68abb389e4..21c2aa0e171 100644 --- a/packages/orchestration/src/exos/remote-chain-facade.js +++ b/packages/orchestration/src/exos/remote-chain-facade.js @@ -4,25 +4,24 @@ import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { pickFacet } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; -import { ChainAddressShape, ChainFacadeI } from '../typeGuards.js'; +import { ChainAddressShape, ChainFacadeI, ICQMsgShape } from '../typeGuards.js'; /** - * @import {HostInterface, HostOf} from '@agoric/async-flow'; + * @import {HostOf} from '@agoric/async-flow'; * @import {Zone} from '@agoric/base-zone'; + * @import {JsonSafe} from '@agoric/cosmic-proto'; + * @import {RequestQuery, ResponseQuery} from '@agoric/cosmic-proto/tendermint/abci/types.js'; * @import {TimerService} from '@agoric/time'; * @import {Remote} from '@agoric/internal'; * @import {Vow, VowTools} from '@agoric/vow'; * @import {CosmosInterchainService} from './cosmos-interchain-service.js'; * @import {prepareCosmosOrchestrationAccount} from './cosmos-orchestration-account.js'; - * @import {ChainInfo, CosmosChainInfo, IBCConnectionInfo, OrchestrationAccount, ChainAddress, IcaAccount, Denom, Chain} from '../types.js'; + * @import {CosmosChainInfo, IBCConnectionInfo, ChainAddress, IcaAccount, Chain, ICQConnection} from '../types.js'; */ const { Fail } = assert; const trace = makeTracer('RemoteChainFacade'); -/** @type {any} */ -const anyVal = null; - /** * @typedef {{ * makeCosmosOrchestrationAccount: ReturnType< @@ -35,6 +34,14 @@ const anyVal = null; * }} RemoteChainFacadePowers */ +/** + * @typedef {{ + * remoteChainInfo: CosmosChainInfo; + * connectionInfo: IBCConnectionInfo; + * icqConnection: ICQConnection | undefined; + * }} RemoteChainFacadeState + */ + /** * @param {Zone} zone * @param {RemoteChainFacadePowers} powers @@ -48,30 +55,38 @@ const prepareRemoteChainFacadeKit = ( // consider making an `accounts` childNode storageNode, timer, - vowTools: { asVow, watch }, + vowTools: { allVows, asVow, watch }, }, ) => zone.exoClassKit( 'RemoteChainFacade', { public: ChainFacadeI, - makeAccountWatcher: M.interface('makeAccountWatcher', { - onFulfilled: M.call(M.remotable()) - .optional(M.arrayOf(M.undefined())) // empty context - .returns(VowShape), - }), + makeICQConnectionQueryWatcher: M.interface( + 'makeICQConnectionQueryWatcher', + { + onFulfilled: M.call(M.remotable(), M.arrayOf(ICQMsgShape)).returns( + VowShape, + ), + }, + ), + makeAccountAndProvideQueryConnWatcher: M.interface( + 'makeAccountAndProvideQueryConnWatcher', + { + onFulfilled: M.call([ + M.remotable(), + M.or(M.remotable(), M.undefined()), + ]).returns(VowShape), + }, + ), getAddressWatcher: M.interface('getAddressWatcher', { - onFulfilled: M.call(M.record()) - .optional(M.remotable()) - .returns(VowShape), + onFulfilled: M.call(ChainAddressShape, M.remotable()).returns(VowShape), }), makeChildNodeWatcher: M.interface('makeChildNodeWatcher', { - onFulfilled: M.call(M.remotable()) - .optional({ - account: M.remotable(), - chainAddress: ChainAddressShape, - }) - .returns(M.remotable()), + onFulfilled: M.call(M.remotable(), { + account: M.remotable(), + chainAddress: ChainAddressShape, + }).returns(M.remotable()), }), }, /** @@ -80,7 +95,11 @@ const prepareRemoteChainFacadeKit = ( */ (remoteChainInfo, connectionInfo) => { trace('making a RemoteChainFacade'); - return { remoteChainInfo, connectionInfo }; + return /** @type {RemoteChainFacadeState} */ ({ + remoteChainInfo, + connectionInfo, + icqConnection: undefined, + }); }, { public: { @@ -94,28 +113,62 @@ const prepareRemoteChainFacadeKit = ( return asVow(() => { const { remoteChainInfo, connectionInfo } = this.state; const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom; - if (!stakingDenom) { - throw Fail`chain info lacks staking 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 + const icqConnOrUndefinedV = + remoteChainInfo.icqEnabled && !this.state.icqConnection + ? E(orchestration).provideICQConnection( + connectionInfo.counterparty.connection_id, + ) + : undefined; + + const makeAccountV = E(orchestration).makeAccount( + remoteChainInfo.chainId, + connectionInfo.counterparty.connection_id, + connectionInfo.id, + ); return watch( - E(orchestration).makeAccount( - remoteChainInfo.chainId, - connectionInfo.counterparty.connection_id, - connectionInfo.id, - ), - this.facets.makeAccountWatcher, + allVows([makeAccountV, icqConnOrUndefinedV]), + this.facets.makeAccountAndProvideQueryConnWatcher, ); }); }, + /** @type {ICQConnection['query']} */ + query(msgs) { + return asVow(() => { + const { + remoteChainInfo: { icqEnabled, chainId }, + connectionInfo, + } = this.state; + if (!icqEnabled) { + throw Fail`Queries not available for chain ${chainId}`; + } + // if none exists, make one and still send the query in the handler + if (!this.state.icqConnection) { + return watch( + E(orchestration).provideICQConnection( + connectionInfo.counterparty.connection_id, + ), + this.facets.makeICQConnectionQueryWatcher, + msgs, + ); + } + return watch(E(this.state.icqConnection).query(msgs)); + }); + }, }, - makeAccountWatcher: { + makeAccountAndProvideQueryConnWatcher: { /** - * XXX Pipeline vows allVows and E - * - * @param {IcaAccount} account + * @param {[IcaAccount, ICQConnection | undefined]} account */ - onFulfilled(account) { + onFulfilled([account, icqConnection]) { + if (icqConnection && !this.state.icqConnection) { + this.state.icqConnection = icqConnection; + // no need to pass icqConnection in ctx; we can get it from state + } return watch( E(account).getAddress(), this.facets.getAddressWatcher, @@ -123,6 +176,19 @@ const prepareRemoteChainFacadeKit = ( ); }, }, + makeICQConnectionQueryWatcher: { + /** + * @param {ICQConnection} icqConnection + * @param {JsonSafe[]} msgs + * @returns {Vow[]>} + */ + onFulfilled(icqConnection, msgs) { + if (!this.state.icqConnection) { + this.state.icqConnection = icqConnection; + } + return watch(E(icqConnection).query(msgs)); + }, + }, getAddressWatcher: { /** * @param {ChainAddress} chainAddress @@ -145,18 +211,15 @@ const prepareRemoteChainFacadeKit = ( * }} ctx */ onFulfilled(childNode, { account, chainAddress }) { - const { remoteChainInfo } = this.state; + const { remoteChainInfo, icqConnection } = this.state; const stakingDenom = remoteChainInfo.stakingTokens?.[0]?.denom; - if (!stakingDenom) { - throw Fail`chain info lacks staking denom`; - } + if (!stakingDenom) throw Fail`chain info lacks staking denom`; + return makeCosmosOrchestrationAccount(chainAddress, stakingDenom, { account, // FIXME storage path https://github.com/Agoric/agoric-sdk/issues/9066 storageNode: childNode, - // FIXME provide real ICQ connection - // FIXME make Query Connection available via chain, not orchestrationAccount - icqConnection: anyVal, + icqConnection, timer, }); }, diff --git a/packages/orchestration/src/typeGuards.js b/packages/orchestration/src/typeGuards.js index 8a1164bcc73..e5a2274fcbb 100644 --- a/packages/orchestration/src/typeGuards.js +++ b/packages/orchestration/src/typeGuards.js @@ -117,9 +117,15 @@ export const DenomAmountShape = { denom: DenomShape, value: M.bigint() }; export const AmountArgShape = M.or(AmountShape, DenomAmountShape); +export const ICQMsgShape = M.splitRecord( + { path: M.string(), data: M.string() }, + { height: M.string(), prove: M.boolean() }, +); + export const chainFacadeMethods = harden({ getChainInfo: M.call().returns(VowShape), makeAccount: M.call().returns(VowShape), + query: M.call(M.arrayOf(ICQMsgShape)).returns(VowShape), }); /** @see {Chain} */ diff --git a/packages/orchestration/test/examples/basic-flows.contract.test.ts b/packages/orchestration/test/examples/basic-flows.contract.test.ts index a7b975c1cd1..3096e13df65 100644 --- a/packages/orchestration/test/examples/basic-flows.contract.test.ts +++ b/packages/orchestration/test/examples/basic-flows.contract.test.ts @@ -4,6 +4,13 @@ import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import type { Instance } from '@agoric/zoe/src/zoeService/utils.js'; import { E, getInterfaceOf } from '@endo/far'; import path from 'path'; +import { JsonSafe, toRequestQueryJson } from '@agoric/cosmic-proto'; +import { + QueryBalanceRequest, + QueryBalanceResponse, +} from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; +import type { ResponseQuery } from '@agoric/cosmic-proto/tendermint/abci/types.js'; +import { decodeBase64 } from '@endo/base64'; import { commonSetup } from '../supports.js'; const dirname = path.dirname(new URL(import.meta.url).pathname); @@ -20,7 +27,10 @@ type TestContext = Awaited> & { const test = anyTest as TestFn; -test.before(async t => { +const decodeBalanceQueryResponse = (results: JsonSafe[]) => + results.map(({ key }) => QueryBalanceResponse.decode(decodeBase64(key))); + +test.beforeEach(async t => { const setupContext = await commonSetup(t); const { bootstrap: { storage }, @@ -48,8 +58,9 @@ test.before(async t => { }); const chainConfigs = { - agoric: { addressPrefix: 'agoric1fakeLCAAddress' }, - cosmoshub: { addressPrefix: 'cosmos1test' }, + agoric: { expectedAddress: 'agoric1fakeLCAAddress' }, + cosmoshub: { expectedAddress: 'cosmos1test' }, + osmosis: { expectedAddress: 'osmo1test' }, }; const orchestrationAccountScenario = test.macro({ @@ -78,7 +89,7 @@ const orchestrationAccountScenario = test.macro({ const { description, storagePath, subscriber } = publicSubscribers.account; t.regex(description!, /Account holder/); - const expectedStoragePath = `mockChainStorageRoot.basic-flows.${config.addressPrefix}`; + const expectedStoragePath = `mockChainStorageRoot.basic-flows.${config.expectedAddress}`; t.is(storagePath, expectedStoragePath); t.regex(getInterfaceOf(subscriber)!, /Durable Publish Kit subscriber/); @@ -87,3 +98,134 @@ const orchestrationAccountScenario = test.macro({ test(orchestrationAccountScenario, 'agoric'); test(orchestrationAccountScenario, 'cosmoshub'); + +test('send query from chain object', async t => { + const { + bootstrap: { vowTools: vt }, + zoe, + instance, + utils: { inspectDibcBridge }, + } = t.context; + const publicFacet = await E(zoe).getPublicFacet(instance); + const balanceQuery = toRequestQueryJson( + QueryBalanceRequest.toProtoMsg({ + address: 'cosmos1test', + denom: 'uatom', + }), + ); + { + t.log('send query on chain with icqEnabled: true'); + const inv = E(publicFacet).makeSendICQQueryInvitation(); + const userSeat = E(zoe).offer(inv, {}, undefined, { + chainName: 'osmosis', + msgs: [balanceQuery], + }); + const offerResult = await vt.when(E(userSeat).getOfferResult()); + t.log(offerResult); + t.assert(offerResult[0].key, 'base64 encoded response returned'); + const decodedResponse = decodeBalanceQueryResponse(offerResult); + t.deepEqual(decodedResponse, [ + { + balance: { + amount: '0', + denom: 'uatom', + }, + }, + ]); + } + { + t.log('send query on chain with icqEnabled: false'); + const inv = E(publicFacet).makeSendICQQueryInvitation(); + const userSeat = E(zoe).offer(inv, {}, undefined, { + chainName: 'cosmoshub', + msgs: [balanceQuery], + }); + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: 'Queries not available for chain "cosmoshub-4"', + }); + } + { + t.log('sending subsequent queries should not result in new ICQ channels'); + const inv = E(publicFacet).makeSendICQQueryInvitation(); + const userSeat = E(zoe).offer(inv, {}, undefined, { + chainName: 'osmosis', + msgs: [balanceQuery], + }); + await vt.when(E(userSeat).getOfferResult()); + const { bridgeDowncalls } = await inspectDibcBridge(); + + const portBindings = bridgeDowncalls.filter(x => x.method === 'bindPort'); + t.is(portBindings.length, 1, 'only one port bound'); + t.regex(portBindings?.[0]?.packet.source_port, /icqcontroller-/); + const icqChannelInits = bridgeDowncalls.filter( + x => x.method === 'startChannelOpenInit' && x.version === 'icq-1', + ); + t.is(icqChannelInits.length, 1, 'only one ICQ channel opened'); + const sendPacketCalls = bridgeDowncalls.filter( + x => + x.method === 'sendPacket' && x.packet.source_port === 'icqcontroller-1', + ); + t.is(sendPacketCalls.length, 2, 'sent two queries'); + } +}); + +test('send query from orch account in an async-flow', async t => { + const { + bootstrap: { vowTools: vt }, + zoe, + instance, + utils: { inspectDibcBridge }, + } = t.context; + const publicFacet = await E(zoe).getPublicFacet(instance); + + { + t.log('send query from orchAccount on chain with icqEnabled: true'); + const inv = E(publicFacet).makeAccountAndSendBalanceQueryInvitation(); + const userSeat = E(zoe).offer(inv, {}, undefined, { + chainName: 'osmosis', + denom: 'uatom', + }); + const offerResult = await vt.when(E(userSeat).getOfferResult()); + t.deepEqual(offerResult, { + value: 0n, + denom: 'uatom', + }); + } + { + t.log('send query from orchAccount that times out'); + const inv = E(publicFacet).makeAccountAndSendBalanceQueryInvitation(); + const userSeat = E(zoe).offer(inv, {}, undefined, { + chainName: 'osmosis', + denom: 'notarealdenom', + }); + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: 'ABCI code: 5: error handling packet: see events for details', + }); + } + { + t.log('send query from orchAccount on chain with icqEnabled: false'); + const inv = E(publicFacet).makeAccountAndSendBalanceQueryInvitation(); + const userSeat = E(zoe).offer(inv, {}, undefined, { + chainName: 'cosmoshub', + denom: 'uatom,', + }); + await t.throwsAsync(vt.when(E(userSeat).getOfferResult()), { + message: 'Queries not available for chain "cosmoshub-4"', + }); + } + + t.log("creating add'l account should not result in new ICQ channels"); + const { bridgeDowncalls } = await inspectDibcBridge(); + const icqPortBindings = bridgeDowncalls.filter( + x => + x.method === 'bindPort' && x.packet.source_port.includes('icqcontroller'), + ); + t.is(icqPortBindings.length, 1, 'only one icq port bound'); + const icqChannelInits = bridgeDowncalls.filter( + x => x.method === 'startChannelOpenInit' && x.version === 'icq-1', + ); + t.is(icqChannelInits.length, 1, 'only one ICQ channel opened'); +}); + +// needs design? +test.todo('send query LocalChainFacade'); From 7cbf3ca0ea2fd81cdf53bbe8d4b39706f2f3d202 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Mon, 19 Aug 2024 23:27:21 -0400 Subject: [PATCH 5/6] types: Chain.query() type --- packages/orchestration/src/cosmos-api.ts | 10 +++++++++- .../orchestration/src/examples/basic-flows.flows.js | 1 - packages/orchestration/src/orchestration-api.ts | 3 +++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/orchestration/src/cosmos-api.ts b/packages/orchestration/src/cosmos-api.ts index 06c4587e404..ea2a2424a58 100644 --- a/packages/orchestration/src/cosmos-api.ts +++ b/packages/orchestration/src/cosmos-api.ts @@ -1,4 +1,4 @@ -import type { AnyJson, TypedJson } from '@agoric/cosmic-proto'; +import type { AnyJson, TypedJson, JsonSafe } from '@agoric/cosmic-proto'; import type { Delegation, Redelegation, @@ -11,6 +11,10 @@ import type { Order, } from '@agoric/cosmic-proto/ibc/core/channel/v1/channel.js'; import type { State as IBCConnectionState } from '@agoric/cosmic-proto/ibc/core/connection/v1/connection.js'; +import type { + RequestQuery, + ResponseQuery, +} from '@agoric/cosmic-proto/tendermint/abci/types.js'; import type { Brand, Purse, Payment, Amount } from '@agoric/ertp/src/types.js'; import type { Port } from '@agoric/network'; import type { IBCChannelID, IBCConnectionID } from '@agoric/vats'; @@ -265,3 +269,7 @@ export type CosmosChainAccountMethods = } ? StakingAccountActions : {}; + +export type ICQQueryFunction = ( + msgs: JsonSafe[], +) => Promise[]>; diff --git a/packages/orchestration/src/examples/basic-flows.flows.js b/packages/orchestration/src/examples/basic-flows.flows.js index d0bf98c9d9a..f3664bfcc15 100644 --- a/packages/orchestration/src/examples/basic-flows.flows.js +++ b/packages/orchestration/src/examples/basic-flows.flows.js @@ -99,7 +99,6 @@ export const sendQuery = async (orch, _ctx, seat, { chainName, msgs }) => { mustMatch(chainName, M.string()); if (chainName === 'agoric') throw Fail`ICQ not supported on local chain`; const remoteChain = await orch.getChain(chainName); - // @ts-expect-error FIXME implement Chain.query const queryResponse = await remoteChain.query(msgs); console.debug('sendQuery response:', queryResponse); return queryResponse; diff --git a/packages/orchestration/src/orchestration-api.ts b/packages/orchestration/src/orchestration-api.ts index 3f9bc0aa37b..8bc5aa2abec 100644 --- a/packages/orchestration/src/orchestration-api.ts +++ b/packages/orchestration/src/orchestration-api.ts @@ -18,6 +18,7 @@ import type { IBCMsgTransferOptions, KnownChains, LocalAccountMethods, + ICQQueryFunction, } from './types.js'; import type { ResolvedContinuingOfferResult } from './utils/zoe-tools.js'; @@ -88,6 +89,8 @@ export interface Chain { makeAccount: () => Promise>; // FUTURE supply optional port object; also fetch port object + query: CI extends { icqEnabled: true } ? ICQQueryFunction : never; + // TODO provide a way to get the local denom/brand/whatever for this chain } From 8c28e67978bdf0e57e62c391b2606b6f08217aab Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Wed, 21 Aug 2024 13:00:54 -0400 Subject: [PATCH 6/6] test: icqConnection flow is restartable in async-flow --- packages/boot/test/orchestration/restart-contracts.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/boot/test/orchestration/restart-contracts.test.ts b/packages/boot/test/orchestration/restart-contracts.test.ts index dd02d8e08ad..e6ef7216b6c 100644 --- a/packages/boot/test/orchestration/restart-contracts.test.ts +++ b/packages/boot/test/orchestration/restart-contracts.test.ts @@ -229,7 +229,7 @@ test.serial('basicFlows', async t => { }, proposal: {}, offerArgs: { - chainNames: ['agoric', 'cosmoshub'], + chainNames: ['agoric', 'cosmoshub', 'osmosis'], }, }); // no errors and no result yet @@ -242,7 +242,8 @@ test.serial('basicFlows', async t => { result: undefined, // no property }, }); - t.is(getInboundQueueLength(), 2); + // 3x ICA Channel Opens, 1x ICQ Channel Open + t.is(getInboundQueueLength(), 4); t.log('restart basicFlows'); await evalProposal( @@ -264,7 +265,7 @@ test.serial('basicFlows', async t => { result: 'UNPUBLISHED', }, }); - t.is(await flushInboundQueue(1), 1); + t.is(await flushInboundQueue(3), 3); t.like(wallet.getLatestUpdateRecord(), { status: { id: id2,