From 16fc446d2ba3e631464cf86f3091ea60367e0812 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 21 Jun 2024 00:21:21 -0400 Subject: [PATCH 1/2] feat(orchestrate): membrane friendly wrapper for agoricNames * 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 --- .../src/examples/sendAnywhere.contract.js | 32 +++---- .../src/exos/agoric-names-tools.js | 96 +++++++++++++++++++ .../orchestration/src/utils/start-helper.js | 7 +- .../test/exos/agoric-names-tools.test.ts | 52 ++++++++++ 4 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 packages/orchestration/src/exos/agoric-names-tools.js create mode 100644 packages/orchestration/test/exos/agoric-names-tools.test.ts diff --git a/packages/orchestration/src/examples/sendAnywhere.contract.js b/packages/orchestration/src/examples/sendAnywhere.contract.js index 5fefa84713c..081af6dad12 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 00000000000..4e4b8aed9d9 --- /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 5c14d99dee1..d1e975dd198 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 00000000000..58577bacb24 --- /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', + ); +}); From 622ed61b708b2177dc398284816d8f7a0861b064 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Fri, 21 Jun 2024 18:10:16 -0400 Subject: [PATCH 2/2] feat(findBrandInVBank): add optional parameter to invalidate cache --- .../src/examples/sendAnywhere.contract.js | 4 +- .../src/exos/agoric-names-tools.js | 61 +++++++++++-------- .../test/exos/agoric-names-tools.test.ts | 46 +++++++++----- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/packages/orchestration/src/examples/sendAnywhere.contract.js b/packages/orchestration/src/examples/sendAnywhere.contract.js index 081af6dad12..d4af19e20e2 100644 --- a/packages/orchestration/src/examples/sendAnywhere.contract.js +++ b/packages/orchestration/src/examples/sendAnywhere.contract.js @@ -71,8 +71,8 @@ export const start = async (zcf, privateArgs, baggage) => { const { chainName, destAddr } = offerArgs; const { give } = seat.getProposal(); const [[kw, amt]] = entries(give); - // TODO remove V.when() when integrating with asyncFlow - const { denom } = await V.when( + // XXX when() until membrane + const { denom } = await E.when( agoricNamesTools.findBrandInVBank(amt.brand), ); const chain = await orch.getChain(chainName); diff --git a/packages/orchestration/src/exos/agoric-names-tools.js b/packages/orchestration/src/exos/agoric-names-tools.js index 4e4b8aed9d9..cc36ee065f5 100644 --- a/packages/orchestration/src/exos/agoric-names-tools.js +++ b/packages/orchestration/src/exos/agoric-names-tools.js @@ -17,7 +17,9 @@ const { Fail } = assert; * 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 + * https://github.com/Agoric/agoric-sdk/pull/9519. + * + * XXX only works once per zone. * * XXX consider exposing `has`, `entries`, `keys`, `values` from `NameHub` * @@ -26,14 +28,16 @@ const { Fail } = assert; */ export const makeResumableAgoricNamesHack = ( zone, - { agoricNames, vowTools: { watch } }, + { agoricNames, vowTools: { watch, asVow } }, ) => { 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), + findBrandInVBank: M.call(BrandShape) + .optional(M.boolean()) + .returns(VowShape), }), vbankAssetEntriesWatcher: M.interface('vbankAssetEntriesWatcher', { onFulfilled: M.call(M.arrayOf(M.record())) @@ -54,16 +58,15 @@ export const makeResumableAgoricNamesHack = ( * @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 asVow(() => { + const { vbankAssetsByBrand } = this.state; + vbankAssetsByBrand.addAll( + makeCopyMap(assets.map(a => [a.brand, a])), ); - } - return watch(vbankAssetsByBrand.get(brand)); + vbankAssetsByBrand.has(brand) || + Fail`brand ${brand} not in agoricNames.vbankAsset`; + return vbankAssetsByBrand.get(brand); + }); }, }, public: { @@ -72,25 +75,35 @@ export const makeResumableAgoricNamesHack = ( return watch(E(agoricNames).lookup(...args)); }, /** + * Look up asset info, like denom, in agoricNames.vbankAsset using a + * Brand. + * + * Caches the query to agoricNames in the first call. Subsequent lookups + * are via cache unless a refetch is specified or a brand is not found. + * * @param {Brand<'nat'>} brand + * @param {boolean} [refetch] if true, will invalidate the cache * @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 }, - ); + findBrandInVBank(brand, refetch) { + return asVow(() => { + const { vbankAssetsByBrand } = this.state; + if (vbankAssetsByBrand.has(brand) && !refetch) { + return vbankAssetsByBrand.get(brand); + } + const vbankAssetNameHubP = E(agoricNames).lookup('vbankAsset'); + const vbankAssetEntriesP = E(vbankAssetNameHubP).values(); + return watch( + vbankAssetEntriesP, + this.facets.vbankAssetEntriesWatcher, + { brand }, + ); + }); }, }, }, ); + // XXX only works once per zone. return makeResumableAgoricNamesHackKit().public; }; /** @typedef {ReturnType} AgNamesTools */ diff --git a/packages/orchestration/test/exos/agoric-names-tools.test.ts b/packages/orchestration/test/exos/agoric-names-tools.test.ts index 58577bacb24..924a72591a7 100644 --- a/packages/orchestration/test/exos/agoric-names-tools.test.ts +++ b/packages/orchestration/test/exos/agoric-names-tools.test.ts @@ -1,10 +1,10 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { E } from '@endo/far'; -import { V } from '@agoric/vow/vat.js'; +import { heapVowE as E } 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 { AssetInfo } from '@agoric/vats/src/vat-bank.js'; import { makeResumableAgoricNamesHack } from '../../src/exos/agoric-names-tools.js'; import { commonSetup } from '../supports.js'; @@ -20,33 +20,49 @@ test('agoric names tools', async t => { vowTools, }); - const chainEntry = await V.when(agNamesTools.lookup('chain', 'celestia')); + const chainEntry = await E.when(agNamesTools.lookup('chain', 'celestia')); t.like(chainEntry, { chainId: 'celestia' }); - const istDenom = await V.when(agNamesTools.findBrandInVBank(ist.brand)); + const istDenom = await E.when(agNamesTools.findBrandInVBank(ist.brand)); t.like(istDenom, { denom: 'uist' }); const moolah = withAmountUtils(makeIssuerKit('MOO')); - await t.throwsAsync(V.when(agNamesTools.findBrandInVBank(moolah.brand)), { + await t.throwsAsync(E.when(agNamesTools.findBrandInVBank(moolah.brand)), { message: /brand(.*?)not in agoricNames.vbankAsset/, }); + const mooToken: AssetInfo = { + brand: moolah.brand, + issuer: moolah.issuer, + issuerName: 'MOO', + denom: 'umoo', + proposedName: 'MOO', + displayInfo: { decimalPlaces: 6, assetKind: 'nat' }, + }; + 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' }, - }), + harden(mooToken), + ); + t.like( + await E.when(agNamesTools.findBrandInVBank(moolah.brand)), + { denom: 'umoo' }, + 'vbankAssets are refetched if brand is not found', ); + await E(E(agoricNamesAdmin).lookupAdmin('vbankAsset')).update( + 'umoo', + harden({ ...mooToken, denom: 'umoo2' }), + ); t.like( - await V.when(agNamesTools.findBrandInVBank(moolah.brand)), + await E.when(agNamesTools.findBrandInVBank(moolah.brand)), { denom: 'umoo' }, - 'refresh stale cache for new assets', + 'old AssetInfo is cached', + ); + t.like( + await E.when(agNamesTools.findBrandInVBank(moolah.brand, true)), + { denom: 'umoo2' }, + 'new AssetInfo is fetched when refetch=true', ); });