diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index 8b0a0b68dae..ef0729b3d7f 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -47,7 +47,7 @@ export const start = async (zcf, privateArgs, baggage) => { zcf, privateArgs.timerService, vowTools, - makeChainHub(privateArgs.agoricNames), + makeChainHub(privateArgs.agoricNames, vowTools), ); // ---------------- diff --git a/packages/orchestration/src/exos/chain-hub.js b/packages/orchestration/src/exos/chain-hub.js index 88ae11e8a15..7c568c4732a 100644 --- a/packages/orchestration/src/exos/chain-hub.js +++ b/packages/orchestration/src/exos/chain-hub.js @@ -3,18 +3,12 @@ import { E } from '@endo/far'; import { M } from '@endo/patterns'; import { VowShape } from '@agoric/vow'; -// eslint-disable-next-line no-restricted-syntax -import { heapVowTools } from '@agoric/vow/vat.js'; import { makeHeapZone } from '@agoric/zone'; import { CosmosChainInfoShape, IBCConnectionInfoShape } from '../typeGuards.js'; -// FIXME test thoroughly whether heap suffices for ChainHub -// eslint-disable-next-line no-restricted-syntax -const { allVows, watch } = heapVowTools; - /** * @import {NameHub} from '@agoric/vats'; - * @import {Vow} from '@agoric/vow'; + * @import {Vow, VowTools} from '@agoric/vow'; * @import {CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api.js'; * @import {ChainInfo, KnownChains} from '../chain-info.js'; * @import {Remote} from '@agoric/internal'; @@ -94,9 +88,10 @@ const ChainHubI = M.interface('ChainHub', { * hub and repeat the registrations. * * @param {Remote} agoricNames - * @param {Zone} [zone] + * @param {VowTools} vowTools */ -export const makeChainHub = (agoricNames, zone = makeHeapZone()) => { +export const makeChainHub = (agoricNames, vowTools) => { + const zone = makeHeapZone(); /** @type {MapStore} */ const chainInfos = zone.mapStore('chainInfos', { keyShape: M.string(), @@ -108,6 +103,89 @@ export const makeChainHub = (agoricNames, zone = makeHeapZone()) => { valueShape: IBCConnectionInfoShape, }); + const lookupChainInfo = vowTools.retriable( + zone, + 'lookupChainInfo', + /** @param {string} chainName */ + // eslint-disable-next-line no-restricted-syntax -- TODO more exact rules for vow best practices + async chainName => { + await null; + try { + const chainInfo = await E(agoricNames).lookup(CHAIN_KEY, chainName); + // It may have been set by another concurrent call + // TODO consider makeAtomicProvider for vows + if (!chainInfos.has(chainName)) { + chainInfos.init(chainName, chainInfo); + } + return chainInfo; + } catch (e) { + console.error('lookupChainInfo', chainName, 'error', e); + throw makeError(`chain not found:${chainName}`); + } + }, + ); + + const lookupConnectionInfo = vowTools.retriable( + zone, + 'lookupConnectionInfo', + /** + * @param {string} chainId1 + * @param {string} chainId2 + */ + // eslint-disable-next-line no-restricted-syntax -- TODO more exact rules for vow best practices + async (chainId1, chainId2) => { + await null; + const key = connectionKey(chainId1, chainId2); + try { + const connectionInfo = await E(agoricNames).lookup( + CONNECTIONS_KEY, + key, + ); + // It may have been set by another concurrent call + // TODO consider makeAtomicProvider for vows + if (!connectionInfos.has(key)) { + connectionInfos.init(key, connectionInfo); + } + return connectionInfo; + } catch (e) { + console.error('lookupConnectionInfo', chainId1, chainId2, 'error', e); + throw makeError(`connection not found: ${chainId1}<->${chainId2}`); + } + }, + ); + + /* eslint-disable no-use-before-define -- chainHub defined below */ + const lookupChainsAndConnection = vowTools.retriable( + zone, + 'lookupChainsAndConnection', + /** + * @template {string} C1 + * @template {string} C2 + * @param {C1} chainName1 + * @param {C2} chainName2 + * @returns {Promise< + * [ActualChainInfo, ActualChainInfo, IBCConnectionInfo] + * >} + */ + // eslint-disable-next-line no-restricted-syntax -- TODO more exact rules for vow best practices + async (chainName1, chainName2) => { + const [chain1, chain2] = await vowTools.asPromise( + vowTools.allVows([ + chainHub.getChainInfo(chainName1), + chainHub.getChainInfo(chainName2), + ]), + ); + const connectionInfo = await vowTools.asPromise( + chainHub.getConnectionInfo(chain2, chain1), + ); + return /** @type {[ActualChainInfo, ActualChainInfo, IBCConnectionInfo]} */ ([ + chain1, + chain2, + connectionInfo, + ]); + }, + ); + const chainHub = zone.exo('ChainHub', ChainHubI, { /** * Register a new chain. The name will override a name in well known chain @@ -133,19 +211,11 @@ export const makeChainHub = (agoricNames, zone = makeHeapZone()) => { // Either from registerChain or memoized remote lookup() if (chainInfos.has(chainName)) { return /** @type {Vow>} */ ( - watch(chainInfos.get(chainName)) + vowTools.asVow(() => chainInfos.get(chainName)) ); } - return watch(E(agoricNames).lookup(CHAIN_KEY, chainName), { - onFulfilled: chainInfo => { - chainInfos.init(chainName, chainInfo); - return chainInfo; - }, - onRejected: _cause => { - throw makeError(`chain not found:${chainName}`); - }, - }); + return lookupChainInfo(chainName); }, /** * @param {string} chainId1 @@ -167,18 +237,10 @@ export const makeChainHub = (agoricNames, zone = makeHeapZone()) => { const chainId2 = typeof chain2 === 'string' ? chain2 : chain2.chainId; const key = connectionKey(chainId1, chainId2); if (connectionInfos.has(key)) { - return watch(connectionInfos.get(key)); + return vowTools.asVow(() => connectionInfos.get(key)); } - return watch(E(agoricNames).lookup(CONNECTIONS_KEY, key), { - onFulfilled: connectionInfo => { - connectionInfos.init(key, connectionInfo); - return connectionInfo; - }, - onRejected: _cause => { - throw makeError(`connection not found: ${chainId1}<->${chainId2}`); - }, - }); + return lookupConnectionInfo(chainId1, chainId2); }, /** @@ -191,21 +253,8 @@ export const makeChainHub = (agoricNames, zone = makeHeapZone()) => { * >} */ getChainsAndConnection(chainName1, chainName2) { - return watch( - allVows([ - chainHub.getChainInfo(chainName1), - chainHub.getChainInfo(chainName2), - ]), - { - onFulfilled: ([chain1, chain2]) => { - return watch(chainHub.getConnectionInfo(chain2, chain1), { - onFulfilled: connectionInfo => { - return [chain1, chain2, connectionInfo]; - }, - }); - }, - }, - ); + // @ts-expect-error XXX generic parameter propagation + return lookupChainsAndConnection(chainName1, chainName2); }, }); diff --git a/packages/orchestration/src/proposals/start-stakeAtom.js b/packages/orchestration/src/proposals/start-stakeAtom.js index 67d73b5fe14..ee63de7c612 100644 --- a/packages/orchestration/src/proposals/start-stakeAtom.js +++ b/packages/orchestration/src/proposals/start-stakeAtom.js @@ -1,6 +1,8 @@ import { makeTracer } from '@agoric/internal'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; -import { heapVowE as E } from '@agoric/vow/vat.js'; +import { prepareVowTools } from '@agoric/vow'; +import { makeHeapZone } from '@agoric/zone'; +import { E } from '@endo/far'; import { makeChainHub } from '../exos/chain-hub.js'; /** @@ -44,9 +46,10 @@ export const startStakeAtom = async ({ const storageNode = await makeStorageNodeChild(chainStorage, VSTORAGE_PATH); const marshaller = await E(board).getPublishingMarshaller(); - const chainHub = makeChainHub(await agoricNames); + const vt = prepareVowTools(makeHeapZone()); + const chainHub = makeChainHub(await agoricNames, vt); - const [_, cosmoshub, connectionInfo] = await E.when( + const [_, cosmoshub, connectionInfo] = await vt.when( chainHub.getChainsAndConnection('agoric', 'cosmoshub'), ); diff --git a/packages/orchestration/src/proposals/start-stakeOsmo.js b/packages/orchestration/src/proposals/start-stakeOsmo.js index af168f27645..fc3b3118096 100644 --- a/packages/orchestration/src/proposals/start-stakeOsmo.js +++ b/packages/orchestration/src/proposals/start-stakeOsmo.js @@ -1,6 +1,8 @@ import { makeTracer } from '@agoric/internal'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; -import { heapVowE as E } from '@agoric/vow/vat.js'; +import { prepareVowTools } from '@agoric/vow'; +import { makeHeapZone } from '@agoric/zone'; +import { E } from '@endo/far'; import { makeChainHub } from '../exos/chain-hub.js'; /** @@ -45,9 +47,10 @@ export const startStakeOsmo = async ({ const storageNode = await makeStorageNodeChild(chainStorage, VSTORAGE_PATH); const marshaller = await E(board).getPublishingMarshaller(); - const chainHub = makeChainHub(await agoricNames); + const vt = prepareVowTools(makeHeapZone()); + const chainHub = makeChainHub(await agoricNames, vt); - const [_, osmosis, connectionInfo] = await E.when( + const [_, osmosis, connectionInfo] = await vt.when( chainHub.getChainsAndConnection('agoric', 'osmosis'), ); diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 26dba53e4b8..e96fedabb8f 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -51,10 +51,10 @@ export const provideOrchestration = ( const zone = makeDurableZone(baggage); const { agoricNames, timerService } = remotePowers; - const chainHub = makeChainHub(agoricNames); - const vowTools = prepareVowTools(zone.subZone('vows')); + const chainHub = makeChainHub(agoricNames, vowTools); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit( zone, diff --git a/packages/orchestration/test/exos/chain-hub.test.ts b/packages/orchestration/test/exos/chain-hub.test.ts index 59924d54327..a893a0cff7a 100644 --- a/packages/orchestration/test/exos/chain-hub.test.ts +++ b/packages/orchestration/test/exos/chain-hub.test.ts @@ -4,8 +4,10 @@ import test from '@endo/ses-ava/prepare-endo.js'; import { makeNameHubKit } from '@agoric/vats'; import { prepareSwingsetVowTools } from '@agoric/vow/vat.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { makeChainHub } from '../../src/exos/chain-hub.js'; import { provideDurableZone } from '../supports.js'; +import { registerChainNamespace } from '../../src/chain-info.js'; const connection = { id: 'connection-1', @@ -29,15 +31,43 @@ const connection = { }, } as const; -test('getConnectionInfo', async t => { +// fresh state for each test +const setup = () => { const zone = provideDurableZone('root'); const vt = prepareSwingsetVowTools(zone); - const { nameHub } = makeNameHubKit(); - const chainHub = makeChainHub(nameHub, zone); + const { nameHub, nameAdmin } = makeNameHubKit(); + const chainHub = makeChainHub(nameHub, vt); + + return { chainHub, nameAdmin, vt }; +}; + +test.serial('getChainInfo', async t => { + const { chainHub, nameAdmin, vt } = setup(); + // use fetched chain info + await registerChainNamespace(nameAdmin); + + const vow = chainHub.getChainInfo('celestia'); + t.like(await vt.asPromise(vow), { chainId: 'celestia' }); +}); + +test.serial('concurrency', async t => { + const { chainHub, nameAdmin, vt } = setup(); + // use fetched chain info + await registerChainNamespace(nameAdmin); + + const v1 = chainHub.getChainInfo('celestia'); + const v2 = chainHub.getChainInfo('celestia'); + t.like(await vt.asPromise(vt.allVows([v1, v2])), [ + { chainId: 'celestia' }, + { chainId: 'celestia' }, + ]); +}); + +test.serial('getConnectionInfo', async t => { + const { chainHub, vt } = setup(); const aChain = { chainId: 'a-1' }; const bChain = { chainId: 'b-2' }; - chainHub.registerConnection(aChain.chainId, bChain.chainId, connection); // Look up by string or info object 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 6f4af899f3c..f604aaeac2b 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -36,7 +36,7 @@ test('deposit, withdraw', async t => { Far('MockZCF', {}), timer, vowTools, - makeChainHub(bootstrap.agoricNames), + makeChainHub(bootstrap.agoricNames, vowTools), ); t.log('request account from vat-localchain'); @@ -107,7 +107,7 @@ test('delegate, undelegate', async t => { Far('MockZCF', {}), timer, vowTools, - makeChainHub(bootstrap.agoricNames), + makeChainHub(bootstrap.agoricNames, vowTools), ); t.log('request account from vat-localchain'); @@ -170,7 +170,7 @@ test('transfer', async t => { Far('MockZCF', {}), timer, vowTools, - makeChainHub(bootstrap.agoricNames), + makeChainHub(bootstrap.agoricNames, vowTools), ); t.log('request account from vat-localchain'); diff --git a/packages/orchestration/test/facade.test.ts b/packages/orchestration/test/facade.test.ts index a893bc28ffe..0820dba3977 100644 --- a/packages/orchestration/test/facade.test.ts +++ b/packages/orchestration/test/facade.test.ts @@ -1,6 +1,6 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { heapVowE as E } from '@agoric/vow/vat.js'; +import { prepareSwingsetVowTools } from '@agoric/vow/vat.js'; import { setupZCFTest } from '@agoric/zoe/test/unitTests/zcf/setupZcfTest.js'; import type { CosmosChainInfo, IBCConnectionInfo } from '../src/cosmos-api.js'; import type { Chain } from '../src/orchestration-api.js'; @@ -39,8 +39,6 @@ export const mockChainConnection: IBCConnectionInfo = { }, }; -const makeLocalOrchestrationAccountKit = () => assert.fail(`not used`); - test('chain info', async t => { const { bootstrap, facadeServices, commonPrivateArgs } = await commonSetup(t); @@ -49,6 +47,7 @@ test('chain info', async t => { // After setupZCFTest because this disables relaxDurabilityRules // which breaks Zoe test setup's fakeVatAdmin const zone = provideDurableZone('test'); + const vt = prepareSwingsetVowTools(zone); const orchKit = provideOrchestration( zcf, @@ -77,7 +76,7 @@ test('chain info', async t => { }); const result = (await handle()) as Chain; - t.deepEqual(await E.when(result.getChainInfo()), mockChainInfo); + t.deepEqual(await vt.when(result.getChainInfo()), mockChainInfo); }); test.todo('contract upgrade'); diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index 81765c8d925..19b5a2390f8 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -7,7 +7,7 @@ import { makeWhen } from './when.js'; /** * @import {Zone} from '@agoric/base-zone'; - * @import {IsRetryableReason, AsPromiseFunction, EVow} from './types.js'; + * @import {IsRetryableReason, AsPromiseFunction, EVow, Vow, ERef} from './types.js'; */ /** @@ -40,9 +40,11 @@ export const prepareVowTools = (zone, powers = {}) => { * * The internal functions * + * @template T * @param {Zone} fnZone - the zone for the named function * @param {string} name - * @param {(...args: unknown[]) => unknown} fn + * @param {(...args: unknown[]) => ERef} fn + * @returns {(...args: unknown[]) => Vow} */ const retriable = (fnZone, name, fn) =>