diff --git a/packages/orchestration/src/examples/sendAnywhere.contract.js b/packages/orchestration/src/examples/sendAnywhere.contract.js index 5fefa84713cf..081af6dad120 100644 --- a/packages/orchestration/src/examples/sendAnywhere.contract.js +++ b/packages/orchestration/src/examples/sendAnywhere.contract.js @@ -5,9 +5,9 @@ import { heapVowE as E } from '@agoric/vow/vat.js'; import { AmountShape } from '@agoric/ertp'; import { CosmosChainInfoShape } from '../typeGuards.js'; import { provideOrchestration } from '../utils/start-helper.js'; +import { makeResumableAgoricNamesHack } from '../exos/agoric-names-tools.js'; const { entries } = Object; -const { Fail } = assert; /** * @import {Baggage} from '@agoric/vat-data'; @@ -16,7 +16,6 @@ const { Fail } = assert; * @import {LocalChain} from '@agoric/vats/src/localchain.js'; * @import {OrchestrationService} from '../service.js'; * @import {NameHub} from '@agoric/vats'; - * @import {VBankAssetDetail} from '@agoric/vats/tools/board-utils.js'; * @import {Remote} from '@agoric/vow'; */ @@ -45,36 +44,26 @@ export const SingleAmountRecord = M.and( * @param {Baggage} baggage */ export const start = async (zcf, privateArgs, baggage) => { - const { chainHub, orchestrate, zone } = provideOrchestration( + const { chainHub, orchestrate, vowTools, zone } = provideOrchestration( zcf, baggage, privateArgs, privateArgs.marshaller, ); + const agoricNamesTools = makeResumableAgoricNamesHack(zone, { + agoricNames: privateArgs.agoricNames, + vowTools, + }); /** @type {import('../orchestration-api.js').OrchestrationAccount} */ let contractAccount; - const findBrandInVBank = async brand => { - const assets = await E( - // XXX heapVowE - /** @type {Promise>>} */ ( - E(privateArgs.agoricNames).lookup('vbankAsset') - ), - ).values(); - const it = assets.find(a => a.brand === brand); - if (!it) { - throw Fail`brand ${brand} not in agoricNames.vbankAsset`; - } - return it; - }; - /** @type {OfferHandler} */ const sendIt = orchestrate( 'sendIt', - { zcf }, + { zcf, agoricNamesTools }, // eslint-disable-next-line no-shadow -- this `zcf` is enclosed in a membrane - async (orch, { zcf }, seat, offerArgs) => { + async (orch, { zcf, agoricNamesTools }, seat, offerArgs) => { mustMatch( offerArgs, harden({ chainName: M.scalar(), destAddr: M.string() }), @@ -82,7 +71,10 @@ export const start = async (zcf, privateArgs, baggage) => { const { chainName, destAddr } = offerArgs; const { give } = seat.getProposal(); const [[kw, amt]] = entries(give); - const { denom } = await findBrandInVBank(amt.brand); + // TODO remove V.when() when integrating with asyncFlow + const { denom } = await V.when( + agoricNamesTools.findBrandInVBank(amt.brand), + ); const chain = await orch.getChain(chainName); // FIXME ok to use a heap var crossing the membrane scope this way? diff --git a/packages/orchestration/src/exos/agoric-names-tools.js b/packages/orchestration/src/exos/agoric-names-tools.js new file mode 100644 index 000000000000..4e4b8aed9d99 --- /dev/null +++ b/packages/orchestration/src/exos/agoric-names-tools.js @@ -0,0 +1,96 @@ +import { VowShape } from '@agoric/vow'; +import { E } from '@endo/far'; +import { M, makeCopyMap } from '@endo/patterns'; +import { BrandShape } from '@agoric/ertp'; + +const { Fail } = assert; + +/** + * @import {NameHub} from '@agoric/vats'; + * @import {AssetInfo} from '@agoric/vats/src/vat-bank.js'; + * @import {Remote} from '@agoric/internal'; + * @import {Vow, VowTools} from '@agoric/vow'; + * @import {Zone} from '@agoric/zone'; + */ + +/** + * Perform remote calls to agoricNames in membrane-friendly way. This is an + * interim approach until https://github.com/Agoric/agoric-sdk/issues/9541, + * https://github.com/Agoric/agoric-sdk/pull/9322, or + * https://github.com/Agoric/agoric-sdk/pull/9519 + * + * XXX consider exposing `has`, `entries`, `keys`, `values` from `NameHub` + * + * @param {Zone} zone + * @param {{ agoricNames: Remote; vowTools: VowTools }} powers + */ +export const makeResumableAgoricNamesHack = ( + zone, + { agoricNames, vowTools: { watch } }, +) => { + const makeResumableAgoricNamesHackKit = zone.exoClassKit( + 'ResumableAgoricNamesHack', + { + public: M.interface('ResumableAgoricNamesHackI', { + lookup: M.call().rest(M.arrayOf(M.string())).returns(VowShape), + findBrandInVBank: M.call(BrandShape).returns(VowShape), + }), + vbankAssetEntriesWatcher: M.interface('vbankAssetEntriesWatcher', { + onFulfilled: M.call(M.arrayOf(M.record())) + .optional({ brand: BrandShape }) + .returns(VowShape), + }), + }, + () => ({ + vbankAssetsByBrand: zone.mapStore('vbankAssetsByBrand', { + keyShape: BrandShape, + valueShape: M.any(), + }), + }), + { + vbankAssetEntriesWatcher: { + /** + * @param {AssetInfo[]} assets + * @param {{ brand: Brand<'nat'> }} ctx + */ + onFulfilled(assets, { brand }) { + const { vbankAssetsByBrand } = this.state; + vbankAssetsByBrand.addAll(makeCopyMap(assets.map(a => [a.brand, a]))); + if (!vbankAssetsByBrand.has(brand)) { + return watch( + Promise.reject( + Fail`brand ${brand} not in agoricNames.vbankAsset`, + ), + ); + } + return watch(vbankAssetsByBrand.get(brand)); + }, + }, + public: { + /** @param {...string} args */ + lookup(...args) { + return watch(E(agoricNames).lookup(...args)); + }, + /** + * @param {Brand<'nat'>} brand + * @returns {Vow} + */ + findBrandInVBank(brand) { + const { vbankAssetsByBrand } = this.state; + if (vbankAssetsByBrand.has(brand)) { + return watch(vbankAssetsByBrand.get(brand)); + } + const vbankAssetNameHubP = E(agoricNames).lookup('vbankAsset'); + const vbankAssetEntriesP = E(vbankAssetNameHubP).values(); + return watch( + vbankAssetEntriesP, + this.facets.vbankAssetEntriesWatcher, + { brand }, + ); + }, + }, + }, + ); + return makeResumableAgoricNamesHackKit().public; +}; +/** @typedef {ReturnType} AgNamesTools */ diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 5c14d99dee1c..d1e975dd1987 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -45,8 +45,9 @@ export const provideOrchestration = ( marshaller, ) => { const zone = makeDurableZone(baggage); + const { agoricNames, timerService } = remotePowers; - const chainHub = makeChainHub(remotePowers.agoricNames); + const chainHub = makeChainHub(agoricNames); const vowTools = prepareVowTools(zone.subZone('vows')); @@ -55,7 +56,7 @@ export const provideOrchestration = ( zone, makeRecorderKit, zcf, - remotePowers.timerService, + timerService, vowTools, chainHub, ); @@ -103,6 +104,6 @@ export const provideOrchestration = ( vowTools, ...remotePowers, }); - return { ...facade, chainHub, zone }; + return { ...facade, chainHub, vowTools, zone }; }; harden(provideOrchestration); diff --git a/packages/orchestration/test/exos/agoric-names-tools.test.ts b/packages/orchestration/test/exos/agoric-names-tools.test.ts new file mode 100644 index 000000000000..58577bacb242 --- /dev/null +++ b/packages/orchestration/test/exos/agoric-names-tools.test.ts @@ -0,0 +1,52 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { E } from '@endo/far'; +import { V } from '@agoric/vow/vat.js'; +import { makeHeapZone } from '@agoric/zone'; +import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; +import { makeIssuerKit } from '@agoric/ertp'; +import { makeResumableAgoricNamesHack } from '../../src/exos/agoric-names-tools.js'; +import { commonSetup } from '../supports.js'; + +test('agoric names tools', async t => { + const { + bootstrap: { agoricNames, agoricNamesAdmin, bankManager, vowTools }, + brands: { ist }, + } = await commonSetup(t); + + const zone = makeHeapZone(); + const agNamesTools = makeResumableAgoricNamesHack(zone, { + agoricNames, + vowTools, + }); + + const chainEntry = await V.when(agNamesTools.lookup('chain', 'celestia')); + t.like(chainEntry, { chainId: 'celestia' }); + + const istDenom = await V.when(agNamesTools.findBrandInVBank(ist.brand)); + t.like(istDenom, { denom: 'uist' }); + + const moolah = withAmountUtils(makeIssuerKit('MOO')); + + await t.throwsAsync(V.when(agNamesTools.findBrandInVBank(moolah.brand)), { + message: /brand(.*?)not in agoricNames.vbankAsset/, + }); + + await E(E(agoricNamesAdmin).lookupAdmin('vbankAsset')).update( + 'umoo', + /** @type {AssetInfo} */ harden({ + brand: moolah.brand, + issuer: moolah.issuer, + issuerName: 'MOO', + denom: 'umoo', + proposedName: 'MOO', + displayInfo: { decimals: 6, symbol: 'MOO' }, + }), + ); + + t.like( + await V.when(agNamesTools.findBrandInVBank(moolah.brand)), + { denom: 'umoo' }, + 'refresh stale cache for new assets', + ); +});