From f4342f1fc49b1dd4bb7e64583ea1be28c33c5d1c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 19 Jun 2024 10:18:53 -0700 Subject: [PATCH 01/12] docs: ref issues --- packages/orchestration/src/exos/chain-account-kit.js | 2 +- packages/orchestration/src/exos/local-chain-facade.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index 1189055a5cb..f61c4d160c1 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -169,7 +169,7 @@ export const prepareChainAccountKit = zone => }, async onClose(_connection, reason) { trace(`ICA Channel closed. Reason: ${reason}`); - // FIXME handle connection closing + // FIXME handle connection closing https://github.com/Agoric/agoric-sdk/issues/9192 // XXX is there a scenario where a connection will unexpectedly close? _I think yes_ }, async onReceive(connection, bytes) { diff --git a/packages/orchestration/src/exos/local-chain-facade.js b/packages/orchestration/src/exos/local-chain-facade.js index 870493c35c8..1c0afc12597 100644 --- a/packages/orchestration/src/exos/local-chain-facade.js +++ b/packages/orchestration/src/exos/local-chain-facade.js @@ -55,7 +55,7 @@ export const prepareLocalChainFacade = ( chainId: localChainInfo.chainId, addressEncoding: 'bech32', }), - // FIXME storage path + // FIXME storage path https://github.com/Agoric/agoric-sdk/issues/9066 storageNode, }); From 724d1fb24c713a75f56db818e11433131ace541d Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 12 Jun 2024 07:33:04 -0700 Subject: [PATCH 02/12] feat: getConnectionInfo by obj or string --- .../src/exos/local-orchestration-account.js | 4 +- .../src/proposals/start-stakeAtom.js | 5 +-- packages/orchestration/src/utils/chainHub.js | 8 ++-- .../orchestration/test/utils/chainHub.test.ts | 44 +++++++++++++++++++ 4 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 packages/orchestration/test/utils/chainHub.test.ts diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 045c176a4c2..579d27adc07 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -252,8 +252,8 @@ export const prepareLocalOrchestrationAccountKit = ( const agoricChainInfo = await chainHub.getChainInfo('agoric'); const { transferChannel } = await chainHub.getConnectionInfo( - agoricChainInfo.chainId, - destination.chainId, + agoricChainInfo, + destination, ); await null; diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js index f0150404470..1fd954c52e1 100644 --- a/packages/orchestration/src/proposals/start-stakeAtom.js +++ b/packages/orchestration/src/proposals/start-stakeAtom.js @@ -48,10 +48,7 @@ export const startStakeAtom = async ({ const agoric = await chainHub.getChainInfo('agoric'); const cosmoshub = await chainHub.getChainInfo('cosmoshub'); - const connectionInfo = await chainHub.getConnectionInfo( - agoric.chainId, - cosmoshub.chainId, - ); + const connectionInfo = await chainHub.getConnectionInfo(agoric, cosmoshub); /** @type {StartUpgradableOpts} */ const startOpts = { diff --git a/packages/orchestration/src/utils/chainHub.js b/packages/orchestration/src/utils/chainHub.js index 11323e4c090..c85fd1d0308 100644 --- a/packages/orchestration/src/utils/chainHub.js +++ b/packages/orchestration/src/utils/chainHub.js @@ -114,11 +114,13 @@ export const makeChainHub = (agoricNames, zone = makeHeapZone()) => { }, /** - * @param {string} chainId1 - * @param {string} chainId2 + * @param {string | { chainId: string }} chain1 + * @param {string | { chainId: string }} chain2 * @returns {Promise} */ - async getConnectionInfo(chainId1, chainId2) { + async getConnectionInfo(chain1, chain2) { + const chainId1 = typeof chain1 === 'string' ? chain1 : chain1.chainId; + const chainId2 = typeof chain2 === 'string' ? chain2 : chain2.chainId; const key = connectionKey(chainId1, chainId2); if (connectionInfos.has(key)) { return connectionInfos.get(key); diff --git a/packages/orchestration/test/utils/chainHub.test.ts b/packages/orchestration/test/utils/chainHub.test.ts new file mode 100644 index 00000000000..4f02ad81afa --- /dev/null +++ b/packages/orchestration/test/utils/chainHub.test.ts @@ -0,0 +1,44 @@ +/* eslint-disable @jessie.js/safe-await-separator -- XXX irrelevant for tests */ +import test from '@endo/ses-ava/prepare-endo.js'; + +import { makeNameHubKit } from '@agoric/vats'; +import { makeChainHub } from '../../src/utils/chainHub.js'; + +const connection = { + id: 'connection-1', + client_id: '07-tendermint-3', + counterparty: { + client_id: '07-tendermint-2', + connection_id: 'connection-1', + prefix: { + key_prefix: '', + }, + }, + state: 3 /* IBCConnectionState.STATE_OPEN */, + transferChannel: { + portId: 'transfer', + channelId: 'channel-1', + counterPartyChannelId: 'channel-1', + counterPartyPortId: 'transfer', + ordering: 1 /* Order.ORDER_UNORDERED */, + state: 3 /* IBCConnectionState.STATE_OPEN */, + version: 'ics20-1', + }, +} as const; + +test('getConnectionInfo', async t => { + const { nameHub } = makeNameHubKit(); + const chainHub = makeChainHub(nameHub); + + const aChain = { chainId: 'a-1' }; + const bChain = { chainId: 'b-2' }; + + chainHub.registerConnection(aChain.chainId, bChain.chainId, connection); + + // Look up by string or info object + t.deepEqual( + await chainHub.getConnectionInfo(aChain.chainId, bChain.chainId), + connection, + ); + t.deepEqual(await chainHub.getConnectionInfo(aChain, bChain), connection); +}); From 28838b6a6bcd91dfa6e27ad91e6af4666dcb6134 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 18 Jun 2024 10:05:47 -0700 Subject: [PATCH 03/12] feat: getChainsAndConnection helper --- .../src/exos/local-orchestration-account.js | 2 + .../src/proposals/start-stakeAtom.js | 10 +++-- .../src/proposals/start-stakeOsmo.js | 11 +++--- packages/orchestration/src/utils/chainHub.js | 37 ++++++++++++++++--- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 579d27adc07..4be1b0f27b5 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -250,6 +250,8 @@ export const prepareLocalOrchestrationAccountKit = ( // TODO #9211 lookup denom from brand if ('brand' in amount) throw Fail`ERTP Amounts not yet supported`; + // TODO consider using `getChainsAndConnection` but right now it's keyed by chain name + // and we only have chainId for destination. const agoricChainInfo = await chainHub.getChainInfo('agoric'); const { transferChannel } = await chainHub.getConnectionInfo( agoricChainInfo, diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js index 1fd954c52e1..470236c54f8 100644 --- a/packages/orchestration/src/proposals/start-stakeAtom.js +++ b/packages/orchestration/src/proposals/start-stakeAtom.js @@ -1,7 +1,7 @@ import { makeTracer } from '@agoric/internal'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; import { E } from '@endo/far'; -import { makeChainHub } from '../utils/chainHub.js'; +import { getChainsAndConnection, makeChainHub } from '../utils/chainHub.js'; /** * @import {IBCConnectionID} from '@agoric/vats'; @@ -46,9 +46,11 @@ export const startStakeAtom = async ({ const chainHub = makeChainHub(await agoricNames); - const agoric = await chainHub.getChainInfo('agoric'); - const cosmoshub = await chainHub.getChainInfo('cosmoshub'); - const connectionInfo = await chainHub.getConnectionInfo(agoric, cosmoshub); + const [_, cosmoshub, connectionInfo] = await getChainsAndConnection( + chainHub, + 'agoric', + 'cosmoshub', + ); /** @type {StartUpgradableOpts} */ const startOpts = { diff --git a/packages/orchestration/src/proposals/start-stakeOsmo.js b/packages/orchestration/src/proposals/start-stakeOsmo.js index 96c8ea69fbe..d61b73b5ae6 100644 --- a/packages/orchestration/src/proposals/start-stakeOsmo.js +++ b/packages/orchestration/src/proposals/start-stakeOsmo.js @@ -1,7 +1,7 @@ import { makeTracer } from '@agoric/internal'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; import { E } from '@endo/far'; -import { makeChainHub } from '../utils/chainHub.js'; +import { getChainsAndConnection, makeChainHub } from '../utils/chainHub.js'; /** * @import {IBCConnectionID} from '@agoric/vats'; @@ -47,11 +47,10 @@ export const startStakeOsmo = async ({ const chainHub = makeChainHub(await agoricNames); - const agoric = await chainHub.getChainInfo('agoric'); - const osmosis = await chainHub.getChainInfo('osmosis'); - const connectionInfo = await chainHub.getConnectionInfo( - agoric.chainId, - osmosis.chainId, + const [_, osmosis, connectionInfo] = await getChainsAndConnection( + chainHub, + 'agoric', + 'osmosis', ); /** @type {StartUpgradableOpts} */ diff --git a/packages/orchestration/src/utils/chainHub.js b/packages/orchestration/src/utils/chainHub.js index c85fd1d0308..6e7a0387239 100644 --- a/packages/orchestration/src/utils/chainHub.js +++ b/packages/orchestration/src/utils/chainHub.js @@ -13,6 +13,13 @@ const { Fail } = assert; * @import {Zone} from '@agoric/zone'; */ +/** + * @template {string} K + * @typedef {K extends keyof KnownChains + * ? Omit + * : ChainInfo} ActualChainInfo + */ + /** agoricNames key for ChainInfo hub */ export const CHAIN_KEY = 'chain'; /** namehub for connection info */ @@ -82,11 +89,7 @@ export const makeChainHub = (agoricNames, zone = makeHeapZone()) => { /** * @template {string} K * @param {K} chainName - * @returns {Promise< - * K extends keyof KnownChains - * ? Omit - * : ChainInfo - * >} + * @returns {Promise>} */ async getChainInfo(chainName) { // Either from registerChain or memoized remote lookup() @@ -177,3 +180,27 @@ export const registerChain = async ( // Bundle to pipeline IO await Promise.all(promises); }; + +/** + * @template {string} C1 + * @template {string} C2 + * @param {ChainHub} chainHub + * @param {C1} chainName1 + * @param {C2} chainName2 + * @returns {Promise< + * [ActualChainInfo, ActualChainInfo, IBCConnectionInfo] + * >} + */ +export const getChainsAndConnection = async ( + chainHub, + chainName1, + chainName2, +) => { + const [chain1, chain2] = await Promise.all([ + chainHub.getChainInfo(chainName1), + chainHub.getChainInfo(chainName2), + ]); + const connectionInfo = await chainHub.getConnectionInfo(chain2, chain1); + + return [chain1, chain2, connectionInfo]; +}; From fc3323835cfe4ef39637a11a1cc24766ce86bc58 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 7 Jun 2024 16:13:50 -0400 Subject: [PATCH 04/12] chore(orchestation): include c8 coverage script --- packages/orchestration/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/orchestration/package.json b/packages/orchestration/package.json index 655e32a1688..91fffe2fc3f 100644 --- a/packages/orchestration/package.json +++ b/packages/orchestration/package.json @@ -14,6 +14,7 @@ "prepack": "tsc --build tsconfig.build.json", "postpack": "git clean -f '*.d.ts*'", "test": "ava", + "test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js", "test:xs": "exit 0", "lint": "run-s --continue-on-error lint:*", "lint:types": "tsc", @@ -57,6 +58,7 @@ "@cosmjs/proto-signing": "^0.32.3", "@endo/ses-ava": "^1.2.2", "ava": "^5.3.1", + "c8": "^9.1.0", "prettier": "^3.3.2" }, "ava": { From ed2317cdaafe748a55769496a828c910be6ad158 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 19 Jun 2024 11:33:52 -0700 Subject: [PATCH 05/12] chore(types): VowTools in tools.js --- packages/vow/src/tools.js | 2 ++ packages/vow/src/types.js | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index 7351457b6a5..a632dda4c81 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -31,3 +31,5 @@ export const prepareVowTools = (zone, powers = {}) => { return harden({ when, watch, makeVowKit, allVows }); }; harden(prepareVowTools); + +/** @typedef {ReturnType} VowTools */ diff --git a/packages/vow/src/types.js b/packages/vow/src/types.js index f76fa76a3b1..2a381f174d7 100644 --- a/packages/vow/src/types.js +++ b/packages/vow/src/types.js @@ -86,5 +86,3 @@ export {}; * @property {(value: T, context?: C) => Vow | PromiseVow | TResult1} [onFulfilled] * @property {(reason: any) => Vow | PromiseVow | TResult2} [onRejected] */ - -/** @typedef {ReturnType} VowTools */ From ad29545174c43d481f1be7dbc6ce848c893a89e7 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 19 Jun 2024 11:37:23 -0700 Subject: [PATCH 06/12] test: provide vowTools --- packages/orchestration/test/network-fakes.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/orchestration/test/network-fakes.ts b/packages/orchestration/test/network-fakes.ts index 92a2f77ac68..62d0d7a5011 100644 --- a/packages/orchestration/test/network-fakes.ts +++ b/packages/orchestration/test/network-fakes.ts @@ -92,26 +92,25 @@ export const prepareProtocolHandler = ( }; export const fakeNetworkEchoStuff = (zone: Zone) => { - const powers = prepareVowTools(zone); - const { makeVowKit, when } = powers; + const vowTools = prepareVowTools(zone); + const { makeVowKit, when } = vowTools; - const makeNetworkProtocol = prepareNetworkProtocol(zone, powers); + const makeNetworkProtocol = prepareNetworkProtocol(zone, vowTools); const makeEchoConnectionHandler = prepareEchoConnectionKit(zone); const makeProtocolHandler = prepareProtocolHandler( zone, makeEchoConnectionHandler, - powers, + vowTools, ); const protocol = makeNetworkProtocol(makeProtocolHandler()); - const makePortAllocator = preparePortAllocator(zone, powers); + const makePortAllocator = preparePortAllocator(zone, vowTools); const portAllocator = makePortAllocator({ protocol }); return { makeEchoConnectionHandler, - makeVowKit, portAllocator, protocol, - when, + vowTools, }; }; From bc3bc6b6cf9f0a7017f5b17adf5e57c300f7e6fe Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 4 Jun 2024 18:02:45 -0400 Subject: [PATCH 07/12] feat(orchestration): icqConnectionKit returns unwrapped vows - refs: #9449 --- .../src/exos/icq-connection-kit.js | 32 +++++++++--- packages/orchestration/src/service.js | 12 +++-- .../orchestration/src/vat-orchestration.js | 3 ++ packages/orchestration/test/service.test.ts | 50 +++++++++++++++++++ packages/orchestration/test/supports.ts | 6 ++- 5 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 packages/orchestration/test/service.test.ts diff --git a/packages/orchestration/src/exos/icq-connection-kit.js b/packages/orchestration/src/exos/icq-connection-kit.js index 6c29d491ce3..4b89efdacfe 100644 --- a/packages/orchestration/src/exos/icq-connection-kit.js +++ b/packages/orchestration/src/exos/icq-connection-kit.js @@ -1,7 +1,7 @@ /** @file ICQConnection Exo */ import { NonNullish } from '@agoric/assert'; import { makeTracer } from '@agoric/internal'; -import { V as E } from '@agoric/vow/vat.js'; +import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { makeQueryPacket, parseQueryPacket } from '../utils/packet.js'; import { ConnectionHandlerI } from '../typeGuards.js'; @@ -9,7 +9,7 @@ import { ConnectionHandlerI } from '../typeGuards.js'; /** * @import {Zone} from '@agoric/base-zone'; * @import {Connection, Port} from '@agoric/network'; - * @import {Remote} from '@agoric/vow'; + * @import {Remote, 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'; @@ -29,6 +29,12 @@ export const ICQConnectionI = M.interface('ICQConnection', { query: M.call(M.arrayOf(ICQMsgShape)).returns(M.promise()), }); +const HandleQueryWatcherI = M.interface('HandleQueryWatcher', { + onFulfilled: M.call(M.string()) + .optional(M.arrayOf(M.undefined())) // does not need watcherContext + .returns(M.arrayOf(M.record())), +}); + /** * @typedef {{ * port: Port; @@ -52,11 +58,16 @@ export const ICQConnectionI = M.interface('ICQConnection', { * sending queries and handling connection events. * * @param {Zone} zone + * @param {VowTools} vowTools */ -export const prepareICQConnectionKit = zone => +export const prepareICQConnectionKit = (zone, { watch, when }) => zone.exoClassKit( 'ICQConnectionKit', - { connection: ICQConnectionI, connectionHandler: ConnectionHandlerI }, + { + connection: ICQConnectionI, + handleQueryWatcher: HandleQueryWatcherI, + connectionHandler: ConnectionHandlerI, + }, /** * @param {Port} port */ @@ -89,13 +100,18 @@ export const prepareICQConnectionKit = zone => query(msgs) { const { connection } = this.state; if (!connection) throw Fail`connection not available`; - return E.when( - E(connection).send(makeQueryPacket(msgs)), - // if parseTxPacket cannot find a `result` key, it throws - ack => parseQueryPacket(ack), + return when( + watch( + E(connection).send(makeQueryPacket(msgs)), + this.facets.handleQueryWatcher, + ), ); }, }, + handleQueryWatcher: { + /** @param {string} ack packet acknowledgement string */ + onFulfilled: ack => parseQueryPacket(ack), + }, connectionHandler: { /** * @param {Remote} connection diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index 39fc4fa9c41..fe6c27b4ae2 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -15,6 +15,7 @@ import { * @import {Remote} from '@agoric/internal'; * @import {Port, PortAllocator} from '@agoric/network'; * @import {IBCConnectionID} from '@agoric/vats'; + * @import {VowTools} from '@agoric/vow'; * @import {ICQConnection, IcaAccount, ICQConnectionKit} from './types.js'; */ @@ -104,7 +105,7 @@ const prepareOrchestrationKit = ( }, async allocateICQControllerPort() { const portAllocator = getPower(this.state.powers, 'portAllocator'); - return E(portAllocator).allocateICAControllerPort(); + return E(portAllocator).allocateICQControllerPort(); }, }, public: { @@ -168,10 +169,13 @@ const prepareOrchestrationKit = ( }, ); -/** @param {Zone} zone */ -export const prepareOrchestrationTools = zone => { +/** + * @param {Zone} zone + * @param {VowTools} vowTools + */ +export const prepareOrchestrationTools = (zone, vowTools) => { const makeChainAccountKit = prepareChainAccountKit(zone); - const makeICQConnectionKit = prepareICQConnectionKit(zone); + const makeICQConnectionKit = prepareICQConnectionKit(zone, vowTools); const makeOrchestrationKit = prepareOrchestrationKit( zone, makeChainAccountKit, diff --git a/packages/orchestration/src/vat-orchestration.js b/packages/orchestration/src/vat-orchestration.js index 675911ab7c5..8f3de0b1cc2 100644 --- a/packages/orchestration/src/vat-orchestration.js +++ b/packages/orchestration/src/vat-orchestration.js @@ -1,4 +1,5 @@ import { Far } from '@endo/far'; +import { prepareVowTools } from '@agoric/vow/vat.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { prepareOrchestrationTools } from './service.js'; @@ -6,8 +7,10 @@ import { prepareOrchestrationTools } from './service.js'; export const buildRootObject = (_vatPowers, _args, baggage) => { const zone = makeDurableZone(baggage); + const vowTools = prepareVowTools(zone.subZone('VowTools')); const { makeOrchestrationKit } = prepareOrchestrationTools( zone.subZone('orchestration'), + vowTools, ); return Far('OrchestrationVat', { diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts new file mode 100644 index 00000000000..a91f427a591 --- /dev/null +++ b/packages/orchestration/test/service.test.ts @@ -0,0 +1,50 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { toRequestQueryJson } from '@agoric/cosmic-proto'; +import { QueryBalanceRequest } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; +import { E } from '@endo/far'; +import { commonSetup } from './supports.js'; + +test('makeICQConnection returns an ICQConnection', async t => { + const { + bootstrap: { orchestration }, + } = await commonSetup(t); + + const CONNECTION_ID = 'connection-0'; + + const icqConnection = + await E(orchestration).provideICQConnection(CONNECTION_ID); + const [localAddr, remoteAddr] = await Promise.all([ + E(icqConnection).getLocalAddress(), + E(icqConnection).getRemoteAddress(), + ]); + t.log(icqConnection, { + localAddr, + remoteAddr, + }); + t.regex(localAddr, /ibc-port\/icqcontroller-\d+/); + t.regex( + remoteAddr, + new RegExp(`/ibc-hop/${CONNECTION_ID}`), + 'remote address contains provided connectionId', + ); + t.regex( + remoteAddr, + /icqhost\/unordered\/icq-1/, + 'remote address contains icqhost port, unordered ordering, and icq-1 version string', + ); + + await t.throwsAsync( + E(icqConnection).query([ + toRequestQueryJson( + QueryBalanceRequest.toProtoMsg({ + address: 'cosmos1test', + denom: 'uatom', + }), + ), + ]), + { message: /"data":"(.*)"memo":""/ }, + 'TODO do not use echo connection', + ); +}); + +test.todo('makeAccount'); diff --git a/packages/orchestration/test/supports.ts b/packages/orchestration/test/supports.ts index b89b482ee19..ed6b5756099 100644 --- a/packages/orchestration/test/supports.ts +++ b/packages/orchestration/test/supports.ts @@ -104,11 +104,12 @@ export const commonSetup = async t => { sequence: false, }); + const { portAllocator } = fakeNetworkEchoStuff(rootZone.subZone('network')); + const { makeOrchestrationKit } = prepareOrchestrationTools( rootZone.subZone('orchestration'), + vowTools, ); - - const { portAllocator } = fakeNetworkEchoStuff(rootZone.subZone('network')); const { public: orchestration } = makeOrchestrationKit({ portAllocator }); await registerChainNamespace(agoricNamesAdmin, () => {}); @@ -125,6 +126,7 @@ export const commonSetup = async t => { // TODO remove; bootstrap doesn't have a zone rootZone: rootZone.subZone('contract'), storage, + vowTools, }, brands: { bld: bldSansMint, From 778eb42f518754481a6ef310deba2b182cda24a2 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 4 Jun 2024 19:48:06 -0400 Subject: [PATCH 08/12] feat(orchestration): service.js returns unwrapped vows - refs: #9449 --- .../src/exos/icq-connection-kit.js | 8 +- packages/orchestration/src/service.js | 193 ++++++++++++------ packages/orchestration/test/service.test.ts | 69 ++++++- 3 files changed, 199 insertions(+), 71 deletions(-) diff --git a/packages/orchestration/src/exos/icq-connection-kit.js b/packages/orchestration/src/exos/icq-connection-kit.js index 4b89efdacfe..238b1e79ac3 100644 --- a/packages/orchestration/src/exos/icq-connection-kit.js +++ b/packages/orchestration/src/exos/icq-connection-kit.js @@ -68,9 +68,7 @@ export const prepareICQConnectionKit = (zone, { watch, when }) => handleQueryWatcher: HandleQueryWatcherI, connectionHandler: ConnectionHandlerI, }, - /** - * @param {Port} port - */ + /** @param {Port} port */ port => /** @type {ICQConnectionKitState} */ ({ port, @@ -110,7 +108,9 @@ export const prepareICQConnectionKit = (zone, { watch, when }) => }, handleQueryWatcher: { /** @param {string} ack packet acknowledgement string */ - onFulfilled: ack => parseQueryPacket(ack), + onFulfilled(ack) { + return parseQueryPacket(ack); + }, }, connectionHandler: { /** diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index fe6c27b4ae2..1ecbbcbe7d8 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -1,6 +1,6 @@ /** @file Orchestration service */ -import { V as E } from '@agoric/vow/vat.js'; +import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { Shape as NetworkShape } from '@agoric/network'; import { prepareChainAccountKit } from './exos/chain-account-kit.js'; @@ -13,10 +13,11 @@ import { /** * @import {Zone} from '@agoric/base-zone'; * @import {Remote} from '@agoric/internal'; - * @import {Port, PortAllocator} from '@agoric/network'; + * @import {Connection, Port, PortAllocator} from '@agoric/network'; * @import {IBCConnectionID} from '@agoric/vats'; + * @import {RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; * @import {VowTools} from '@agoric/vow'; - * @import {ICQConnection, IcaAccount, ICQConnectionKit} from './types.js'; + * @import {ICQConnection, IcaAccount, ICQConnectionKit, ChainAccountKit} from './types.js'; */ const { Fail, bare } = assert; @@ -37,9 +38,9 @@ const { Fail, bare } = assert; * >} PowerStore */ -/** - * @typedef {MapStore} ICQConnectionStore - */ +/** @typedef {MapStore} ICQConnectionStore */ + +/** @typedef {ChainAccountKit | ICQConnectionKit} ConnectionKit */ /** * @template {keyof OrchestrationPowers} K @@ -51,39 +52,54 @@ const getPower = (powers, name) => { return /** @type {OrchestrationPowers[K]} */ (powers.get(name)); }; -export const OrchestrationI = M.interface('Orchestration', { - makeAccount: M.callWhen(M.string(), M.string(), M.string()).returns( - M.remotable('ChainAccount'), - ), - provideICQConnection: M.callWhen(M.string()).returns( - M.remotable('Connection'), - ), -}); - /** @typedef {{ powers: PowerStore; icqConnections: ICQConnectionStore }} OrchestrationState */ /** * @param {Zone} zone + * @param {VowTools} vowTools * @param {ReturnType} makeChainAccountKit * @param {ReturnType} makeICQConnectionKit */ const prepareOrchestrationKit = ( zone, + { when, watch }, makeChainAccountKit, makeICQConnectionKit, ) => zone.exoClassKit( 'Orchestration', { - self: M.interface('OrchestrationSelf', { - allocateICAControllerPort: M.callWhen().returns( - NetworkShape.Vow$(NetworkShape.Port), + requestICAConnectionWatcher: M.interface('RequestICAConnectionWatcher', { + onFulfilled: M.call(M.remotable('Port')) + .optional({ chainId: M.string(), remoteConnAddr: M.string() }) + .returns(NetworkShape.Vow$(NetworkShape.Connection)), + }), + requestICQConnectionWatcher: M.interface('RequestICQConnectionWatcher', { + onFulfilled: M.call(M.remotable('Port')) + .optional({ + remoteConnAddr: M.string(), + controllerConnectionId: M.string(), + }) + .returns(NetworkShape.Vow$(NetworkShape.Connection)), + }), + channelOpenWatcher: M.interface('ChannelOpenWatcher', { + onFulfilled: M.call(M.remotable('Connection')) + .optional( + M.splitRecord( + { connectionKit: M.record(), returnFacet: M.string() }, + { saveICQConnection: M.string() }, + ), + ) + .returns(M.remotable('ConnectionKit Holder facet')), + }), + public: M.interface('OrchestrationService', { + makeAccount: M.callWhen(M.string(), M.string(), M.string()).returns( + M.remotable('ChainAccountKit'), ), - allocateICQControllerPort: M.callWhen().returns( - NetworkShape.Vow$(NetworkShape.Port), + provideICQConnection: M.callWhen(M.string()).returns( + M.remotable('ICQConnection'), ), }), - public: OrchestrationI, }, /** @param {Partial} [initialPowers] */ initialPowers => { @@ -98,16 +114,78 @@ const prepareOrchestrationKit = ( return /** @type {OrchestrationState} */ ({ powers, icqConnections }); }, { - self: { - async allocateICAControllerPort() { - const portAllocator = getPower(this.state.powers, 'portAllocator'); - return E(portAllocator).allocateICAControllerPort(); + requestICAConnectionWatcher: { + /** + * @param {Port} port + * @param {{ + * chainId: string; + * remoteConnAddr: RemoteIbcAddress; + * }} watchContext + */ + onFulfilled(port, { chainId, remoteConnAddr }) { + const chainAccountKit = makeChainAccountKit( + chainId, + port, + remoteConnAddr, + ); + return watch( + E(port).connect(remoteConnAddr, chainAccountKit.connectionHandler), + this.facets.channelOpenWatcher, + { returnFacet: 'account', connectionKit: chainAccountKit }, + ); }, - async allocateICQControllerPort() { - const portAllocator = getPower(this.state.powers, 'portAllocator'); - return E(portAllocator).allocateICQControllerPort(); + }, + requestICQConnectionWatcher: { + /** + * @param {Port} port + * @param {{ + * remoteConnAddr: RemoteIbcAddress; + * controllerConnectionId: IBCConnectionID; + * }} watchContext + */ + onFulfilled(port, { remoteConnAddr, controllerConnectionId }) { + const connectionKit = makeICQConnectionKit(port); + /** @param {ICQConnectionKit} kit */ + return watch( + E(port).connect(remoteConnAddr, connectionKit.connectionHandler), + this.facets.channelOpenWatcher, + { + connectionKit, + returnFacet: 'connection', + saveICQConnection: controllerConnectionId, + }, + ); }, }, + /** + * Waits for a channel (ICA, ICQ) to open and returns the consumer-facing + * facet of the ConnectionKit, specified by `returnFacet`. Saves the + * ConnectionKit if `saveICQConnection` is provided. + */ + channelOpenWatcher: { + /** + * @param {Connection} _connection + * @param {{ + * connectionKit: ConnectionKit; + * returnFacet: string; + * saveICQConnection?: IBCConnectionID; + * }} watchContext + */ + onFulfilled( + _connection, + { connectionKit, returnFacet, saveICQConnection }, + ) { + if (saveICQConnection) { + this.state.icqConnections.init( + saveICQConnection, + /** @type {ICQConnectionKit} */ (connectionKit), + ); + } + return connectionKit[returnFacet]; + }, + // TODO #9317 if we fail, should we revoke the port (if it was created in this flow)? + // onRejected() {} + }, public: { /** * @param {string} chainId @@ -116,54 +194,46 @@ const prepareOrchestrationKit = ( * @param {IBCConnectionID} controllerConnectionId self connection_id * @returns {Promise} */ - async makeAccount(chainId, hostConnectionId, controllerConnectionId) { - const port = await this.facets.self.allocateICAControllerPort(); - + makeAccount(chainId, hostConnectionId, controllerConnectionId) { const remoteConnAddr = makeICAChannelAddress( hostConnectionId, controllerConnectionId, ); - const chainAccountKit = makeChainAccountKit( - chainId, - port, - remoteConnAddr, - ); - - // await so we do not return a ChainAccount before it successfully instantiates - await E(port).connect( - remoteConnAddr, - chainAccountKit.connectionHandler, + const portAllocator = getPower(this.state.powers, 'portAllocator'); + return when( + watch( + E(portAllocator).allocateICAControllerPort(), + this.facets.requestICAConnectionWatcher, + { + chainId, + remoteConnAddr, + }, + ), ); - // FIXME if we fail, should we close the port (if it was created in this flow)? - return chainAccountKit.account; }, /** * @param {IBCConnectionID} controllerConnectionId - * @returns {Promise} + * @returns {ICQConnection | Promise} */ - async provideICQConnection(controllerConnectionId) { + provideICQConnection(controllerConnectionId) { if (this.state.icqConnections.has(controllerConnectionId)) { return this.state.icqConnections.get(controllerConnectionId) .connection; } - // allocate a new Port for every Connection - // TODO #9317 optimize ICQ port allocation - const port = await this.facets.self.allocateICQControllerPort(); const remoteConnAddr = makeICQChannelAddress(controllerConnectionId); - const icqConnectionKit = makeICQConnectionKit(port); - - // await so we do not return/save a ICQConnection before it successfully instantiates - await E(port).connect( - remoteConnAddr, - icqConnectionKit.connectionHandler, - ); - - this.state.icqConnections.init( - controllerConnectionId, - icqConnectionKit, + const portAllocator = getPower(this.state.powers, 'portAllocator'); + return when( + watch( + // allocate a new Port for every Connection + // TODO #9317 optimize ICQ port allocation + E(portAllocator).allocateICQControllerPort(), + this.facets.requestICQConnectionWatcher, + { + remoteConnAddr, + controllerConnectionId, + }, + ), ); - - return icqConnectionKit.connection; }, }, }, @@ -178,6 +248,7 @@ export const prepareOrchestrationTools = (zone, vowTools) => { const makeICQConnectionKit = prepareICQConnectionKit(zone, vowTools); const makeOrchestrationKit = prepareOrchestrationKit( zone, + vowTools, makeChainAccountKit, makeICQConnectionKit, ); diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts index a91f427a591..fa6405446a3 100644 --- a/packages/orchestration/test/service.test.ts +++ b/packages/orchestration/test/service.test.ts @@ -1,7 +1,9 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { E } from '@endo/far'; import { toRequestQueryJson } from '@agoric/cosmic-proto'; import { QueryBalanceRequest } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; -import { E } from '@endo/far'; +import { MsgDelegate } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; +import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js'; import { commonSetup } from './supports.js'; test('makeICQConnection returns an ICQConnection', async t => { @@ -9,10 +11,11 @@ test('makeICQConnection returns an ICQConnection', async t => { bootstrap: { orchestration }, } = await commonSetup(t); - const CONNECTION_ID = 'connection-0'; + const CONTROLLER_CONNECTION_ID = 'connection-0'; - const icqConnection = - await E(orchestration).provideICQConnection(CONNECTION_ID); + const icqConnection = await E(orchestration).provideICQConnection( + CONTROLLER_CONNECTION_ID, + ); const [localAddr, remoteAddr] = await Promise.all([ E(icqConnection).getLocalAddress(), E(icqConnection).getRemoteAddress(), @@ -24,7 +27,7 @@ test('makeICQConnection returns an ICQConnection', async t => { t.regex(localAddr, /ibc-port\/icqcontroller-\d+/); t.regex( remoteAddr, - new RegExp(`/ibc-hop/${CONNECTION_ID}`), + new RegExp(`/ibc-hop/${CONTROLLER_CONNECTION_ID}`), 'remote address contains provided connectionId', ); t.regex( @@ -47,4 +50,58 @@ test('makeICQConnection returns an ICQConnection', async t => { ); }); -test.todo('makeAccount'); +test('makeAccount returns a ChainAccount', async t => { + const { + bootstrap: { orchestration }, + } = await commonSetup(t); + + const CHAIN_ID = 'cosmoshub-99'; + const HOST_CONNECTION_ID = 'connection-0'; + const CONTROLLER_CONNECTION_ID = 'connection-1'; + + const account = await E(orchestration).makeAccount( + CHAIN_ID, + HOST_CONNECTION_ID, + CONTROLLER_CONNECTION_ID, + ); + const [localAddr, remoteAddr, chainAddr] = await Promise.all([ + E(account).getLocalAddress(), + E(account).getRemoteAddress(), + E(account).getAddress(), + ]); + t.log(account, { + localAddr, + remoteAddr, + chainAddr, + }); + t.regex(localAddr, /ibc-port\/icacontroller-\d+/); + t.regex( + remoteAddr, + new RegExp(`/ibc-hop/${CONTROLLER_CONNECTION_ID}`), + 'remote address contains provided connectionId', + ); + t.regex( + remoteAddr, + /icahost\/ordered/, + 'remote address contains icahost port, ordered ordering', + ); + t.regex( + remoteAddr, + /"version":"ics27-1"(.*)"encoding":"proto3"/, + 'remote address contains version and encoding in version string', + ); + + await t.throwsAsync( + E(account).executeEncodedTx([ + Any.toJSON( + MsgDelegate.toProtoMsg({ + delegatorAddress: 'cosmos1test', + validatorAddress: 'cosmosvaloper1test', + amount: { denom: 'uatom', amount: '10' }, + }), + ), + ]), + { message: /"type":1(.*)"data":"(.*)"memo":""/ }, + 'TODO do not use echo connection', + ); +}); From 7c99929c73a30beb156512cdc4749e52ceaf608e Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 4 Jun 2024 22:07:17 -0400 Subject: [PATCH 09/12] feat(orchestration): ChainAccountKit returns unwrapped vows - refs: #9449 --- .../src/exos/chain-account-kit.js | 42 +++++++++++++------ packages/orchestration/src/service.js | 2 +- packages/orchestration/test/service.test.ts | 35 ++++++++++++---- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index f61c4d160c1..bc5cc2b8251 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -2,7 +2,7 @@ import { NonNullish } from '@agoric/assert'; import { PurseShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; -import { V as E } from '@agoric/vow/vat.js'; +import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { ChainAddressShape, @@ -15,7 +15,7 @@ import { makeTxPacket, parseTxPacket } from '../utils/packet.js'; /** * @import {Zone} from '@agoric/base-zone'; * @import {Connection, Port} from '@agoric/network'; - * @import {Remote} from '@agoric/vow'; + * @import {Remote, VowTools} from '@agoric/vow'; * @import {AnyJson} from '@agoric/cosmic-proto'; * @import {TxBody} from '@agoric/cosmic-proto/cosmos/tx/v1beta1/tx.js'; * @import {LocalIbcAddress, RemoteIbcAddress} from '@agoric/vats/tools/ibc-utils.js'; @@ -55,11 +55,22 @@ export const ChainAccountI = M.interface('ChainAccount', { * }} State */ -/** @param {Zone} zone */ -export const prepareChainAccountKit = zone => +/** + * @param {Zone} zone + * @param {VowTools} vowTools + */ +export const prepareChainAccountKit = (zone, { watch, when }) => zone.exoClassKit( 'ChainAccountKit', - { account: ChainAccountI, connectionHandler: ConnectionHandlerI }, + { + account: ChainAccountI, + connectionHandler: ConnectionHandlerI, + handleExecuteEncodedTxWatcher: M.interface('HandleQueryWatcher', { + onFulfilled: M.call(M.string()) + .optional(M.arrayOf(M.undefined())) // does not need watcherContext + .returns(M.string()), + }), + }, /** * @param {string} chainId * @param {Port} port @@ -76,6 +87,12 @@ export const prepareChainAccountKit = zone => localAddress: undefined, }), { + handleExecuteEncodedTxWatcher: { + /** @param {string} ack */ + onFulfilled(ack) { + return parseTxPacket(ack); + }, + }, account: { /** @returns {ChainAddress} */ getAddress() { @@ -123,20 +140,21 @@ export const prepareChainAccountKit = zone => executeEncodedTx(msgs, opts) { const { connection } = this.state; if (!connection) throw Fail`connection not available`; - return E.when( - E(connection).send(makeTxPacket(msgs, opts)), - // if parseTxPacket cannot find a `result` key, it throws - ack => parseTxPacket(ack), + return when( + watch( + E(connection).send(makeTxPacket(msgs, opts)), + this.facets.handleExecuteEncodedTxWatcher, + ), ); }, /** Close the remote account */ - async close() { - // FIXME what should the behavior be here? and `onClose`? + close() { + /// TODO #9192 what should the behavior be here? and `onClose`? // - retrieve assets? // - revoke the port? const { connection } = this.state; if (!connection) throw Fail`connection not available`; - await E(connection).close(); + return when(watch(E(connection).close())); }, /** * get Purse for a brand to .withdraw() a Payment from the account diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index 1ecbbcbe7d8..e087193b69a 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -244,7 +244,7 @@ const prepareOrchestrationKit = ( * @param {VowTools} vowTools */ export const prepareOrchestrationTools = (zone, vowTools) => { - const makeChainAccountKit = prepareChainAccountKit(zone); + const makeChainAccountKit = prepareChainAccountKit(zone, vowTools); const makeICQConnectionKit = prepareICQConnectionKit(zone, vowTools); const makeOrchestrationKit = prepareOrchestrationKit( zone, diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts index fa6405446a3..a1ce3f543df 100644 --- a/packages/orchestration/test/service.test.ts +++ b/packages/orchestration/test/service.test.ts @@ -4,7 +4,9 @@ import { toRequestQueryJson } from '@agoric/cosmic-proto'; import { QueryBalanceRequest } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js'; import { MsgDelegate } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; import { Any } from '@agoric/cosmic-proto/google/protobuf/any.js'; +import { matches } from '@endo/patterns'; import { commonSetup } from './supports.js'; +import { ChainAddressShape } from '../src/typeGuards.js'; test('makeICQConnection returns an ICQConnection', async t => { const { @@ -91,17 +93,32 @@ test('makeAccount returns a ChainAccount', async t => { 'remote address contains version and encoding in version string', ); + t.true(matches(chainAddr, ChainAddressShape)); + t.regex( + chainAddr.address, + /UNPARSABLE_CHAIN_ADDRESS/, + 'TODO use mocked ibc connection instead of echo connection', + ); + + const delegateMsg = Any.toJSON( + MsgDelegate.toProtoMsg({ + delegatorAddress: 'cosmos1test', + validatorAddress: 'cosmosvaloper1test', + amount: { denom: 'uatom', amount: '10' }, + }), + ); await t.throwsAsync( - E(account).executeEncodedTx([ - Any.toJSON( - MsgDelegate.toProtoMsg({ - delegatorAddress: 'cosmos1test', - validatorAddress: 'cosmosvaloper1test', - amount: { denom: 'uatom', amount: '10' }, - }), - ), - ]), + E(account).executeEncodedTx([delegateMsg]), { message: /"type":1(.*)"data":"(.*)"memo":""/ }, 'TODO do not use echo connection', ); + + await E(account).close(); + await t.throwsAsync( + E(account).executeEncodedTx([delegateMsg]), + { + message: 'Connection closed', + }, + 'cannot execute transaction if connection is closed', + ); }); From b9a18b02d478f6812d5927f5876d1d7205fedf2e Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 6 Jun 2024 11:28:53 -0400 Subject: [PATCH 10/12] fix: add TODOs for early synchronous returns --- .../src/exos/chain-account-kit.js | 10 +++++++--- .../src/exos/icq-connection-kit.js | 18 +++++++++--------- packages/orchestration/src/service.js | 13 +++++++------ packages/orchestration/test/service.test.ts | 6 ++++++ 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index bc5cc2b8251..6cec4150694 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -65,7 +65,7 @@ export const prepareChainAccountKit = (zone, { watch, when }) => { account: ChainAccountI, connectionHandler: ConnectionHandlerI, - handleExecuteEncodedTxWatcher: M.interface('HandleQueryWatcher', { + parseTxPacketWatcher: M.interface('ParseTxPacketWatcher', { onFulfilled: M.call(M.string()) .optional(M.arrayOf(M.undefined())) // does not need watcherContext .returns(M.string()), @@ -87,7 +87,7 @@ export const prepareChainAccountKit = (zone, { watch, when }) => localAddress: undefined, }), { - handleExecuteEncodedTxWatcher: { + parseTxPacketWatcher: { /** @param {string} ack */ onFulfilled(ack) { return parseTxPacket(ack); @@ -139,11 +139,13 @@ export const prepareChainAccountKit = (zone, { watch, when }) => */ executeEncodedTx(msgs, opts) { 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( E(connection).send(makeTxPacket(msgs, opts)), - this.facets.handleExecuteEncodedTxWatcher, + this.facets.parseTxPacketWatcher, ), ); }, @@ -153,6 +155,8 @@ export const prepareChainAccountKit = (zone, { watch, when }) => // - retrieve assets? // - revoke the port? 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(E(connection).close())); }, diff --git a/packages/orchestration/src/exos/icq-connection-kit.js b/packages/orchestration/src/exos/icq-connection-kit.js index 238b1e79ac3..e54d4599652 100644 --- a/packages/orchestration/src/exos/icq-connection-kit.js +++ b/packages/orchestration/src/exos/icq-connection-kit.js @@ -29,12 +29,6 @@ export const ICQConnectionI = M.interface('ICQConnection', { query: M.call(M.arrayOf(ICQMsgShape)).returns(M.promise()), }); -const HandleQueryWatcherI = M.interface('HandleQueryWatcher', { - onFulfilled: M.call(M.string()) - .optional(M.arrayOf(M.undefined())) // does not need watcherContext - .returns(M.arrayOf(M.record())), -}); - /** * @typedef {{ * port: Port; @@ -65,8 +59,12 @@ export const prepareICQConnectionKit = (zone, { watch, when }) => 'ICQConnectionKit', { connection: ICQConnectionI, - handleQueryWatcher: HandleQueryWatcherI, connectionHandler: ConnectionHandlerI, + parseQueryPacketWatcher: M.interface('ParseQueryPacketWatcher', { + onFulfilled: M.call(M.string()) + .optional(M.arrayOf(M.undefined())) // does not need watcherContext + .returns(M.arrayOf(M.record())), + }), }, /** @param {Port} port */ port => @@ -97,16 +95,18 @@ export const prepareICQConnectionKit = (zone, { watch, when }) => */ 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( E(connection).send(makeQueryPacket(msgs)), - this.facets.handleQueryWatcher, + this.facets.parseQueryPacketWatcher, ), ); }, }, - handleQueryWatcher: { + parseQueryPacketWatcher: { /** @param {string} ack packet acknowledgement string */ onFulfilled(ack) { return parseQueryPacket(ack); diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index e087193b69a..ffaeb936b60 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -69,12 +69,12 @@ const prepareOrchestrationKit = ( zone.exoClassKit( 'Orchestration', { - requestICAConnectionWatcher: M.interface('RequestICAConnectionWatcher', { + requestICAChannelWatcher: M.interface('RequestICAChannelWatcher', { onFulfilled: M.call(M.remotable('Port')) .optional({ chainId: M.string(), remoteConnAddr: M.string() }) .returns(NetworkShape.Vow$(NetworkShape.Connection)), }), - requestICQConnectionWatcher: M.interface('RequestICQConnectionWatcher', { + requestICQChannelWatcher: M.interface('RequestICQChannelWatcher', { onFulfilled: M.call(M.remotable('Port')) .optional({ remoteConnAddr: M.string(), @@ -114,7 +114,7 @@ const prepareOrchestrationKit = ( return /** @type {OrchestrationState} */ ({ powers, icqConnections }); }, { - requestICAConnectionWatcher: { + requestICAChannelWatcher: { /** * @param {Port} port * @param {{ @@ -135,7 +135,7 @@ const prepareOrchestrationKit = ( ); }, }, - requestICQConnectionWatcher: { + requestICQChannelWatcher: { /** * @param {Port} port * @param {{ @@ -203,7 +203,7 @@ const prepareOrchestrationKit = ( return when( watch( E(portAllocator).allocateICAControllerPort(), - this.facets.requestICAConnectionWatcher, + this.facets.requestICAChannelWatcher, { chainId, remoteConnAddr, @@ -217,6 +217,7 @@ const prepareOrchestrationKit = ( */ provideICQConnection(controllerConnectionId) { if (this.state.icqConnections.has(controllerConnectionId)) { + // TODO #9281 do not return synchronously. see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 return this.state.icqConnections.get(controllerConnectionId) .connection; } @@ -227,7 +228,7 @@ const prepareOrchestrationKit = ( // allocate a new Port for every Connection // TODO #9317 optimize ICQ port allocation E(portAllocator).allocateICQControllerPort(), - this.facets.requestICQConnectionWatcher, + this.facets.requestICQChannelWatcher, { remoteConnAddr, controllerConnectionId, diff --git a/packages/orchestration/test/service.test.ts b/packages/orchestration/test/service.test.ts index a1ce3f543df..b8ac167eeaf 100644 --- a/packages/orchestration/test/service.test.ts +++ b/packages/orchestration/test/service.test.ts @@ -38,6 +38,12 @@ test('makeICQConnection returns an ICQConnection', async t => { 'remote address contains icqhost port, unordered ordering, and icq-1 version string', ); + const icqConnection2 = await E(orchestration).provideICQConnection( + CONTROLLER_CONNECTION_ID, + ); + const localAddr2 = await E(icqConnection2).getLocalAddress(); + t.is(localAddr, localAddr2, 'provideICQConnection is idempotent'); + await t.throwsAsync( E(icqConnection).query([ toRequestQueryJson( From 33085ffa9fd001d5048df205897b6ec25004ce44 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 18 Jun 2024 10:09:45 -0700 Subject: [PATCH 11/12] feat(orchestration): localChainAccountKit returns unwrapped vows --- .../src/examples/stakeBld.contract.js | 4 +- .../src/exos/local-orchestration-account.js | 241 +++++++++++++----- packages/orchestration/src/service.js | 7 +- .../orchestration/src/utils/start-helper.js | 4 +- packages/orchestration/src/utils/time.js | 2 + .../test/examples/stake-bld.contract.test.ts | 5 +- .../local-orchestration-account-kit.test.ts | 23 +- packages/vats/tools/fake-bridge.js | 2 + 8 files changed, 218 insertions(+), 70 deletions(-) diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index 76f2f411540..009094e802e 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -6,7 +6,7 @@ import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/record import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js'; import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; -import { V } from '@agoric/vow/vat.js'; +import { prepareVowTools, V } from '@agoric/vow/vat.js'; import { E } from '@endo/far'; import { deeplyFulfilled } from '@endo/marshal'; import { M } from '@endo/patterns'; @@ -40,12 +40,14 @@ export const start = async (zcf, privateArgs, baggage) => { baggage, privateArgs.marshaller, ); + const vowTools = prepareVowTools(zone.subZone('vows')); const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit( zone, makeRecorderKit, zcf, privateArgs.timerService, + vowTools, makeChainHub(privateArgs.agoricNames), ); diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index 4be1b0f27b5..fd78b28793c 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -7,17 +7,24 @@ import { V } from '@agoric/vow/vat.js'; import { TopicsRecordShape } from '@agoric/zoe/src/contractSupport/index.js'; import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { E } from '@endo/far'; +import { + ChainAddressShape, + ChainAmountShape, + IBCTransferOptionsShape, +} from '../typeGuards.js'; import { maxClockSkew } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; /** * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; - * @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, OrchestrationAccount, OrchestrationAccountI} from '@agoric/orchestration'; + * @import {AmountArg, ChainAddress, DenomAmount, IBCMsgTransferOptions, OrchestrationAccount, ChainInfo, IBCConnectionInfo} from '@agoric/orchestration'; * @import {RecorderKit, MakeRecorderKit} from '@agoric/zoe/src/contractSupport/recorder.js'. * @import {Zone} from '@agoric/zone'; * @import {Remote} from '@agoric/internal'; - * @import {TimerService, TimerBrand} from '@agoric/time'; + * @import {TimerService, TimerBrand, TimestampRecord} from '@agoric/time'; + * @import {PromiseVow, VowTools} from '@agoric/vow'; + * @import {TypedJson} from '@agoric/cosmic-proto'; * @import {ChainHub} from '../utils/chainHub.js'; */ @@ -56,6 +63,7 @@ const PUBLIC_TOPICS = { * @param {MakeRecorderKit} makeRecorderKit * @param {ZCF} zcf * @param {Remote} timerService + * @param {VowTools} vowTools * @param {ChainHub} chainHub */ export const prepareLocalOrchestrationAccountKit = ( @@ -63,6 +71,7 @@ export const prepareLocalOrchestrationAccountKit = ( makeRecorderKit, zcf, timerService, + { watch, when, allVows }, chainHub, ) => { const timestampHelper = makeTimestampHelper(timerService); @@ -72,6 +81,40 @@ export const prepareLocalOrchestrationAccountKit = ( 'Local Orchestration Account Kit', { holder: HolderI, + undelegateWatcher: M.interface('undelegateWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) // XXX consider specifying `completionTime` + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.promise()), + }), + getChainInfoWatcher: M.interface('getChainInfoWatcher', { + onFulfilled: M.call(M.record()) // agoric chain info + .optional({ destination: ChainAddressShape }) // empty context + .returns(M.promise()), // transfer channel + }), + getTimeoutTimestampWatcher: M.interface('getTimeoutTimestampWatcher', { + onFulfilled: M.call(M.bigint()) + .optional(IBCTransferOptionsShape) + .returns(M.bigint()), + }), + transferWatcher: M.interface('transferWatcher', { + onFulfilled: M.call(M.any()) + .optional({ + destination: ChainAddressShape, + opts: M.or(M.undefined(), IBCTransferOptionsShape), + amount: ChainAmountShape, + }) + .returns(M.promise()), + }), + extractFirstResultWatcher: M.interface('extractFirstResultWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) + .optional(M.arrayOf(M.undefined())) + .returns(M.record()), + }), + returnVoidWatcher: M.interface('extractFirstResultWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) + .optional(M.arrayOf(M.undefined())) + .returns(M.undefined()), + }), invitationMakers: M.interface('invitationMakers', { Delegate: M.callWhen(M.string(), AmountShape).returns(InvitationShape), Undelegate: M.callWhen(M.string(), AmountShape).returns( @@ -127,6 +170,110 @@ export const prepareLocalOrchestrationAccountKit = ( throw Error('not yet implemented'); }, }, + undelegateWatcher: { + /** + * @param {[ + * TypedJson<'/cosmos.staking.v1beta1.MsgUndelegateResponse'>, + * ]} response + */ + onFulfilled(response) { + const { completionTime } = response[0]; + return E(timerService).wakeAt( + // TODO clean up date handling once we have real data + dateInSeconds(new Date(completionTime)) + maxClockSkew, + ); + }, + }, + getChainInfoWatcher: { + /** + * @param {ChainInfo} agoricChainInfo + * @param {{ destination: ChainAddress }} ctx + */ + onFulfilled(agoricChainInfo, { destination }) { + return chainHub.getConnectionInfo( + agoricChainInfo.chainId, + destination.chainId, + ); + }, + }, + getTimeoutTimestampWatcher: { + /** + * @param {bigint} timeoutTimestamp + * @param {{ opts: IBCMsgTransferOptions }} ctx + */ + onFulfilled(timeoutTimestamp, { opts }) { + // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided + return ( + opts?.timeoutTimestamp ?? + (opts?.timeoutHeight ? 0n : timeoutTimestamp) + ); + }, + }, + transferWatcher: { + /** + * @param {[ + * { transferChannel: IBCConnectionInfo['transferChannel'] }, + * bigint, + * ]} params + * @param {{ + * destination: ChainAddress; + * opts: IBCMsgTransferOptions; + * amount: DenomAmount; + * }} ctx + */ + onFulfilled( + [{ transferChannel }, timeoutTimestamp], + { opts, amount, destination }, + ) { + return E(this.state.account).executeTx([ + typedJson('/ibc.applications.transfer.v1.MsgTransfer', { + sourcePort: transferChannel.portId, + sourceChannel: transferChannel.channelId, + token: { + amount: String(amount.value), + denom: amount.denom, + }, + sender: this.state.address.address, + receiver: destination.address, + timeoutHeight: opts?.timeoutHeight ?? { + revisionHeight: 0n, + revisionNumber: 0n, + }, + timeoutTimestamp, + memo: opts?.memo ?? '', + }), + ]); + }, + }, + /** + * takes an array of results (from `executeEncodedTx`) and returns the + * first result + */ + extractFirstResultWatcher: { + /** + * @param {Record[]} results + */ + onFulfilled(results) { + results.length === 1 || + Fail`expected exactly one result; got ${results}`; + return results[0]; + }, + }, + /** + * takes an array of results (from `executeEncodedTx`) and returns void + * since we are not interested in the result + */ + returnVoidWatcher: { + /** + * @param {Record[]} results + */ + onFulfilled(results) { + results.length === 1 || + Fail`expected exactly one result; got ${results}`; + trace('Result', results[0]); + return undefined; + }, + }, holder: { /** @type {OrchestrationAccount['getBalance']} */ async getBalance(denomArg) { @@ -167,23 +314,21 @@ export const prepareLocalOrchestrationAccountKit = ( denom: 'ubld', }; const { account: lca } = this.state; - trace('lca', lca); - const delegatorAddress = await V(lca).getAddress(); - trace('delegatorAddress', delegatorAddress); - const [result] = await V(lca).executeTx([ + + const results = E(lca).executeTx([ typedJson('/cosmos.staking.v1beta1.MsgDelegate', { amount, validatorAddress, - delegatorAddress, + delegatorAddress: this.state.address.address, }), ]); - trace('got result', result); - return result; + + return when(watch(results, this.facets.extractFirstResultWatcher)); }, /** * @param {string} validatorAddress * @param {Amount<'nat'>} ertpAmount - * @returns {Promise} + * @returns {PromiseVow} */ async undelegate(validatorAddress, ertpAmount) { // TODO #9211 lookup denom from brand @@ -192,23 +337,15 @@ export const prepareLocalOrchestrationAccountKit = ( denom: 'ubld', }; const { account: lca } = this.state; - trace('lca', lca); - const delegatorAddress = await V(lca).getAddress(); - trace('delegatorAddress', delegatorAddress); - const [response] = await V(lca).executeTx([ + const results = E(lca).executeTx([ typedJson('/cosmos.staking.v1beta1.MsgUndelegate', { amount, validatorAddress, - delegatorAddress, + delegatorAddress: this.state.address.address, }), ]); - trace('undelegate response', response); - const { completionTime } = response; - - await E(timerService).wakeAt( - // TODO clean up date handling once we have real data - dateInSeconds(new Date(completionTime)) + maxClockSkew, - ); + // @ts-expect-error Type 'JsonSafe' is not assignable to type 'MsgUndelegateResponse'. + return when(watch(results, this.facets.undelegateWatcher)); }, /** * Starting a transfer revokes the account holder. The associated @@ -217,16 +354,21 @@ export const prepareLocalOrchestrationAccountKit = ( */ /** @type {OrchestrationAccount['deposit']} */ async deposit(payment) { - await V(this.state.account).deposit(payment); + return when( + watch( + E(this.state.account) + .deposit(payment) + .then(() => {}), + ), + ); }, /** @type {LocalChainAccount['withdraw']} */ async withdraw(amount) { - return V(this.state.account).withdraw(amount); + return when(watch(E(this.state.account).withdraw(amount))); }, /** @type {LocalChainAccount['executeTx']} */ async executeTx(messages) { - // @ts-expect-error subtype - return V(this.state.account).executeTx(messages); + return when(watch(E(this.state.account).executeTx(messages))); }, /** @returns {ChainAddress} */ getAddress() { @@ -250,42 +392,27 @@ export const prepareLocalOrchestrationAccountKit = ( // TODO #9211 lookup denom from brand if ('brand' in amount) throw Fail`ERTP Amounts not yet supported`; - // TODO consider using `getChainsAndConnection` but right now it's keyed by chain name - // and we only have chainId for destination. - const agoricChainInfo = await chainHub.getChainInfo('agoric'); - const { transferChannel } = await chainHub.getConnectionInfo( - agoricChainInfo, - destination, + const connectionInfoV = watch( + chainHub.getChainInfo('agoric'), + this.facets.getChainInfoWatcher, + { destination }, ); - await null; // set a `timeoutTimestamp` if caller does not supply either `timeoutHeight` or `timeoutTimestamp` // TODO #9324 what's a reasonable default? currently 5 minutes - const timeoutTimestamp = - opts?.timeoutTimestamp ?? - (opts?.timeoutHeight - ? 0n - : await timestampHelper.getTimeoutTimestampNS()); + // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided + const timeoutTimestampV = watch( + timestampHelper.getTimeoutTimestampNS(), + this.facets.getTimeoutTimestampWatcher, + { opts }, + ); - const [result] = await V(this.state.account).executeTx([ - typedJson('/ibc.applications.transfer.v1.MsgTransfer', { - sourcePort: transferChannel.portId, - sourceChannel: transferChannel.channelId, - token: { - amount: String(amount.value), - denom: amount.denom, - }, - sender: this.state.address.address, - receiver: destination.address, - timeoutHeight: opts?.timeoutHeight ?? { - revisionHeight: 0n, - revisionNumber: 0n, - }, - timeoutTimestamp, - memo: opts?.memo ?? '', - }), - ]); - trace('MsgTransfer result', result); + const transferV = watch( + allVows([connectionInfoV, timeoutTimestampV]), + this.facets.transferWatcher, + { opts, amount, destination }, + ); + return when(watch(transferV, this.facets.returnVoidWatcher)); }, /** @type {OrchestrationAccount['transferSteps']} */ transferSteps(amount, msg) { diff --git a/packages/orchestration/src/service.js b/packages/orchestration/src/service.js index ffaeb936b60..88d7bc8ccff 100644 --- a/packages/orchestration/src/service.js +++ b/packages/orchestration/src/service.js @@ -213,13 +213,14 @@ const prepareOrchestrationKit = ( }, /** * @param {IBCConnectionID} controllerConnectionId - * @returns {ICQConnection | Promise} + * @returns {Promise} */ provideICQConnection(controllerConnectionId) { if (this.state.icqConnections.has(controllerConnectionId)) { // TODO #9281 do not return synchronously. see https://github.com/Agoric/agoric-sdk/pull/9454#discussion_r1626898694 - return this.state.icqConnections.get(controllerConnectionId) - .connection; + return when( + this.state.icqConnections.get(controllerConnectionId).connection, + ); } const remoteConnAddr = makeICQChannelAddress(controllerConnectionId); const portAllocator = getPower(this.state.powers, 'portAllocator'); diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 7b3f2c6fb74..3c301281a18 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -48,16 +48,18 @@ export const provideOrchestration = ( const chainHub = makeChainHub(remotePowers.agoricNames); + const vowTools = prepareVowTools(zone.subZone('vows')); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit( zone, makeRecorderKit, zcf, remotePowers.timerService, + vowTools, chainHub, ); - const vowTools = prepareVowTools(zone.subZone('vows')); const asyncFlowTools = prepareAsyncFlowTools(zone.subZone('asyncFlow'), { vowTools, }); diff --git a/packages/orchestration/src/utils/time.js b/packages/orchestration/src/utils/time.js index 975069f4efa..a7e63504a12 100644 --- a/packages/orchestration/src/utils/time.js +++ b/packages/orchestration/src/utils/time.js @@ -21,6 +21,8 @@ export function makeTimestampHelper(timer) { return harden({ /** + * XXX do this need to be resumable / use Vows? + * * Takes the current time from ChainTimerService and adds a relative time to * determine a timeout timestamp in nanoseconds. Useful for * {@link MsgTransfer.timeoutTimestamp}. diff --git a/packages/orchestration/test/examples/stake-bld.contract.test.ts b/packages/orchestration/test/examples/stake-bld.contract.test.ts index db6ef562ab4..4c056007d8a 100644 --- a/packages/orchestration/test/examples/stake-bld.contract.test.ts +++ b/packages/orchestration/test/examples/stake-bld.contract.test.ts @@ -1,8 +1,9 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath } from '@agoric/ertp'; -import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { V } from '@agoric/vow/vat.js'; +import { setUpZoeForTest } from '@agoric/zoe/tools/setup-zoe.js'; import { E } from '@endo/far'; import path from 'path'; import { commonSetup } from '../supports.js'; @@ -58,6 +59,8 @@ test('makeAccount, deposit, withdraw', async t => { ); // FIXME #9211 // t.deepEqual(await E(account).getBalance('ubld'), bld.units(100)); + // XXX races in the bridge + await eventLoopIteration(); t.log('withdraw bld from account'); const withdrawResp = await V(account).withdraw(bld.units(100)); diff --git a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts index b3e91a264b3..dfff8afc620 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -1,21 +1,23 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath } from '@agoric/ertp'; -import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { V as E } from '@agoric/vow/vat.js'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { Far } from '@endo/far'; -import { commonSetup } from '../supports.js'; import { prepareLocalOrchestrationAccountKit } from '../../src/exos/local-orchestration-account.js'; import { ChainAddress } from '../../src/orchestration-api.js'; -import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; import { makeChainHub } from '../../src/utils/chainHub.js'; +import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; +import { commonSetup } from '../supports.js'; test('deposit, withdraw', async t => { const { bootstrap, brands, utils } = await commonSetup(t); const { bld: stake } = brands; - const { timer, localchain, marshaller, rootZone, storage } = bootstrap; + const { timer, localchain, marshaller, rootZone, storage, vowTools } = + bootstrap; t.log('chainInfo mocked via `prepareMockChainInfo` until #8879'); @@ -30,6 +32,7 @@ test('deposit, withdraw', async t => { // @ts-expect-error mocked zcf. use `stake-bld.contract.test.ts` to test LCA with offer Far('MockZCF', {}), timer, + vowTools, makeChainHub(bootstrap.agoricNames), ); @@ -51,10 +54,12 @@ test('deposit, withdraw', async t => { const oneHundredStakePmt = await utils.pourPayment(stake.units(100)); t.log('deposit 100 bld to account'); - const depositResp = await E(account).deposit(oneHundredStakePmt); + await E(account).deposit(oneHundredStakePmt); // FIXME #9211 // t.deepEqual(await E(account).getBalance('ubld'), stake.units(100)); + // XXX races in the bridge + await eventLoopIteration(); const withdrawal1 = await E(account).withdraw(stake.units(50)); t.true( AmountMath.isEqual( @@ -84,7 +89,8 @@ test('delegate, undelegate', async t => { const { bld } = brands; - const { timer, localchain, marshaller, rootZone, storage } = bootstrap; + const { timer, localchain, marshaller, rootZone, storage, vowTools } = + bootstrap; t.log('exo setup - prepareLocalChainAccountKit'); const { makeRecorderKit } = prepareRecorderKitMakers( @@ -97,6 +103,7 @@ test('delegate, undelegate', async t => { // @ts-expect-error mocked zcf. use `stake-bld.contract.test.ts` to test LCA with offer Far('MockZCF', {}), timer, + vowTools, makeChainHub(bootstrap.agoricNames), ); @@ -134,7 +141,8 @@ test('transfer', async t => { const { bld: stake } = brands; - const { timer, localchain, marshaller, rootZone, storage } = bootstrap; + const { timer, localchain, marshaller, rootZone, storage, vowTools } = + bootstrap; t.log('exo setup - prepareLocalChainAccountKit'); const { makeRecorderKit } = prepareRecorderKitMakers( @@ -147,6 +155,7 @@ test('transfer', async t => { // @ts-expect-error mocked zcf. use `stake-bld.contract.test.ts` to test LCA with offer Far('MockZCF', {}), timer, + vowTools, makeChainHub(bootstrap.agoricNames), ); diff --git a/packages/vats/tools/fake-bridge.js b/packages/vats/tools/fake-bridge.js index 9976648508e..307cfae3dab 100644 --- a/packages/vats/tools/fake-bridge.js +++ b/packages/vats/tools/fake-bridge.js @@ -71,6 +71,8 @@ export const makeFakeBankBridge = (zone, opts = { balances: {} }) => { case 'VBANK_GIVE': { const { amount, denom } = obj; const address = type === 'VBANK_GRAB' ? obj.sender : obj.recipient; + (address && typeof address === 'string') || + Fail`invalid address ${address}`; balances[address] ||= {}; balances[address][denom] ||= 0n; From c0fd4118c2ecbd8fde3fd22571a5d97b92b6a2ec Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 18 Jun 2024 13:14:42 -0700 Subject: [PATCH 12/12] feat(orchestration): cosmosOrchestrationAccount returns unwrapped vows --- .../src/examples/stakeIca.contract.js | 5 +- .../src/exos/chain-account-kit.js | 4 +- .../src/exos/cosmos-orchestration-account.js | 170 ++++++++++++------ .../src/exos/local-orchestration-account.js | 2 +- packages/orchestration/src/facade.js | 2 +- .../orchestration/src/utils/start-helper.js | 1 + .../orchestration/test/staking-ops.test.ts | 87 ++++++--- packages/orchestration/test/types.test-d.ts | 1 + 8 files changed, 189 insertions(+), 83 deletions(-) diff --git a/packages/orchestration/src/examples/stakeIca.contract.js b/packages/orchestration/src/examples/stakeIca.contract.js index e700f275468..e257764d914 100644 --- a/packages/orchestration/src/examples/stakeIca.contract.js +++ b/packages/orchestration/src/examples/stakeIca.contract.js @@ -2,7 +2,7 @@ import { makeTracer, StorageNodeShape } from '@agoric/internal'; import { TimerServiceShape } from '@agoric/time'; -import { V as E } from '@agoric/vow/vat.js'; +import { V as E, prepareVowTools } from '@agoric/vow/vat.js'; import { prepareRecorderKitMakers, provideAll, @@ -76,9 +76,12 @@ export const start = async (zcf, privateArgs, baggage) => { const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); + const vowTools = prepareVowTools(zone.subZone('vows')); + const makeCosmosOrchestrationAccount = prepareCosmosOrchestrationAccount( zone, makeRecorderKit, + vowTools, zcf, ); diff --git a/packages/orchestration/src/exos/chain-account-kit.js b/packages/orchestration/src/exos/chain-account-kit.js index 6cec4150694..4d14e1410eb 100644 --- a/packages/orchestration/src/exos/chain-account-kit.js +++ b/packages/orchestration/src/exos/chain-account-kit.js @@ -137,7 +137,7 @@ export const prepareChainAccountKit = (zone, { watch, when }) => * decoded using the corresponding `Msg*Response` object. * @throws {Error} if packet fails to send or an error is returned */ - executeEncodedTx(msgs, opts) { + async executeEncodedTx(msgs, opts) { 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 @@ -150,7 +150,7 @@ export const prepareChainAccountKit = (zone, { watch, when }) => ); }, /** Close the remote account */ - close() { + async close() { /// TODO #9192 what should the behavior be here? and `onClose`? // - retrieve assets? // - revoke the port? diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 529f35c77d4..46ddd1e6bcb 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -11,7 +11,6 @@ import { import { MsgBeginRedelegate, MsgDelegate, - MsgDelegateResponse, MsgUndelegate, MsgUndelegateResponse, } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; @@ -26,13 +25,10 @@ import { AmountArgShape, ChainAddressShape, ChainAmountShape, + CoinShape, DelegationShape, } from '../typeGuards.js'; -import { - encodeTxResponse, - maxClockSkew, - tryDecodeResponse, -} from '../utils/cosmos.js'; +import { maxClockSkew, tryDecodeResponse } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; import { dateInSeconds } from '../utils/time.js'; @@ -43,7 +39,10 @@ import { dateInSeconds } from '../utils/time.js'; * @import {Delegation} from '@agoric/cosmic-proto/cosmos/staking/v1beta1/staking.js'; * @import {Remote} from '@agoric/internal'; * @import {TimerService} from '@agoric/time'; + * @import {VowTools} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; + * @import {ResponseQuery} from '@agoric/cosmic-proto/tendermint/abci/types.js'; + * @import {JsonSafe} from '@agoric/cosmic-proto'; */ const trace = makeTracer('ComosOrchestrationAccountHolder'); @@ -94,28 +93,19 @@ const PUBLIC_TOPICS = { account: ['Staking Account holder status', M.any()], }; -export const trivialDelegateResponse = encodeTxResponse( - {}, - MsgDelegateResponse.toProtoMsg, -); - -const expect = (actual, expected, message) => { - if (actual !== expected) { - console.log(message, { actual, expected }); - } -}; - /** @type {(c: { denom: string; amount: string }) => DenomAmount} */ const toDenomAmount = c => ({ denom: c.denom, value: BigInt(c.amount) }); /** * @param {Zone} zone * @param {MakeRecorderKit} makeRecorderKit + * @param {VowTools} vowTools * @param {ZCF} zcf */ export const prepareCosmosOrchestrationAccountKit = ( zone, makeRecorderKit, + { when, watch }, zcf, ) => { const makeCosmosOrchestrationAccountKit = zone.exoClassKit( @@ -126,6 +116,26 @@ export const prepareCosmosOrchestrationAccountKit = ( getUpdater: M.call().returns(M.remotable()), amountToCoin: M.call(AmountArgShape).returns(M.record()), }), + returnVoidWatcher: M.interface('returnVoidWatcher', { + onFulfilled: M.call(M.or(M.string(), M.record())) + .optional(M.arrayOf(M.undefined())) + .returns(M.undefined()), + }), + balanceQueryWatcher: M.interface('balanceQueryWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.or(M.record(), M.undefined())), + }), + undelegateWatcher: M.interface('undelegateWatcher', { + onFulfilled: M.call(M.string()) + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.promise()), + }), + withdrawRewardWatcher: M.interface('withdrawRewardWatcher', { + onFulfilled: M.call(M.string()) + .optional(M.arrayOf(M.undefined())) // empty context + .returns(M.arrayOf(CoinShape)), + }), holder: IcaAccountHolderI, invitationMakers: M.interface('invitationMakers', { Delegate: M.callWhen(ChainAddressShape, AmountArgShape).returns( @@ -195,6 +205,59 @@ export const prepareCosmosOrchestrationAccountKit = ( }); }, }, + balanceQueryWatcher: { + /** + * @param {JsonSafe[]} results + */ + onFulfilled([result]) { + if (!result?.key) throw Fail`Error parsing result ${result}`; + const { balance } = QueryBalanceResponse.decode( + decodeBase64(result.key), + ); + if (!balance) throw Fail`Result lacked balance key: ${result}`; + return harden(toDenomAmount(balance)); + }, + }, + undelegateWatcher: { + /** + * @param {string} result + */ + onFulfilled(result) { + const response = tryDecodeResponse( + result, + MsgUndelegateResponse.fromProtoMsg, + ); + trace('undelegate response', response); + const { completionTime } = response; + completionTime || Fail`No completion time result ${result}`; + return E(this.state.timer).wakeAt( + dateInSeconds(completionTime) + maxClockSkew, + ); + }, + }, + /** + * takes an array of results (from `executeEncodedTx`) and returns void + * since we are not interested in the result + */ + returnVoidWatcher: { + /** @param {string | Record} result */ + onFulfilled(result) { + trace('Result', result); + return undefined; + }, + }, + withdrawRewardWatcher: { + /** @param {string} result */ + onFulfilled(result) { + const response = tryDecodeResponse( + result, + MsgWithdrawDelegatorRewardResponse.fromProtoMsg, + ); + trace('withdrawReward response', response); + const { amount: coins } = response; + return harden(coins.map(toDenomAmount)); + }, + }, invitationMakers: { /** * @param {CosmosValidatorAddress} validator @@ -292,7 +355,7 @@ export const prepareCosmosOrchestrationAccountKit = ( const { helper } = this.facets; const { chainAddress } = this.state; - const result = await E(helper.owned()).executeEncodedTx([ + const results = E(helper.owned()).executeEncodedTx([ Any.toJSON( MsgDelegate.toProtoMsg({ delegatorAddress: chainAddress.address, @@ -301,8 +364,7 @@ export const prepareCosmosOrchestrationAccountKit = ( }), ), ]); - - expect(result, trivialDelegateResponse, 'MsgDelegateResponse'); + return when(watch(results, this.facets.returnVoidWatcher)); }, async deposit(payment) { trace('deposit', payment); @@ -325,8 +387,7 @@ export const prepareCosmosOrchestrationAccountKit = ( const { helper } = this.facets; const { chainAddress } = this.state; - // NOTE: response, including completionTime, is currently discarded. - await E(helper.owned()).executeEncodedTx([ + const results = E(helper.owned()).executeEncodedTx([ Any.toJSON( MsgBeginRedelegate.toProtoMsg({ delegatorAddress: chainAddress.address, @@ -336,8 +397,15 @@ export const prepareCosmosOrchestrationAccountKit = ( }), ), ]); - }, + return when( + watch( + results, + // NOTE: response, including completionTime, is currently discarded. + this.facets.returnVoidWatcher, + ), + ); + }, /** * @param {CosmosValidatorAddress} validator * @returns {Promise} @@ -351,14 +419,9 @@ export const prepareCosmosOrchestrationAccountKit = ( validatorAddress: validator.address, }); const account = helper.owned(); - const result = await E(account).executeEncodedTx([Any.toJSON(msg)]); - const response = tryDecodeResponse( - result, - MsgWithdrawDelegatorRewardResponse.fromProtoMsg, - ); - trace('withdrawReward response', response); - const { amount: coins } = response; - return harden(coins.map(toDenomAmount)); + + const results = E(account).executeEncodedTx([Any.toJSON(msg)]); + return when(watch(results, this.facets.withdrawRewardWatcher)); }, /** * @param {DenomArg} denom @@ -372,7 +435,7 @@ export const prepareCosmosOrchestrationAccountKit = ( // TODO #9211 lookup denom from brand assert.typeof(denom, 'string'); - const [result] = await E(icqConnection).query([ + const results = E(icqConnection).query([ toRequestQueryJson( QueryBalanceRequest.toProtoMsg({ address: chainAddress.address, @@ -380,12 +443,7 @@ export const prepareCosmosOrchestrationAccountKit = ( }), ), ]); - if (!result?.key) throw Fail`Error parsing result ${result}`; - const { balance } = QueryBalanceResponse.decode( - decodeBase64(result.key), - ); - if (!balance) throw Fail`Result lacked balance key: ${result}`; - return harden(toDenomAmount(balance)); + return when(watch(results, this.facets.balanceQueryWatcher)); }, send(toAccount, amount) { @@ -411,28 +469,23 @@ export const prepareCosmosOrchestrationAccountKit = ( async undelegate(delegations) { trace('undelegate', delegations); const { helper } = this.facets; - const { chainAddress, bondDenom, timer } = this.state; - - const result = await E(helper.owned()).executeEncodedTx( - delegations.map(d => - Any.toJSON( - MsgUndelegate.toProtoMsg({ - delegatorAddress: chainAddress.address, - validatorAddress: d.validatorAddress, - amount: { denom: bondDenom, amount: d.shares }, - }), + const { chainAddress, bondDenom } = this.state; + + const undelegateV = watch( + E(helper.owned()).executeEncodedTx( + delegations.map(d => + Any.toJSON( + MsgUndelegate.toProtoMsg({ + delegatorAddress: chainAddress.address, + validatorAddress: d.validatorAddress, + amount: { denom: bondDenom, amount: d.shares }, + }), + ), ), ), + this.facets.undelegateWatcher, ); - - const response = tryDecodeResponse( - result, - MsgUndelegateResponse.fromProtoMsg, - ); - trace('undelegate response', response); - const { completionTime } = response; - - await E(timer).wakeAt(dateInSeconds(completionTime) + maxClockSkew); + return when(watch(undelegateV, this.facets.returnVoidWatcher)); }, }, }, @@ -450,6 +503,7 @@ export const prepareCosmosOrchestrationAccountKit = ( /** * @param {Zone} zone * @param {MakeRecorderKit} makeRecorderKit + * @param {VowTools} vowTools * @param {ZCF} zcf * @returns {( * ...args: Parameters< @@ -460,11 +514,13 @@ export const prepareCosmosOrchestrationAccountKit = ( export const prepareCosmosOrchestrationAccount = ( zone, makeRecorderKit, + vowTools, zcf, ) => { const makeKit = prepareCosmosOrchestrationAccountKit( zone, makeRecorderKit, + vowTools, zcf, ); return (...args) => makeKit(...args).holder; diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index fd78b28793c..15e28d830a1 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -110,7 +110,7 @@ export const prepareLocalOrchestrationAccountKit = ( .optional(M.arrayOf(M.undefined())) .returns(M.record()), }), - returnVoidWatcher: M.interface('extractFirstResultWatcher', { + returnVoidWatcher: M.interface('returnVoidWatcher', { onFulfilled: M.call(M.arrayOf(M.record())) .optional(M.arrayOf(M.undefined())) .returns(M.undefined()), diff --git a/packages/orchestration/src/facade.js b/packages/orchestration/src/facade.js index 2043346b14f..6de79caa23f 100644 --- a/packages/orchestration/src/facade.js +++ b/packages/orchestration/src/facade.js @@ -7,7 +7,7 @@ import { prepareOrchestrator } from './exos/orchestrator.js'; /** * @import {AsyncFlowTools} from '@agoric/async-flow'; * @import {Zone} from '@agoric/zone'; - * @import {Vow} from '@agoric/vow'; + * @import {Vow, VowTools} from '@agoric/vow'; * @import {TimerService} from '@agoric/time'; * @import {IBCConnectionID} from '@agoric/vats'; * @import {LocalChain} from '@agoric/vats/src/localchain.js'; diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 3c301281a18..8ec73eb8bea 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -68,6 +68,7 @@ export const provideOrchestration = ( // FIXME what zone? zone, makeRecorderKit, + vowTools, zcf, ); diff --git a/packages/orchestration/test/staking-ops.test.ts b/packages/orchestration/test/staking-ops.test.ts index 8bbe71c6388..ed8f99868c4 100644 --- a/packages/orchestration/test/staking-ops.test.ts +++ b/packages/orchestration/test/staking-ops.test.ts @@ -1,5 +1,7 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import type { AnyJson } from '@agoric/cosmic-proto'; +import type { Coin } from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js'; import { MsgWithdrawDelegatorRewardResponse } from '@agoric/cosmic-proto/cosmos/distribution/v1beta1/tx.js'; import { MsgBeginRedelegateResponse, @@ -7,28 +9,29 @@ import { MsgDelegateResponse, MsgUndelegateResponse, } from '@agoric/cosmic-proto/cosmos/staking/v1beta1/tx.js'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { makeNotifierFromSubscriber } from '@agoric/notifier'; +import type { TimestampRecord, TimestampValue } from '@agoric/time'; import { makeScalarBigMapStore, type Baggage } from '@agoric/vat-data'; import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js'; -import { decodeBase64 } from '@endo/base64'; -import { E, Far } from '@endo/far'; +import { prepareVowTools } from '@agoric/vow/vat.js'; +import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; -import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; -import type { Coin } from '@agoric/cosmic-proto/cosmos/base/v1beta1/coin.js'; -import type { TimestampRecord, TimestampValue } from '@agoric/time'; -import type { AnyJson } from '@agoric/cosmic-proto'; import { makeDurableZone } from '@agoric/zone/durable.js'; -import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; -import { makeNotifierFromSubscriber } from '@agoric/notifier'; -import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; -import { - prepareCosmosOrchestrationAccountKit, - trivialDelegateResponse, -} from '../src/exos/cosmos-orchestration-account.js'; +import { decodeBase64 } from '@endo/base64'; +import { E, Far } from '@endo/far'; +import { prepareCosmosOrchestrationAccountKit } from '../src/exos/cosmos-orchestration-account.js'; +import type { ChainAddress, IcaAccount, ICQConnection } from '../src/types.js'; import { encodeTxResponse } from '../src/utils/cosmos.js'; -import type { IcaAccount, ChainAddress, ICQConnection } from '../src/types.js'; const { Fail } = assert; +const trivialDelegateResponse = encodeTxResponse( + {}, + MsgDelegateResponse.toProtoMsg, +); + test('MsgDelegateResponse trivial response', t => { t.is( trivialDelegateResponse, @@ -189,6 +192,8 @@ const makeScenario = () => { sequence: false, }); + const vowTools = prepareVowTools(zone.subZone('VowTools')); + const icqConnection = Far('ICQConnection', {}) as ICQConnection; const timer = buildZoeManualTimer(undefined, time.parse(startTime), { @@ -203,6 +208,7 @@ const makeScenario = () => { storageNode: rootNode, timer, icqConnection, + vowTools, ...mockZCF(), }; }; @@ -210,8 +216,14 @@ const makeScenario = () => { test('makeAccount() writes to storage', async t => { const s = makeScenario(); const { account, timer } = s; - const { makeRecorderKit, storageNode, zcf, icqConnection, zone } = s; - const make = prepareCosmosOrchestrationAccountKit(zone, makeRecorderKit, zcf); + const { makeRecorderKit, storageNode, zcf, icqConnection, vowTools, zone } = + s; + const make = prepareCosmosOrchestrationAccountKit( + zone, + makeRecorderKit, + vowTools, + zcf, + ); const { holder } = make(account.getAddress(), 'uatom', { account, @@ -233,8 +245,14 @@ test('makeAccount() writes to storage', async t => { test('withdrawRewards() on StakingAccountHolder formats message correctly', async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, icqConnection, zone } = s; - const make = prepareCosmosOrchestrationAccountKit(zone, makeRecorderKit, zcf); + const { makeRecorderKit, storageNode, zcf, icqConnection, vowTools, zone } = + s; + const make = prepareCosmosOrchestrationAccountKit( + zone, + makeRecorderKit, + vowTools, + zcf, + ); // Higher fidelity tests below use invitationMakers. const { holder } = make(account.getAddress(), 'uatom', { @@ -256,11 +274,20 @@ test('withdrawRewards() on StakingAccountHolder formats message correctly', asyn test(`delegate; redelegate using invitationMakers`, async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, zoe, icqConnection, zone } = s; + const { + makeRecorderKit, + storageNode, + zcf, + zoe, + icqConnection, + vowTools, + zone, + } = s; const aBrand = Far('Token') as Brand<'nat'>; const makeAccountKit = prepareCosmosOrchestrationAccountKit( zone, makeRecorderKit, + vowTools, zcf, ); @@ -329,10 +356,19 @@ test(`delegate; redelegate using invitationMakers`, async t => { test(`withdraw rewards using invitationMakers`, async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, zoe, icqConnection, zone } = s; + const { + makeRecorderKit, + storageNode, + zcf, + zoe, + icqConnection, + vowTools, + zone, + } = s; const makeAccountKit = prepareCosmosOrchestrationAccountKit( zone, makeRecorderKit, + vowTools, zcf, ); @@ -359,10 +395,19 @@ test(`withdraw rewards using invitationMakers`, async t => { test(`undelegate waits for unbonding period`, async t => { const s = makeScenario(); const { account, calls, timer } = s; - const { makeRecorderKit, storageNode, zcf, zoe, icqConnection, zone } = s; + const { + makeRecorderKit, + storageNode, + zcf, + zoe, + icqConnection, + vowTools, + zone, + } = s; const makeAccountKit = prepareCosmosOrchestrationAccountKit( zone, makeRecorderKit, + vowTools, zcf, ); diff --git a/packages/orchestration/test/types.test-d.ts b/packages/orchestration/test/types.test-d.ts index cfc5989588b..a880f300038 100644 --- a/packages/orchestration/test/types.test-d.ts +++ b/packages/orchestration/test/types.test-d.ts @@ -60,6 +60,7 @@ expectNotType(chainAddr); anyVal, anyVal, anyVal, + anyVal, ); makeCosmosOrchestrationAccount( anyVal,