Skip to content

Commit

Permalink
feat(orchestrate): membrane friendly wrapper for agoricNames (#9551)
Browse files Browse the repository at this point in the history
refs: #9449

## Description

 Perform remote calls to agoricNames in membrane-friendly way. This is an interim approach until #9541, #9322, or #9519.
  • Loading branch information
mergify[bot] authored Jun 25, 2024
2 parents 0ad10c6 + 622ed61 commit 80db38c
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 23 deletions.
32 changes: 12 additions & 20 deletions packages/orchestration/src/examples/sendAnywhere.contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
*/

Expand Down Expand Up @@ -45,44 +44,37 @@ 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<any>} */
let contractAccount;

const findBrandInVBank = async brand => {
const assets = await E(
// XXX heapVowE
/** @type {Promise<Promise<NameHub<VBankAssetDetail>>>} */ (
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() }),
);
const { chainName, destAddr } = offerArgs;
const { give } = seat.getProposal();
const [[kw, amt]] = entries(give);
const { denom } = await findBrandInVBank(amt.brand);
// XXX when() until membrane
const { denom } = await E.when(
agoricNamesTools.findBrandInVBank(amt.brand),
);
const chain = await orch.getChain(chainName);

// FIXME ok to use a heap var crossing the membrane scope this way?
Expand Down
109 changes: 109 additions & 0 deletions packages/orchestration/src/exos/agoric-names-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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 only works once per zone.
*
* XXX consider exposing `has`, `entries`, `keys`, `values` from `NameHub`
*
* @param {Zone} zone
* @param {{ agoricNames: Remote<NameHub>; vowTools: VowTools }} powers
*/
export const makeResumableAgoricNamesHack = (
zone,
{ 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)
.optional(M.boolean())
.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 }) {
return asVow(() => {
const { vbankAssetsByBrand } = this.state;
vbankAssetsByBrand.addAll(
makeCopyMap(assets.map(a => [a.brand, a])),
);
vbankAssetsByBrand.has(brand) ||
Fail`brand ${brand} not in agoricNames.vbankAsset`;
return vbankAssetsByBrand.get(brand);
});
},
},
public: {
/** @param {...string} args */
lookup(...args) {
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<AssetInfo>}
*/
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<typeof makeResumableAgoricNamesHack>} AgNamesTools */
7 changes: 4 additions & 3 deletions packages/orchestration/src/utils/start-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand All @@ -55,7 +56,7 @@ export const provideOrchestration = (
zone,
makeRecorderKit,
zcf,
remotePowers.timerService,
timerService,
vowTools,
chainHub,
);
Expand Down Expand Up @@ -103,6 +104,6 @@ export const provideOrchestration = (
vowTools,
...remotePowers,
});
return { ...facade, chainHub, zone };
return { ...facade, chainHub, vowTools, zone };
};
harden(provideOrchestration);
68 changes: 68 additions & 0 deletions packages/orchestration/test/exos/agoric-names-tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { test } from '@agoric/zoe/tools/prepare-test-env-ava.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';

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 E.when(agNamesTools.lookup('chain', 'celestia'));
t.like(chainEntry, { chainId: 'celestia' });

const istDenom = await E.when(agNamesTools.findBrandInVBank(ist.brand));
t.like(istDenom, { denom: 'uist' });

const moolah = withAmountUtils(makeIssuerKit('MOO'));

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',
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 E.when(agNamesTools.findBrandInVBank(moolah.brand)),
{ denom: 'umoo' },
'old AssetInfo is cached',
);
t.like(
await E.when(agNamesTools.findBrandInVBank(moolah.brand, true)),
{ denom: 'umoo2' },
'new AssetInfo is fetched when refetch=true',
);
});

0 comments on commit 80db38c

Please sign in to comment.