diff --git a/golang/cosmos/x/vstorage/vstorage.go b/golang/cosmos/x/vstorage/vstorage.go index 3f94f52593c..b2120948a30 100644 --- a/golang/cosmos/x/vstorage/vstorage.go +++ b/golang/cosmos/x/vstorage/vstorage.go @@ -197,7 +197,11 @@ func (sh vstorageHandler) Receive(cctx *vm.ControllerContext, str string) (ret s entries := make([][]interface{}, len(children.Children)) for i, child := range children.Children { entry := keeper.GetEntry(cctx.Context, fmt.Sprintf("%s.%s", path, child)) - entries[i] = []interface{}{child, entry.Value()} + if !entry.HasData() { + entries[i] = []interface{}{child} + } else { + entries[i] = []interface{}{child, entry.Value()} + } } bytes, err := json.Marshal(entries) if err != nil { diff --git a/golang/cosmos/x/vstorage/vstorage_test.go b/golang/cosmos/x/vstorage/vstorage_test.go index b316aa8dbc1..02e478aea09 100644 --- a/golang/cosmos/x/vstorage/vstorage_test.go +++ b/golang/cosmos/x/vstorage/vstorage_test.go @@ -110,7 +110,7 @@ func TestGetAndHas(t *testing.T) { t.Errorf("%s: got unexpected error %v", desc.label, err) } - // Verify that has returns false iff get returns null. + // Verify that `has` returns false iff `get` returns null. noData := desc.want == `null` if (noData && has != `false`) || (!noData && has != `true`) { t.Errorf("%s: got has %v; want %v", desc.label, has, !noData) @@ -274,13 +274,13 @@ func TestEntries(t *testing.T) { want string } cases := []testCase{ - {path: "key1", want: `[["child1",null]]`}, + {path: "key1", want: `[["child1"]]`}, {path: "key1.child1", // Empty non-terminals are included, empty leaves are not. - want: `[["empty-non-terminal",null],["grandchild1","value1grandchild"]]`}, + want: `[["empty-non-terminal"],["grandchild1","value1grandchild"]]`}, {path: "key1.child1.grandchild1", want: `[]`}, {path: "key1.child1.empty-non-terminal", want: `[["leaf",""]]`}, - {path: "key2", want: `[["child2",null]]`}, + {path: "key2", want: `[["child2"]]`}, {path: "key2.child2", want: `[["grandchild2","value2grandchild"],["grandchild2a","value2grandchilda"]]`}, {path: "nosuchkey", want: `[]`}, diff --git a/packages/SwingSet/src/types-external.js b/packages/SwingSet/src/types-external.js index 0189fac650c..1f2da52bd5b 100644 --- a/packages/SwingSet/src/types-external.js +++ b/packages/SwingSet/src/types-external.js @@ -180,7 +180,7 @@ export {}; * @property {string[]} [exportStorageSubtrees] chain storage paths identifying roots of subtrees * for which data should be exported into bootstrap vat parameter `chainStorageEntries` * (e.g., `exportStorageSubtrees: ['c.o']` might result in vatParameters including - * `chainStorageEntries: [ ['c.o', null], ['c.o.i', null], ['c.o.i.n', '42'], ['c.o.w', '"moo"'] ]`). + * `chainStorageEntries: [ ['c.o', '"top"'], ['c.o.i'], ['c.o.i.n', '42'], ['c.o.w', '"moo"'] ]`). * @property {boolean} [includeDevDependencies] indicates that * `devDependencies` of the surrounding `package.json` should be accessible to * bundles. diff --git a/packages/casting/package.json b/packages/casting/package.json index a0c6c14952a..b70f1d79d53 100644 --- a/packages/casting/package.json +++ b/packages/casting/package.json @@ -22,6 +22,7 @@ "author": "Agoric", "license": "Apache-2.0", "dependencies": { + "@agoric/internal": "^0.2.1", "@agoric/notifier": "^0.5.1", "@agoric/spawner": "^0.6.3", "@agoric/store": "^0.8.3", diff --git a/packages/casting/src/follower-cosmjs.js b/packages/casting/src/follower-cosmjs.js index fb2b542cfb0..8a966fb4c1e 100644 --- a/packages/casting/src/follower-cosmjs.js +++ b/packages/casting/src/follower-cosmjs.js @@ -5,6 +5,8 @@ import { E, Far } from '@endo/far'; import * as tendermint34 from '@cosmjs/tendermint-rpc'; import * as stargateStar from '@cosmjs/stargate'; +import { isStreamCell } from '@agoric/internal/src/lib-chainStorage.js'; + import { MAKE_DEFAULT_DECODER, MAKE_DEFAULT_UNSERIALIZER } from './defaults.js'; import { makeCastingSpec } from './casting-spec.js'; import { makeLeader as defaultMakeLeader } from './leader-netconfig.js'; @@ -27,21 +29,6 @@ const textDecoder = new TextDecoder(); * to abstract away Tendermint versions. */ -/** - * This is an imperfect heuristic to navigate the migration from value cells to - * stream cells. - * At time of writing, no legacy cells have the same shape as a stream cell, - * and we do not intend to create any more legacy value cells. - * - * @param {any} cell - */ -const isStreamCell = cell => - cell && - typeof cell === 'object' && - Array.isArray(cell.values) && - typeof cell.blockHeight === 'string' && - /^0$|^[1-9][0-9]*$/.test(cell.blockHeight); - /** * @param {Uint8Array} a * @param {Uint8Array} b diff --git a/packages/cosmic-swingset/src/launch-chain.js b/packages/cosmic-swingset/src/launch-chain.js index 9e77279a8f6..f48be2460f8 100644 --- a/packages/cosmic-swingset/src/launch-chain.js +++ b/packages/cosmic-swingset/src/launch-chain.js @@ -124,7 +124,7 @@ export async function buildSwingset( bootVat.parameters = { ...bootVat.parameters, coreProposalCode: code }; } - // Extract data from chain storage. + // Extract data from chain storage as [path, value?] pairs. if (exportStorageSubtrees) { // Disallow exporting internal details like bundle contents and the action queue. const exportRoot = STORAGE_PATH.CUSTOM; @@ -137,19 +137,22 @@ export async function buildSwingset( const callChainStorage = (method, path) => bridgeOutbound(BRIDGE_ID.STORAGE, { method, args: [path] }); + const makeExportEntry = (path, value) => + value == null ? [path] : [path, value]; + const chainStorageEntries = []; // Preserve the ordering of each subtree via depth-first traversal. let pendingEntries = exportStorageSubtrees.map(path => { const value = callChainStorage('get', path); - return [path, value]; + return makeExportEntry(path, value); }); while (pendingEntries.length > 0) { - const entry = /** @type {[string, string]} */ (pendingEntries.shift()); + const entry = /** @type {[string, string?]} */ (pendingEntries.shift()); chainStorageEntries.push(entry); const [path, _value] = entry; const childEntryData = callChainStorage('entries', path); const childEntries = childEntryData.map(([pathSegment, value]) => { - return [`${path}.${pathSegment}`, value]; + return makeExportEntry(`${path}.${pathSegment}`, value); }); pendingEntries = [...childEntries, ...pendingEntries]; } diff --git a/packages/inter-protocol/src/proposals/startPSM.js b/packages/inter-protocol/src/proposals/startPSM.js index 1f2564f8576..6d4b02e50d9 100644 --- a/packages/inter-protocol/src/proposals/startPSM.js +++ b/packages/inter-protocol/src/proposals/startPSM.js @@ -7,7 +7,11 @@ import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js'; import { E } from '@endo/far'; import { Stable } from '@agoric/vats/src/tokens.js'; -import { makeHistoryReviver } from '@agoric/vats/tools/board-utils.js'; +import { + makeHistoryReviver, + makeBoardRemote, + slotToBoardRemote, +} from '@agoric/vats/tools/board-utils.js'; import { deeplyFulfilledObject } from '@agoric/internal'; import { makeScalarBigMapStore } from '@agoric/vat-data'; @@ -39,7 +43,10 @@ const stablePsmKey = `published.psm.${Stable.symbol}`; const findOldPSMState = (chainStorageEntries, keyword, brands) => { // In this reviver, object references are revived as boardIDs // from the pre-bulldozer board. - const toSlotReviver = makeHistoryReviver(chainStorageEntries); + const toSlotReviver = makeHistoryReviver( + chainStorageEntries, + slotToBoardRemote, + ); if (!toSlotReviver.has(`${stablePsmKey}.${keyword}.metrics`)) { return {}; } @@ -47,12 +54,19 @@ const findOldPSMState = (chainStorageEntries, keyword, brands) => { `${stablePsmKey}.${keyword}.metrics`, ); const oldIDtoNewBrand = makeMap([ - [metricsWithOldBoardIDs.feePoolBalance.brand, brands.minted], - [metricsWithOldBoardIDs.anchorPoolBalance.brand, brands.anchor], + [metricsWithOldBoardIDs.feePoolBalance.brand.getBoardId(), brands.minted], + [ + metricsWithOldBoardIDs.anchorPoolBalance.brand.getBoardId(), + brands.anchor, + ], ]); - // revive brands; other object references map to undefined - const brandReviver = makeHistoryReviver(chainStorageEntries, s => - oldIDtoNewBrand.get(s), + // revive brands; other object references map to dummy remotables + const brandReviver = makeHistoryReviver( + chainStorageEntries, + (slotID, iface) => { + const newBrand = oldIDtoNewBrand.get(slotID); + return newBrand || makeBoardRemote({ boardId: slotID, iface }); + }, ); return { metrics: brandReviver.getItem(`${stablePsmKey}.${keyword}.metrics`), @@ -340,7 +354,10 @@ export const makeAnchorAsset = async ( testFirstAnchorKit.resolve(kit); - const toSlotReviver = makeHistoryReviver(chainStorageEntries); + const toSlotReviver = makeHistoryReviver( + chainStorageEntries, + slotToBoardRemote, + ); const metricsKey = `${stablePsmKey}.${keyword}.metrics`; if (toSlotReviver.has(metricsKey)) { const metrics = toSlotReviver.getItem(metricsKey); @@ -351,6 +368,8 @@ export const makeAnchorAsset = async ( // eslint-disable-next-line @jessie.js/no-nested-await const anchorPaymentMap = await anchorBalancePayments; + // TODO: validate that `metrics.anchorPoolBalance.value` is + // pass-by-copy PureData (e.g., contains no remotables). // eslint-disable-next-line @jessie.js/no-nested-await const pmt = await E(mint).mintPayment( AmountMath.make(brand, metrics.anchorPoolBalance.value), diff --git a/packages/internal/src/lib-chainStorage.js b/packages/internal/src/lib-chainStorage.js index 46807045ecd..e50464bf1aa 100644 --- a/packages/internal/src/lib-chainStorage.js +++ b/packages/internal/src/lib-chainStorage.js @@ -19,6 +19,13 @@ const { Fail } = assert; * @property {string} [noDataValue] */ +/** + * @template [T=unknown] + * @typedef StreamCell + * @property {string} blockHeight decimal representation of a natural number + * @property {T[]} values + */ + /** * This represents a node in an IAVL tree. * @@ -44,6 +51,70 @@ const ChainStorageNodeI = M.interface('StorageNode', { .returns(M.remotable('StorageNode')), }); +/** + * This is an imperfect heuristic to navigate the migration from value cells to + * stream cells. + * At time of writing, no legacy cells have the same shape as a stream cell, + * and we do not intend to create any more legacy value cells. + * + * @param {any} cell + * @returns {cell is StreamCell} + */ +export const isStreamCell = cell => + cell && + typeof cell === 'object' && + Array.isArray(cell.values) && + typeof cell.blockHeight === 'string' && + /^0$|^[1-9][0-9]*$/.test(cell.blockHeight); +harden(isStreamCell); + +// TODO: Consolidate with `insistCapData` functions from swingset-liveslots, +// swingset-xsnap-supervisor, etc. +/** + * @param {unknown} data + * @returns {asserts data is import('@endo/marshal').CapData} + */ +export const assertCapData = data => { + assert.typeof(data, 'object'); + assert(data); + assert.typeof(data.body, 'string'); + assert(Array.isArray(data.slots)); + // XXX check that the .slots array elements are actually strings +}; +harden(assertCapData); + +/** + * Read and unmarshal a value from a map representation of vstorage data + * + * @param {Map} data + * @param {string} key + * @param {ReturnType['fromCapData']} fromCapData + * @param {number} [index=-1] index of the desired value in a deserialized stream cell + */ +export const unmarshalFromVstorage = (data, key, fromCapData, index = -1) => { + const serialized = data.get(key) || Fail`no data for ${key}`; + assert.typeof(serialized, 'string'); + + const streamCell = JSON.parse(serialized); + if (!isStreamCell(streamCell)) { + throw Fail`not a StreamCell: ${streamCell}`; + } + + const { values } = streamCell; + values.length > 0 || Fail`no StreamCell values: ${streamCell}`; + + const marshalled = values.at(index); + assert.typeof(marshalled, 'string'); + + /** @type {import("@endo/marshal").CapData} */ + const capData = harden(JSON.parse(marshalled)); + assertCapData(capData); + + const unmarshalled = fromCapData(capData); + return unmarshalled; +}; +harden(unmarshalFromVstorage); + /** * @typedef {object} StoredFacet * @property {() => Promise} getPath the chain storage path at which the node was constructed diff --git a/packages/internal/src/storage-test-utils.js b/packages/internal/src/storage-test-utils.js index 9ab304e4948..651b52c1a50 100644 --- a/packages/internal/src/storage-test-utils.js +++ b/packages/internal/src/storage-test-utils.js @@ -1,10 +1,26 @@ // @ts-check import { Far } from '@endo/far'; import { makeMarshal, Remotable } from '@endo/marshal'; -import { makeChainStorageRoot } from './lib-chainStorage.js'; +import { + isStreamCell, + makeChainStorageRoot, + unmarshalFromVstorage, +} from './lib-chainStorage.js'; import { bindAllMethods } from './method-tools.js'; -const { Fail, quote: q } = assert; +const { Fail } = assert; + +/** + * A map corresponding with a total function such that `get(key)` + * is assumed to always succeed. + * + * @template K, V + * @typedef {{[k in Exclude, 'get'>]: Map[k]} & {get: (key: K) => V}} TotalMap + */ +/** + * @template T + * @typedef {T extends Map ? TotalMap : never} TotalMapFrom + */ /** * A convertSlotToVal function that produces basic Remotables. Assumes @@ -71,54 +87,88 @@ export const slotStringUnserialize = makeSlotStringUnserialize(); /** * For testing, creates a chainStorage root node over an in-memory map * and exposes both the map and the sequence of received messages. + * The `sequence` option defaults to true. * * @param {string} rootPath * @param {Parameters[2]} [rootOptions] */ export const makeFakeStorageKit = (rootPath, rootOptions) => { - /** @type {Map} */ + const resolvedOptions = { sequence: true, ...rootOptions }; + /** @type {TotalMap} */ const data = new Map(); /** @type {import('../src/lib-chainStorage.js').StorageMessage[]} */ const messages = []; /** @param {import('../src/lib-chainStorage.js').StorageMessage} message */ // eslint-disable-next-line consistent-return - const toStorage = async message => { + const toStorage = message => { messages.push(message); switch (message.method) { case 'getStoreKey': { - return { - storeName: 'swingset', - storeSubkey: `fake:${message.args[0]}`, - }; + const [key] = message.args; + return { storeName: 'swingset', storeSubkey: `fake:${key}` }; + } + case 'get': { + const [key] = message.args; + return data.has(key) ? data.get(key) : null; + } + case 'entries': { + const [key] = message.args; + const prefix = `${key}.`; + const childData = new Map(); + for (const [path, value] of data.entries()) { + if (!path.startsWith(prefix)) { + continue; + } + const [segment, ...suffix] = path.slice(prefix.length).split('.'); + if (suffix.length === 0) { + childData.set(segment, value); + } else if (!childData.has(segment)) { + childData.set(segment, null); + } + } + return [...childData.entries()].map(entry => + entry[1] != null ? entry : [entry[0]], + ); } - case 'set': - for (const [key, value] of message.args) { - if (value !== undefined) { - data.set(key, [value]); + case 'set': { + /** @type {import('../src/lib-chainStorage.js').StorageEntry[]} */ + const newEntries = message.args; + for (const [key, value] of newEntries) { + if (value != null) { + data.set(key, value); } else { data.delete(key); } } break; - case 'append': - for (const [key, value] of message.args) { - if (value === undefined) { - throw Error(`attempt to append with no value`); - } - let sequence = data.get(key); - if (!Array.isArray(sequence)) { - if (sequence === undefined) { - // Initialize an empty collection. - sequence = []; - } else { - // Wrap a previous single value in a collection. - sequence = [sequence]; + } + case 'append': { + /** @type {import('../src/lib-chainStorage.js').StorageEntry[]} */ + const newEntries = message.args; + for (const [key, value] of newEntries) { + value != null || Fail`attempt to append with no value`; + // In the absence of block boundaries, everything goes in a single StreamCell. + const oldVal = data.get(key); + let streamCell; + if (oldVal != null) { + try { + streamCell = JSON.parse(oldVal); + assert(isStreamCell(streamCell)); + } catch (_err) { + streamCell = undefined; } - data.set(key, sequence); } - sequence.push(value); + if (streamCell === undefined) { + streamCell = { + blockHeight: '0', + values: oldVal != null ? [oldVal] : [], + }; + } + streamCell.values.push(value); + data.set(key, JSON.stringify(streamCell)); } break; + } case 'size': // Intentionally incorrect because it counts non-child descendants, // but nevertheless supports a "has children" test. @@ -128,7 +178,7 @@ export const makeFakeStorageKit = (rootPath, rootOptions) => { throw Error(`unsupported method: ${message.method}`); } }; - const rootNode = makeChainStorageRoot(toStorage, rootPath, rootOptions); + const rootNode = makeChainStorageRoot(toStorage, rootPath, resolvedOptions); return { rootNode, data, messages, toStorage }; }; harden(makeFakeStorageKit); @@ -150,14 +200,10 @@ export const makeMockChainStorageRoot = () => { */ getBody: (path, marshaller = defaultMarshaller) => { data.size || Fail`no data in storage`; - const dataStr = data.get(path)?.at(-1); - if (!dataStr) { - console.debug('mockChainStorage data:', data); - Fail`no data at ${q(path)}`; - } - assert.typeof(dataStr, 'string'); - const datum = JSON.parse(dataStr); - return marshaller.fromCapData(datum); + /** @type {ReturnType['fromCapData']} */ + const fromCapData = (...args) => + Reflect.apply(marshaller.fromCapData, marshaller, args); + return unmarshalFromVstorage(data, path, fromCapData); }, keys: () => [...data.keys()], }); diff --git a/packages/internal/test/test-priority-senders.js b/packages/internal/test/test-priority-senders.js index 3ce040ea41e..c3990235bf8 100644 --- a/packages/internal/test/test-priority-senders.js +++ b/packages/internal/test/test-priority-senders.js @@ -15,7 +15,7 @@ test('basic', async t => { const nodeEquals = async ( /** @type {string} */ address, - /** @type {string[] | undefined} */ val, + /** @type {string | undefined} */ val, ) => { await writesSettled(); const key = `${HIGH_PRIORITY_SENDERS}.${address}`; @@ -23,21 +23,21 @@ test('basic', async t => { }; await manager.add('oracles', 'agoric1a'); - await nodeEquals('agoric1a', ['oracles']); + await nodeEquals('agoric1a', 'oracles'); await manager.remove('oracles', 'agoric1a'); await nodeEquals('agoric1a', undefined); await manager.add('oracles', 'agoric1a'); - await nodeEquals('agoric1a', ['oracles']); + await nodeEquals('agoric1a', 'oracles'); await manager.add('ec', 'agoric1a'); - await nodeEquals('agoric1a', ['ec,oracles']); + await nodeEquals('agoric1a', 'ec,oracles'); await manager.add('oracles', 'agoric1b'); - await nodeEquals('agoric1b', ['oracles']); + await nodeEquals('agoric1b', 'oracles'); await manager.remove('oracles', 'agoric1a'); - await nodeEquals('agoric1a', ['ec']); + await nodeEquals('agoric1a', 'ec'); }); test('errors', async t => { @@ -71,7 +71,8 @@ test('normalization', async t => { await manager.add('this,has,commas,', 'addr'); await writesSettled(); - t.deepEqual(storage.data.get(`${HIGH_PRIORITY_SENDERS}.addr`), [ + t.deepEqual( + storage.data.get(`${HIGH_PRIORITY_SENDERS}.addr`), 'something_with_spaces,something_with_spaces_and___,this_has_commas_', - ]); + ); }); diff --git a/packages/internal/test/test-storage-test-utils.js b/packages/internal/test/test-storage-test-utils.js index b3b5136a82c..adccf8df054 100644 --- a/packages/internal/test/test-storage-test-utils.js +++ b/packages/internal/test/test-storage-test-utils.js @@ -12,7 +12,8 @@ import { test('makeFakeStorageKit', async t => { const rootPath = 'root'; - const { rootNode, messages } = makeFakeStorageKit(rootPath); + const opts = { sequence: false }; + const { rootNode, messages, toStorage } = makeFakeStorageKit(rootPath, opts); t.is(rootNode.getPath(), rootPath); const rootStoreKey = await rootNode.getStoreKey(); t.deepEqual( @@ -150,6 +151,16 @@ test('makeFakeStorageKit', async t => { [{ method: 'set', args: [[deepPath, 'foo']] }], 'level-skipping setValue message', ); + t.deepEqual( + await toStorage({ method: 'entries', args: [`${rootPath}.child`] }), + [['grandchild', 'foo']], + 'child entries', + ); + t.deepEqual( + await toStorage({ method: 'entries', args: [rootPath] }), + [...extremeSegments.map(segment => [segment, 'foo']), ['child']], + 'entries include empty non-terminals', + ); await childNode.setValue(''); t.deepEqual( diff --git a/packages/smart-wallet/src/walletFactory.js b/packages/smart-wallet/src/walletFactory.js index 846c26833bb..bf7d281c35c 100644 --- a/packages/smart-wallet/src/walletFactory.js +++ b/packages/smart-wallet/src/walletFactory.js @@ -122,6 +122,12 @@ export const makeAssetRegistry = assetPublisher => { * import('@agoric/notifier/src/types').IterableEachTopic< * import('@agoric/vats/src/vat-bank').AssetDescriptor>> * }} AssetPublisher + * + * @typedef {boolean} isRevive + * @typedef {{ + * reviveWallet: (address: string) => Promise, + * ackWallet: (address: string) => isRevive, + * }} WalletReviver */ // NB: even though all the wallets share this contract, they @@ -132,6 +138,7 @@ export const makeAssetRegistry = assetPublisher => { * @param {{ * storageNode: ERef, * walletBridgeManager?: ERef, + * walletReviver?: ERef, * }} privateArgs * @param {import('@agoric/vat-data').Baggage} baggage */ @@ -139,7 +146,7 @@ export const prepare = async (zcf, privateArgs, baggage) => { const { agoricNames, board, assetPublisher } = zcf.getTerms(); const zoe = zcf.getZoeService(); - const { storageNode, walletBridgeManager } = privateArgs; + const { storageNode, walletBridgeManager, walletReviver } = privateArgs; /** @type {MapStore} */ const walletsByAddress = provideDurableMapStore(baggage, 'walletsByAddress'); @@ -179,7 +186,15 @@ export const prepare = async (zcf, privateArgs, baggage) => { ); mustMatch(harden(actionCapData), shape.StringCapData); - const wallet = walletsByAddress.get(obj.owner); // or throw + // Revive an old wallet if necessary, but otherwise + // insist that it is already in the store. + const address = obj.owner; + const walletP = + !walletsByAddress.has(address) && walletReviver + ? // this will call provideSmartWallet which will update `walletsByAddress` for next time + E(walletReviver).reviveWallet(address) + : walletsByAddress.get(address); // or throw + const wallet = await walletP; console.log('walletFactory:', { wallet, actionCapData }); return E(wallet).handleBridgeAction(actionCapData, canSpend); @@ -237,14 +252,15 @@ export const prepare = async (zcf, privateArgs, baggage) => { * @param {string} address * @param {ERef} bank * @param {ERef} namesByAddressAdmin - * @returns {Promise<[import('./smartWallet').SmartWallet, boolean]>} wallet + * @returns {Promise<[wallet: import('./smartWallet').SmartWallet, isNew: boolean]>} wallet * along with a flag to distinguish between looking up an existing wallet * and creating a new one. */ provideSmartWallet(address, bank, namesByAddressAdmin) { - let makerCalled = false; - /** @type {() => Promise} */ - const maker = async () => { + let isNew = false; + + /** @type {(address: string) => Promise} */ + const maker = async _address => { const invitationPurse = await E(invitationIssuer).makeEmptyPurse(); const walletStorageNode = E(storageNode).makeChildNode(address); const wallet = await makeSmartWallet( @@ -254,13 +270,20 @@ export const prepare = async (zcf, privateArgs, baggage) => { // An await here would deadlock with invitePSMCommitteeMembers void publishDepositFacet(address, wallet, namesByAddressAdmin); - makerCalled = true; + isNew = true; return wallet; }; + const finisher = walletReviver + ? async (_address, _wallet) => { + const isRevive = await E(walletReviver).ackWallet(address); + isNew = !isRevive; + } + : undefined; + return provider - .provideAsync(address, maker) - .then(w => [w, makerCalled]); + .provideAsync(address, maker, finisher) + .then(w => [w, isNew]); }, }, ); diff --git a/packages/vats/decentral-devnet-config.json b/packages/vats/decentral-devnet-config.json index cf49d9ceeeb..cd2e297f9d6 100644 --- a/packages/vats/decentral-devnet-config.json +++ b/packages/vats/decentral-devnet-config.json @@ -163,6 +163,10 @@ } } }, + "exportStorageSubtrees": [ + "published.psm.IST", + "published.wallet" + ], "bundles": { "agoricNames": { "sourceSpec": "@agoric/vats/src/vat-agoricNames.js" diff --git a/packages/vats/decentral-test-vaults-config.json b/packages/vats/decentral-test-vaults-config.json index 1b2cd2c10f4..918b54b22de 100644 --- a/packages/vats/decentral-test-vaults-config.json +++ b/packages/vats/decentral-test-vaults-config.json @@ -159,6 +159,10 @@ } } }, + "exportStorageSubtrees": [ + "published.psm.IST", + "published.wallet" + ], "bundles": { "agoricNames": { "sourceSpec": "@agoric/vats/src/vat-agoricNames.js" diff --git a/packages/vats/src/core/startWalletFactory.js b/packages/vats/src/core/startWalletFactory.js index fd50dd08555..c4ccd187444 100644 --- a/packages/vats/src/core/startWalletFactory.js +++ b/packages/vats/src/core/startWalletFactory.js @@ -5,6 +5,10 @@ import { makeTracer, VBankAccount } from '@agoric/internal'; import { AmountMath } from '@agoric/ertp'; import { ParamTypes } from '@agoric/governance'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; +import { + makeHistoryReviver, + slotToBoardRemote, +} from '../../tools/board-utils.js'; import { Stable } from '../tokens.js'; const trace = makeTracer('StartWF'); @@ -23,7 +27,7 @@ const StableUnit = BigInt(10 ** Stable.displayInfo.decimalPlaces); * Register for PLEASE_PROVISION bridge messages and handle * them by providing a smart wallet from the wallet factory. * - * @param {BootstrapPowers & PromiseSpaceOf<{ + * @param {BootstrapPowers & ChainStorageVatParams & PromiseSpaceOf<{ * economicCommitteeCreatorFacet: import('@agoric/governance/src/committee.js').CommitteeElectorateCreatorFacet * econCharterKit: { * creatorFacet: Awaited>['creatorFacet'], @@ -40,6 +44,7 @@ const StableUnit = BigInt(10 ** Stable.displayInfo.decimalPlaces); */ export const startWalletFactory = async ( { + vatParameters: { chainStorageEntries = [] }, consume: { agoricNames, bankManager, @@ -101,7 +106,34 @@ export const startWalletFactory = async ( feeIssuerP, ]); + // Carry forward wallets with an address already in chain storage. + const dataReviver = makeHistoryReviver( + chainStorageEntries, + slotToBoardRemote, + ); + const walletStoragePath = await E(walletStorageNode).getPath(); + const oldAddresses = dataReviver.children(`${walletStoragePath}.`); + const poolBank = E(bankManager).getBankForAddress(poolAddr); + const ppFacets = await E(startGovernedUpgradable)({ + installation: provisionPool, + terms: {}, + privateArgs: harden({ + poolBank, + storageNode: poolStorageNode, + marshaller: await E(board).getPublishingMarshaller(), + }), + label: 'provisionPool', + governedParams: { + PerAccountInitialAmount: { + type: ParamTypes.AMOUNT, + value: AmountMath.make(feeBrand, perAccountInitialValue), + }, + }, + }); + provisionPoolStartResult.resolve(ppFacets); + instanceProduce.provisionPool.resolve(ppFacets.instance); + const terms = await deeplyFulfilled( harden({ agoricNames, @@ -121,39 +153,25 @@ export const startWalletFactory = async ( privateArgs: { storageNode: walletStorageNode, walletBridgeManager, + walletReviver: E(ppFacets.creatorFacet).getWalletReviver(), }, label: 'walletFactory', }); walletFactoryStartResult.resolve(wfFacets); instanceProduce.walletFactory.resolve(wfFacets.instance); - const ppFacets = await E(startGovernedUpgradable)({ - installation: provisionPool, - terms: {}, - privateArgs: harden({ - poolBank, - storageNode: poolStorageNode, - marshaller: await E(board).getPublishingMarshaller(), + await Promise.all([ + E(ppFacets.creatorFacet).addRevivableAddresses(oldAddresses), + E(ppFacets.creatorFacet).setReferences({ + bankManager, + namesByAddressAdmin, + walletFactory: wfFacets.creatorFacet, }), - label: 'provisionPool', - governedParams: { - PerAccountInitialAmount: { - type: ParamTypes.AMOUNT, - value: AmountMath.make(feeBrand, perAccountInitialValue), - }, - }, - }); - provisionPoolStartResult.resolve(ppFacets); - instanceProduce.provisionPool.resolve(ppFacets.instance); - - const handler = await E(ppFacets.creatorFacet).makeHandler({ - bankManager, - namesByAddressAdmin, - walletFactory: wfFacets.creatorFacet, - }); + ]); + const bridgeHandler = await E(ppFacets.creatorFacet).makeHandler(); await Promise.all([ - E(provisionWalletBridgeManager).initHandler(handler), + E(provisionWalletBridgeManager).initHandler(bridgeHandler), E(E.get(econCharterKit).creatorFacet).addInstance( ppFacets.instance, ppFacets.governorCreatorFacet, @@ -177,6 +195,7 @@ export const startWalletFactory = async ( export const WALLET_FACTORY_MANIFEST = { [startWalletFactory.name]: { + vatParameters: { chainStorageEntries: true }, consume: { agoricNames: true, bankManager: 'bank', diff --git a/packages/vats/src/provisionPool.js b/packages/vats/src/provisionPool.js index 3fc4cbb0b5e..86a680bd78a 100644 --- a/packages/vats/src/provisionPool.js +++ b/packages/vats/src/provisionPool.js @@ -31,8 +31,8 @@ export const privateArgsShape = harden({ * @param {{ * poolBank: import('@endo/far').ERef, * initialPoserInvitation: Invitation, - * storageNode: StorageNode, - * marshaller: Marshaller + * storageNode: StorageNode, + * marshaller: Marshaller, * }} privateArgs * @param {import('@agoric/vat-data').Baggage} baggage */ diff --git a/packages/vats/src/provisionPoolKit.js b/packages/vats/src/provisionPoolKit.js index 5e2214f231b..f14fff53e9b 100644 --- a/packages/vats/src/provisionPoolKit.js +++ b/packages/vats/src/provisionPoolKit.js @@ -6,20 +6,32 @@ import { observeNotifier, subscribeEach, } from '@agoric/notifier'; -import { M, makeScalarBigMapStore, prepareExoClassKit } from '@agoric/vat-data'; +import { + M, + makeScalarBigMapStore, + makeScalarBigSetStore, + prepareExoClassKit, +} from '@agoric/vat-data'; import { makeRecorderTopic, PublicTopicShape, } from '@agoric/zoe/src/contractSupport/topics.js'; import { InstanceHandleShape } from '@agoric/zoe/src/typeGuards.js'; import { E } from '@endo/far'; -import { Far } from '@endo/marshal'; +import { deeplyFulfilled, Far } from '@endo/marshal'; import { PowerFlags } from './walletFlags.js'; -const { details: X, quote: q } = assert; +const { details: X, quote: q, Fail } = assert; /** @typedef {import('@agoric/zoe/src/zoeService/utils').Instance} PsmInstance */ +/** + * @typedef {object} ProvisionPoolKitReferences + * @property {ERef} bankManager + * @property {ERef} namesByAddressAdmin + * @property {ERef} walletFactory + */ + /** * @typedef {object} MetricsNotification * Metrics naming scheme is that nouns are present values and past-participles @@ -41,30 +53,25 @@ const { details: X, quote: q } = assert; */ export const makeBridgeProvisionTool = (sendInitialPayment, onProvisioned) => { /** - * @param {{ - * bankManager: ERef, - * namesByAddressAdmin: ERef, - * walletFactory: ERef - * }} io + * @param {ProvisionPoolKitReferences} refs */ - const makeHandler = ({ bankManager, namesByAddressAdmin, walletFactory }) => + const makeBridgeHandler = ({ + bankManager, + namesByAddressAdmin, + walletFactory, + }) => Far('provisioningHandler', { fromBridge: async obj => { - assert.equal( - obj.type, - 'PLEASE_PROVISION', - X`Unrecognized request ${obj.type}`, - ); + obj.type === 'PLEASE_PROVISION' || + Fail`Unrecognized request ${obj.type}`; console.info('PLEASE_PROVISION', obj); const { address, powerFlags } = obj; - assert( - powerFlags.includes(PowerFlags.SMART_WALLET), - 'missing SMART_WALLET in powerFlags', - ); + powerFlags.includes(PowerFlags.SMART_WALLET) || + Fail`missing SMART_WALLET in powerFlags`; const bank = E(bankManager).getBankForAddress(address); + // only proceed if we can provide funds await sendInitialPayment(bank); - // only proceed if we can provide funds const [_, created] = await E(walletFactory).provideSmartWallet( address, @@ -77,16 +84,17 @@ export const makeBridgeProvisionTool = (sendInitialPayment, onProvisioned) => { console.info(created ? 'provisioned' : 're-provisioned', address); }, }); - return makeHandler; + return makeBridgeHandler; }; /** * @param {import('@agoric/vat-data').Baggage} baggage * @param {{ - * makeRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit, - * params: *, - * poolBank: import('@endo/far').ERef, - * zcf: ZCF}} powers + * makeRecorderKit: import('@agoric/zoe/src/contractSupport/recorder.js').MakeRecorderKit, + * params: *, + * poolBank: import('@endo/far').ERef, + * zcf: ZCF, + * }} powers */ export const prepareProvisionPoolKit = ( baggage, @@ -99,13 +107,24 @@ export const prepareProvisionPoolKit = ( 'ProvisionPoolKit', { machine: M.interface('ProvisionPoolKit machine', { - makeHandler: M.call({ - bankManager: M.any(), - namesByAddressAdmin: M.any(), - walletFactory: M.any(), - }).returns(M.remotable('BridgeHandler')), + addRevivableAddresses: M.call(M.arrayOf(M.string())).returns(), + getWalletReviver: M.call().returns( + M.remotable('ProvisionPoolKit wallet reviver'), + ), + setReferences: M.callWhen({ + bankManager: M.eref(M.remotable('bankManager')), + namesByAddressAdmin: M.eref(M.remotable('nameAdmin')), + walletFactory: M.eref(M.remotable('walletFactory')), + }).returns(), + makeHandler: M.call().returns(M.remotable('BridgeHandler')), initPSM: M.call(BrandShape, InstanceHandleShape).returns(), }), + walletReviver: M.interface('ProvisionPoolKit wallet reviver', { + reviveWallet: M.callWhen(M.string()).returns( + M.remotable('SmartWallet'), + ), + ackWallet: M.call(M.string()).returns(M.boolean()), + }), helper: UnguardedHelperI, public: M.interface('ProvisionPoolKit public', { getPublicTopics: M.call().returns({ metrics: PublicTopicShape }), @@ -123,6 +142,21 @@ export const prepareProvisionPoolKit = ( /** @type {MapStore} */ const brandToPSM = makeScalarBigMapStore('brandToPSM', { durable: true }); + const revivableAddresses = makeScalarBigSetStore('revivableAddresses', { + durable: true, + keyShape: M.string(), + }); + + /** + * to be set by `setReferences` + * + * @type {Partial} + */ + const references = { + bankManager: undefined, + namesByAddressAdmin: undefined, + walletFactory: undefined, + }; return { brandToPSM, @@ -132,31 +166,55 @@ export const prepareProvisionPoolKit = ( walletsProvisioned: 0n, totalMintedProvided: AmountMath.makeEmpty(poolBrand), totalMintedConverted: AmountMath.makeEmpty(poolBrand), + revivableAddresses, + ...references, }; }, { // aka "limitedCreatorFacet" machine: { /** - * @param {{ - * bankManager: *, - * namesByAddressAdmin: *, - * walletFactory: *, - * }} opts + * @param {string[]} oldAddresses */ - makeHandler(opts) { - const { - facets: { helper }, - } = this; + addRevivableAddresses(oldAddresses) { + console.log('revivableAddresses count', oldAddresses.length); + this.state.revivableAddresses.addAll(oldAddresses); + }, + getWalletReviver() { + return this.facets.walletReviver; + }, + /** + * @param {ProvisionPoolKitReferences} erefs + */ + async setReferences(erefs) { + const { bankManager, namesByAddressAdmin, walletFactory } = erefs; + const obj = harden({ + bankManager, + namesByAddressAdmin, + walletFactory, + }); + const refs = await deeplyFulfilled(obj); + Object.assign(this.state, refs); + }, + makeHandler() { + const { bankManager, namesByAddressAdmin, walletFactory } = + this.state; + if (!bankManager || !namesByAddressAdmin || !walletFactory) { + throw Fail`must set references before handling requests`; + } + const { helper } = this.facets; // a bit obtuse but leave for backwards compatibility with tests - const innerMake = makeBridgeProvisionTool( + const innerMaker = makeBridgeProvisionTool( bank => helper.sendInitialPayment(bank), () => helper.onProvisioned(), ); - return innerMake(opts); + return innerMaker({ + bankManager, + namesByAddressAdmin, + walletFactory, + }); }, /** - * * @param {Brand} brand * @param {PsmInstance} instance */ @@ -165,6 +223,41 @@ export const prepareProvisionPoolKit = ( brandToPSM.init(brand, instance); }, }, + walletReviver: { + /** @param {string} address */ + async reviveWallet(address) { + const { + revivableAddresses, + bankManager, + namesByAddressAdmin, + walletFactory, + } = this.state; + if (!bankManager || !namesByAddressAdmin || !walletFactory) { + throw Fail`must set references before handling requests`; + } + revivableAddresses.has(address) || + Fail`non-revivable address ${address}`; + const bank = E(bankManager).getBankForAddress(address); + const [wallet, _created] = await E(walletFactory).provideSmartWallet( + address, + bank, + namesByAddressAdmin, + ); + return wallet; + }, + /** + * @param {string} address + * @returns {boolean} isRevive + */ + ackWallet(address) { + const { revivableAddresses } = this.state; + if (!revivableAddresses.has(address)) { + return false; + } + revivableAddresses.delete(address); + return true; + }, + }, helper: { publishMetrics() { const { diff --git a/packages/vats/test/bootstrapTests/supports.js b/packages/vats/test/bootstrapTests/supports.js index 4b48d014b67..911b7cb836f 100644 --- a/packages/vats/test/bootstrapTests/supports.js +++ b/packages/vats/test/bootstrapTests/supports.js @@ -14,6 +14,7 @@ import { E } from '@endo/eventual-send'; import { makeQueue } from '@endo/stream'; import { promises as fs } from 'fs'; import { resolve as importMetaResolve } from 'import-meta-resolve'; +import { unmarshalFromVstorage } from '@agoric/internal/src/lib-chainStorage.js'; import { boardSlottingMarshaller } from '../../tools/board-utils.js'; // to retain for ESlint, used by typedef @@ -193,8 +194,11 @@ export const makeWalletFactoryDriver = async ( /** * @param {string} walletAddress * @param {import('@agoric/smart-wallet/src/smartWallet.js').SmartWallet} walletPresence + * @param {boolean} isNew */ - const makeWalletDriver = (walletAddress, walletPresence) => ({ + const makeWalletDriver = (walletAddress, walletPresence, isNew) => ({ + isNew, + /** * @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer * @returns {Promise} @@ -257,9 +261,13 @@ export const makeWalletFactoryDriver = async ( * @returns {import('@agoric/smart-wallet/src/smartWallet.js').UpdateRecord} */ getLatestUpdateRecord() { - const key = `published.wallet.${walletAddress}`; - const lastWalletStatus = JSON.parse(storage.data.get(key)?.at(-1)); - return marshaller.fromCapData(lastWalletStatus); + const fromCapData = (...args) => + Reflect.apply(marshaller.fromCapData, marshaller, args); + return unmarshalFromVstorage( + storage.data, + `published.wallet.${walletAddress}`, + fromCapData, + ); }, }); @@ -271,8 +279,8 @@ export const makeWalletFactoryDriver = async ( const bank = await EV(bankManager).getBankForAddress(walletAddress); return EV(walletFactoryStartResult.creatorFacet) .provideSmartWallet(walletAddress, bank, namesByAddressAdmin) - .then(([walletPresence, _isNew]) => - makeWalletDriver(walletAddress, walletPresence), + .then(([walletPresence, isNew]) => + makeWalletDriver(walletAddress, walletPresence, isNew), ); }, }; @@ -328,28 +336,23 @@ export const getNodeTestVaultsConfig = async ( * * @param {import('ava').ExecutionContext} t * @param {string} bundleDir directory to write bundles and config to - * @param {string} [specifier] bootstrap config specifier + * @param {object} [options] + * @param {string} [options.configSpecifier] bootstrap config specifier + * @param {import('@agoric/internal/src/storage-test-utils.js').FakeStorageKit} [options.storage] */ export const makeSwingsetTestKit = async ( t, bundleDir = 'bundles', - specifier, + { configSpecifier, storage = makeFakeStorageKit('bootstrapTests') } = {}, ) => { console.time('makeSwingsetTestKit'); - const configPath = await getNodeTestVaultsConfig(bundleDir, specifier); + const configPath = await getNodeTestVaultsConfig(bundleDir, configSpecifier); const swingStore = initSwingStore(); const { kernelStorage, hostStorage } = swingStore; + const { fromCapData } = boardSlottingMarshaller(slotToRemotable); - const storage = makeFakeStorageKit('bootstrapTests'); - - const marshal = boardSlottingMarshaller(slotToRemotable); - - const readLatest = path => { - const str = storage.data.get(path)?.at(-1); - str || Fail`no data at path ${path}`; - const capData = JSON.parse(storage.data.get(path)?.at(-1)); - return marshal.fromCapData(capData); - }; + const readLatest = path => + unmarshalFromVstorage(storage.data, path, fromCapData); let lastNonce = 0n; @@ -412,8 +415,7 @@ export const makeSwingsetTestKit = async ( console.warn('Bridge returning undefined for', bridgeId, ':', obj); return undefined; case BridgeId.STORAGE: - void storage.toStorage(obj); - return undefined; + return storage.toStorage(obj); default: throw Error(`unknown bridgeId ${bridgeId}`); } diff --git a/packages/vats/test/bootstrapTests/test-demo-config.js b/packages/vats/test/bootstrapTests/test-demo-config.js index 04dfaf55c41..d9a53b5fd47 100644 --- a/packages/vats/test/bootstrapTests/test-demo-config.js +++ b/packages/vats/test/bootstrapTests/test-demo-config.js @@ -12,11 +12,9 @@ const { keys } = Object; const test = anyTest; const makeDefaultTestContext = async t => { - const swingsetTestKit = await makeSwingsetTestKit( - t, - 'bundles/demo-config', - '@agoric/vats/decentral-demo-config.json', - ); + const swingsetTestKit = await makeSwingsetTestKit(t, 'bundles/demo-config', { + configSpecifier: '@agoric/vats/decentral-demo-config.json', + }); return swingsetTestKit; }; diff --git a/packages/vats/test/bootstrapTests/test-vaults-integration.js b/packages/vats/test/bootstrapTests/test-vaults-integration.js index ad309fab75d..5b70c4b1cbd 100644 --- a/packages/vats/test/bootstrapTests/test-vaults-integration.js +++ b/packages/vats/test/bootstrapTests/test-vaults-integration.js @@ -6,6 +6,7 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { Fail } from '@agoric/assert'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; +import { unmarshalFromVstorage } from '@agoric/internal/src/lib-chainStorage.js'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { makeMarshal } from '@endo/marshal'; import { @@ -352,10 +353,9 @@ test('propose change to auction governance param', async t => { await eventLoopIteration(); t.like(wd.getLatestUpdateRecord(), { status: { numWantsSatisfied: 1 } }); - const key = `published.committees.Economic_Committee.latestQuestion`; - const capData = JSON.parse(storage.data.get(key)?.at(-1)); const { fromCapData } = makeMarshal(undefined, slotToBoardRemote); - const lastQuestion = fromCapData(capData); + const key = `published.committees.Economic_Committee.latestQuestion`; + const lastQuestion = unmarshalFromVstorage(storage.data, key, fromCapData); const changes = lastQuestion?.issue?.spec?.changes; t.log('check Economic_Committee.latestQuestion against proposal'); t.like(changes, { StartFrequency: { relValue: 300n } }); diff --git a/packages/vats/test/bootstrapTests/test-vaults-upgrade.js b/packages/vats/test/bootstrapTests/test-vaults-upgrade.js index fe30f53bcc7..27bb2a85e72 100644 --- a/packages/vats/test/bootstrapTests/test-vaults-upgrade.js +++ b/packages/vats/test/bootstrapTests/test-vaults-upgrade.js @@ -1,6 +1,9 @@ // @ts-check /** - * @file Bootstrap test integration vaults with smart-wallet + * @file Bootstrap test integration vaults with smart-wallet. + * The tests in this file are NOT independent; a single `test.before()` + * handler creates shared state with `makeSwingsetTestKit` and each + * test is run serially and assumes changes from earlier tests. */ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; @@ -10,43 +13,48 @@ import { Far, makeMarshal } from '@endo/marshal'; import { makeAgoricNamesRemotesFromFakeStorage } from '../../tools/board-utils.js'; import { makeSwingsetTestKit, makeWalletFactoryDriver } from './supports.js'; -/** - * @type {import('ava').TestFn>>} - */ -const test = anyTest; - // presently all these tests use one collateral manager const collateralBrandKey = 'ATOM'; -const makeDefaultTestContext = async t => { - console.time('DefaultTestContext'); - const swingsetTestKit = await makeSwingsetTestKit(t, 'bundles/vaults'); +/** + * @param {import('ava').ExecutionContext} t + * @param {object} [options] + * @param {number} [options.incarnation=1] + * @param {boolean} [options.logTiming=true] + * @param {import('@agoric/internal/src/storage-test-utils.js').FakeStorageKit} [options.storage] + */ +const makeDefaultTestContext = async ( + t, + { incarnation = 1, logTiming = true, storage = undefined } = {}, +) => { + logTiming && console.time('DefaultTestContext'); + const swingsetTestKit = await makeSwingsetTestKit(t, 'bundles/vaults', { + storage, + }); - const { runUtils, storage } = swingsetTestKit; - console.timeLog('DefaultTestContext', 'swingsetTestKit'); + const { readLatest, runUtils } = swingsetTestKit; + ({ storage } = swingsetTestKit); const { EV } = runUtils; + logTiming && console.timeLog('DefaultTestContext', 'swingsetTestKit'); // Wait for ATOM to make it into agoricNames await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); - console.timeLog('DefaultTestContext', 'vaultFactoryKit'); + logTiming && console.timeLog('DefaultTestContext', 'vaultFactoryKit'); // has to be late enough for agoricNames data to have been published - const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage( - swingsetTestKit.storage, - ); + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage(storage); agoricNamesRemotes.brand.ATOM || Fail`ATOM missing from agoricNames`; - console.timeLog('DefaultTestContext', 'agoricNamesRemotes'); + logTiming && console.timeLog('DefaultTestContext', 'agoricNamesRemotes'); const walletFactoryDriver = await makeWalletFactoryDriver( runUtils, storage, agoricNamesRemotes, ); - console.timeLog('DefaultTestContext', 'walletFactoryDriver'); + logTiming && console.timeLog('DefaultTestContext', 'walletFactoryDriver'); - console.timeEnd('DefaultTestContext'); + logTiming && console.timeEnd('DefaultTestContext'); - const { readLatest } = swingsetTestKit; const readRewardPoolBalance = () => { return readLatest('published.vaultFactory.metrics').rewardPoolAllocation .Minted?.value; @@ -58,6 +66,7 @@ const makeDefaultTestContext = async t => { return { ...swingsetTestKit, + incarnation, agoricNamesRemotes, readCollateralMetrics, readRewardPoolBalance, @@ -65,10 +74,50 @@ const makeDefaultTestContext = async t => { }; }; +/** + * Shared context can be updated by re-bootstrapping, and is placed one + * property deep so such changes propagate to later tests. + * + * @type {import('ava').TestFn<{shared: Awaited>}>} + */ +const test = anyTest; test.before(async t => { - t.context = await makeDefaultTestContext(t); + const shared = await makeDefaultTestContext(t); + t.context = { shared }; +}); +test.after.always(t => t.context.shared.shutdown()); + +test.serial('re-bootstrap', async t => { + const oldContext = { ...t.context.shared }; + const { storage } = oldContext; + t.is(oldContext.incarnation, 1); + const wd1 = await oldContext.walletFactoryDriver.provideSmartWallet( + 'agoric1a', + ); + t.true(wd1.isNew); + await oldContext.shutdown(); + const newContext = await makeDefaultTestContext(t, { + incarnation: oldContext.incarnation + 1, + logTiming: false, + storage, + }); + Object.assign(t.context.shared, newContext); + + t.is(newContext.incarnation, 2); + await t.throwsAsync( + oldContext.walletFactoryDriver.provideSmartWallet('agoric1a'), + undefined, + 'must not be able to use old swingset', + ); + const wd2 = await newContext.walletFactoryDriver.provideSmartWallet( + 'agoric1a', + ); + t.false(wd2.isNew); + const wd3 = await newContext.walletFactoryDriver.provideSmartWallet( + 'agoric1b', + ); + t.true(wd3.isNew); }); -test.after.always(t => t.context.shutdown()); test.serial('audit bootstrap exports', async t => { const expected = { @@ -85,7 +134,7 @@ test.serial('audit bootstrap exports', async t => { }, }; - const { controller } = t.context; + const { controller } = t.context.shared; const kState = controller.dump(); const myVatID = 'v1'; @@ -162,11 +211,17 @@ test.serial('audit bootstrap exports', async t => { test.serial('open vault', async t => { console.time('open vault'); - t.falsy(t.context.readRewardPoolBalance()); - - const { walletFactoryDriver } = t.context; + const { + incarnation, + readRewardPoolBalance, + readCollateralMetrics, + walletFactoryDriver, + } = t.context.shared; + t.is(incarnation, 2); + t.falsy(readRewardPoolBalance()); const wd = await walletFactoryDriver.provideSmartWallet('agoric1a'); + t.false(wd.isNew); await wd.executeOfferMaker(Offers.vaults.OpenVault, { offerId: 'open1', @@ -181,8 +236,8 @@ test.serial('open vault', async t => { status: { id: 'open1', numWantsSatisfied: 1 }, }); - t.is(t.context.readRewardPoolBalance(), 25000n); - t.like(t.context.readCollateralMetrics(0), { + t.is(readRewardPoolBalance(), 25000n); + t.like(readCollateralMetrics(0), { numActiveVaults: 1, totalCollateral: { value: 9000000n }, totalDebt: { value: 5025000n }, @@ -191,7 +246,8 @@ test.serial('open vault', async t => { }); test.serial('restart vaultFactory', async t => { - const { EV } = t.context.runUtils; + const { runUtils, readCollateralMetrics } = t.context.shared; + const { EV } = runUtils; /** @type {Awaited} */ const vaultFactoryKit = await EV.vat('bootstrap').consumeItem( 'vaultFactoryKit', @@ -210,15 +266,15 @@ test.serial('restart vaultFactory', async t => { totalCollateral: { value: 9000000n }, totalDebt: { value: 5025000n }, }; - t.like(t.context.readCollateralMetrics(0), keyMetrics); - t.log('awaiting VF restartContract'); + t.like(readCollateralMetrics(0), keyMetrics); + t.log('awaiting VaultFactory restartContract'); const upgradeResult = await EV(vfAdminFacet).restartContract(privateArgs); t.deepEqual(upgradeResult, { incarnationNumber: 1 }); - t.like(t.context.readCollateralMetrics(0), keyMetrics); // unchanged + t.like(readCollateralMetrics(0), keyMetrics); // unchanged }); test.serial('restart contractGovernor', async t => { - const { EV } = t.context.runUtils; + const { EV } = t.context.shared.runUtils; /** @type {Awaited} */ const vaultFactoryKit = await EV.vat('bootstrap').consumeItem( 'vaultFactoryKit', @@ -238,9 +294,8 @@ test.serial('restart contractGovernor', async t => { }); test.serial('open vault 2', async t => { - t.is(t.context.readRewardPoolBalance(), 25000n); - - const { walletFactoryDriver } = t.context; + const { readRewardPoolBalance, walletFactoryDriver } = t.context.shared; + t.is(readRewardPoolBalance(), 25000n); const wd = await walletFactoryDriver.provideSmartWallet('agoric1a'); @@ -260,12 +315,13 @@ test.serial('open vault 2', async t => { }); // balance goes up as before restart (doubles because same wantMinted) - t.is(t.context.readRewardPoolBalance(), 50000n); + t.is(readRewardPoolBalance(), 50000n); }); test.serial('adjust balance of vault opened before restart', async t => { - const { walletFactoryDriver } = t.context; - t.is(t.context.readRewardPoolBalance(), 50000n); + const { readCollateralMetrics, readRewardPoolBalance, walletFactoryDriver } = + t.context.shared; + t.is(readRewardPoolBalance(), 50000n); const wd = await walletFactoryDriver.provideSmartWallet('agoric1a'); @@ -294,7 +350,7 @@ test.serial('adjust balance of vault opened before restart', async t => { }, }); // sanity check - t.like(t.context.readCollateralMetrics(0), { + t.like(readCollateralMetrics(0), { numActiveVaults: 2, numLiquidatingVaults: 0, }); @@ -302,12 +358,13 @@ test.serial('adjust balance of vault opened before restart', async t => { // charge interest to force a liquidation and verify the shortfall is transferred test.serial('force liquidation', async t => { - const { advanceTime } = t.context; + const { advanceTime, readCollateralMetrics, readRewardPoolBalance } = + t.context.shared; // advance a year to drive interest charges advanceTime(365, 'days'); - t.is(t.context.readRewardPoolBalance(), 340000n); - t.like(t.context.readCollateralMetrics(0), { + t.is(readRewardPoolBalance(), 340000n); + t.like(readCollateralMetrics(0), { totalDebt: { value: 68340000n }, }); @@ -316,7 +373,7 @@ test.serial('force liquidation', async t => { await advanceTime(1, 'hours'); await advanceTime(1, 'hours'); // wait for it... - t.like(t.context.readCollateralMetrics(0), { + t.like(readCollateralMetrics(0), { liquidatingCollateral: { value: 0n }, liquidatingDebt: { value: 0n }, numLiquidatingVaults: 0, @@ -324,7 +381,7 @@ test.serial('force liquidation', async t => { // POW await advanceTime(1, 'hours'); - t.like(t.context.readCollateralMetrics(0), { + t.like(readCollateralMetrics(0), { liquidatingCollateral: { value: 9000000n }, liquidatingDebt: { value: 696421994n }, numLiquidatingVaults: 1, @@ -338,7 +395,7 @@ test.serial( controller, runUtils: { EV }, swingStore, - } = t.context; + } = t.context.shared; const kState = controller.dump(); const { kernelTable, vatTables } = kState; diff --git a/packages/vats/test/test-board-utils.js b/packages/vats/test/test-board-utils.js index ad9ec91e206..53fd73794d9 100644 --- a/packages/vats/test/test-board-utils.js +++ b/packages/vats/test/test-board-utils.js @@ -11,11 +11,14 @@ import { makeHistoryReviver, } from '../tools/board-utils.js'; +const streamCellTextFromArray = values => + JSON.stringify({ blockHeight: '0', values }); + /** @type {Array<[string, any]>} */ const agoricNamesDataEntriesFixture = [ [ 'published.agoricNames.issuer', - [ + streamCellTextFromArray([ '{"body":"[]","slots":[]}', '{"body":"[[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":0}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation issuer\\",\\"index\\":1}]]","slots":["board0371","board0592"]}', '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":0}],[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":1}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation issuer\\",\\"index\\":2}]]","slots":["board0425","board0371","board0592"]}', @@ -25,11 +28,11 @@ const agoricNamesDataEntriesFixture = [ '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":0}],[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":1}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation issuer\\",\\"index\\":2}],[\\"USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl issuer\\",\\"index\\":3}],[\\"USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv issuer\\",\\"index\\":4}],[\\"USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl issuer\\",\\"index\\":5}],[\\"USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv issuer\\",\\"index\\":6}]]","slots":["board0425","board0371","board0592","board02314","board00917","board06120","board05024"]}', '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":0}],[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":1}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation issuer\\",\\"index\\":2}],[\\"AUSD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: AUSD issuer\\",\\"index\\":3}],[\\"USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl issuer\\",\\"index\\":4}],[\\"USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv issuer\\",\\"index\\":5}],[\\"USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl issuer\\",\\"index\\":6}],[\\"USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv issuer\\",\\"index\\":7}]]","slots":["board0425","board0371","board0592","board04827","board02314","board00917","board06120","board05024"]}', '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":0}],[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":1}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation issuer\\",\\"index\\":2}],[\\"AUSD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: AUSD issuer\\",\\"index\\":3}],[\\"ATOM\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: ATOM issuer\\",\\"index\\":4}],[\\"USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl issuer\\",\\"index\\":5}],[\\"USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv issuer\\",\\"index\\":6}],[\\"USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl issuer\\",\\"index\\":7}],[\\"USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv issuer\\",\\"index\\":8}]]","slots":["board0425","board0371","board0592","board04827","board02575","board02314","board00917","board06120","board05024"]}', - ], + ]), ], [ 'published.agoricNames.brand', - [ + streamCellTextFromArray([ '{"body":"[]","slots":[]}', '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation brand\\",\\"index\\":1}]]","slots":["board0223","board0074"]}', '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0}],[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST brand\\",\\"index\\":1}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation brand\\",\\"index\\":2}]]","slots":["board0223","board0566","board0074"]}', @@ -39,11 +42,11 @@ const agoricNamesDataEntriesFixture = [ '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0}],[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST brand\\",\\"index\\":1}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation brand\\",\\"index\\":2}],[\\"USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl brand\\",\\"index\\":3}],[\\"USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv brand\\",\\"index\\":4}],[\\"USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl brand\\",\\"index\\":5}],[\\"USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv brand\\",\\"index\\":6}]]","slots":["board0223","board0566","board0074","board05815","board00218","board02021","board03125"]}', '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0}],[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST brand\\",\\"index\\":1}],[\\"AUSD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: AUSD brand\\",\\"index\\":2}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation brand\\",\\"index\\":3}],[\\"USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl brand\\",\\"index\\":4}],[\\"USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv brand\\",\\"index\\":5}],[\\"USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl brand\\",\\"index\\":6}],[\\"USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv brand\\",\\"index\\":7}]]","slots":["board0223","board0566","board03928","board0074","board05815","board00218","board02021","board03125"]}', '{"body":"[[\\"BLD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0}],[\\"IST\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST brand\\",\\"index\\":1}],[\\"AUSD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: AUSD brand\\",\\"index\\":2}],[\\"Invitation\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: Zoe Invitation brand\\",\\"index\\":3}],[\\"ATOM\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: ATOM brand\\",\\"index\\":4}],[\\"USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl brand\\",\\"index\\":5}],[\\"USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv brand\\",\\"index\\":6}],[\\"USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl brand\\",\\"index\\":7}],[\\"USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv brand\\",\\"index\\":8}]]","slots":["board0223","board0566","board03928","board0074","board02963","board05815","board00218","board02021","board03125"]}', - ], + ]), ], [ 'published.agoricNames.vbankAsset', - [ + streamCellTextFromArray([ '{"body":"[]","slots":[]}', '{"body":"[[\\"ubld\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0},\\"denom\\":\\"ubld\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":1},\\"issuerName\\":\\"BLD\\",\\"proposedName\\":\\"Agoric staking token\\"}]]","slots":["board0223","board0425"]}', '{"body":"[[\\"ubld\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0},\\"denom\\":\\"ubld\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":1},\\"issuerName\\":\\"BLD\\",\\"proposedName\\":\\"Agoric staking token\\"}],[\\"uist\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST brand\\",\\"index\\":2},\\"denom\\":\\"uist\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":3},\\"issuerName\\":\\"IST\\",\\"proposedName\\":\\"Agoric stable token\\"}]]","slots":["board0223","board0425","board0566","board0371"]}', @@ -53,11 +56,11 @@ const agoricNamesDataEntriesFixture = [ '{"body":"[[\\"ubld\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0},\\"denom\\":\\"ubld\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":1},\\"issuerName\\":\\"BLD\\",\\"proposedName\\":\\"Agoric staking token\\"}],[\\"uist\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST brand\\",\\"index\\":2},\\"denom\\":\\"uist\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":3},\\"issuerName\\":\\"IST\\",\\"proposedName\\":\\"Agoric stable token\\"}],[\\"ibc/toyusdc\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl brand\\",\\"index\\":4},\\"denom\\":\\"ibc/toyusdc\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl issuer\\",\\"index\\":5},\\"issuerName\\":\\"USDC_axl\\",\\"proposedName\\":\\"USD Coin\\"}],[\\"ibc/usdc5678\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv brand\\",\\"index\\":6},\\"denom\\":\\"ibc/usdc5678\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv issuer\\",\\"index\\":7},\\"issuerName\\":\\"USDC_grv\\",\\"proposedName\\":\\"USC Coin\\"}],[\\"ibc/usdt1234\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl brand\\",\\"index\\":8},\\"denom\\":\\"ibc/usdt1234\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl issuer\\",\\"index\\":9},\\"issuerName\\":\\"USDT_axl\\",\\"proposedName\\":\\"Tether USD\\"}],[\\"ibc/toyollie\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv brand\\",\\"index\\":10},\\"denom\\":\\"ibc/toyollie\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv issuer\\",\\"index\\":11},\\"issuerName\\":\\"USDT_grv\\",\\"proposedName\\":\\"Tether USD\\"}]]","slots":["board0223","board0425","board0566","board0371","board05815","board02314","board00218","board00917","board02021","board06120","board03125","board05024"]}', '{"body":"[[\\"ubld\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0},\\"denom\\":\\"ubld\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":1},\\"issuerName\\":\\"BLD\\",\\"proposedName\\":\\"Agoric staking token\\"}],[\\"uist\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST brand\\",\\"index\\":2},\\"denom\\":\\"uist\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":3},\\"issuerName\\":\\"IST\\",\\"proposedName\\":\\"Agoric stable token\\"}],[\\"ibc/toyusdc\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl brand\\",\\"index\\":4},\\"denom\\":\\"ibc/toyusdc\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl issuer\\",\\"index\\":5},\\"issuerName\\":\\"USDC_axl\\",\\"proposedName\\":\\"USD Coin\\"}],[\\"ibc/usdc5678\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv brand\\",\\"index\\":6},\\"denom\\":\\"ibc/usdc5678\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv issuer\\",\\"index\\":7},\\"issuerName\\":\\"USDC_grv\\",\\"proposedName\\":\\"USC Coin\\"}],[\\"ibc/usdt1234\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl brand\\",\\"index\\":8},\\"denom\\":\\"ibc/usdt1234\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl issuer\\",\\"index\\":9},\\"issuerName\\":\\"USDT_axl\\",\\"proposedName\\":\\"Tether USD\\"}],[\\"ibc/toyollie\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv brand\\",\\"index\\":10},\\"denom\\":\\"ibc/toyollie\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv issuer\\",\\"index\\":11},\\"issuerName\\":\\"USDT_grv\\",\\"proposedName\\":\\"Tether USD\\"}],[\\"ibc/toyellie\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: AUSD brand\\",\\"index\\":12},\\"denom\\":\\"ibc/toyellie\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: AUSD issuer\\",\\"index\\":13},\\"issuerName\\":\\"AUSD\\",\\"proposedName\\":\\"Anchor USD\\"}]]","slots":["board0223","board0425","board0566","board0371","board05815","board02314","board00218","board00917","board02021","board06120","board03125","board05024","board03928","board04827"]}', '{"body":"[[\\"ubld\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD brand\\",\\"index\\":0},\\"denom\\":\\"ubld\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BLD issuer\\",\\"index\\":1},\\"issuerName\\":\\"BLD\\",\\"proposedName\\":\\"Agoric staking token\\"}],[\\"uist\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST brand\\",\\"index\\":2},\\"denom\\":\\"uist\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: IST issuer\\",\\"index\\":3},\\"issuerName\\":\\"IST\\",\\"proposedName\\":\\"Agoric stable token\\"}],[\\"ibc/toyusdc\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl brand\\",\\"index\\":4},\\"denom\\":\\"ibc/toyusdc\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_axl issuer\\",\\"index\\":5},\\"issuerName\\":\\"USDC_axl\\",\\"proposedName\\":\\"USD Coin\\"}],[\\"ibc/usdc5678\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv brand\\",\\"index\\":6},\\"denom\\":\\"ibc/usdc5678\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDC_grv issuer\\",\\"index\\":7},\\"issuerName\\":\\"USDC_grv\\",\\"proposedName\\":\\"USC Coin\\"}],[\\"ibc/usdt1234\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl brand\\",\\"index\\":8},\\"denom\\":\\"ibc/usdt1234\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_axl issuer\\",\\"index\\":9},\\"issuerName\\":\\"USDT_axl\\",\\"proposedName\\":\\"Tether USD\\"}],[\\"ibc/toyollie\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv brand\\",\\"index\\":10},\\"denom\\":\\"ibc/toyollie\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USDT_grv issuer\\",\\"index\\":11},\\"issuerName\\":\\"USDT_grv\\",\\"proposedName\\":\\"Tether USD\\"}],[\\"ibc/toyellie\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: AUSD brand\\",\\"index\\":12},\\"denom\\":\\"ibc/toyellie\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: AUSD issuer\\",\\"index\\":13},\\"issuerName\\":\\"AUSD\\",\\"proposedName\\":\\"Anchor USD\\"}],[\\"ibc/toyatom\\",{\\"brand\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: ATOM brand\\",\\"index\\":14},\\"denom\\":\\"ibc/toyatom\\",\\"displayInfo\\":{\\"assetKind\\":\\"nat\\",\\"decimalPlaces\\":6},\\"issuer\\":{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: ATOM issuer\\",\\"index\\":15},\\"issuerName\\":\\"ATOM\\",\\"proposedName\\":\\"ATOM\\"}]]","slots":["board0223","board0425","board0566","board0371","board05815","board02314","board00218","board00917","board02021","board06120","board03125","board05024","board03928","board04827","board02963","board02575"]}', - ], + ]), ], [ 'published.agoricNames.installation', - [ + streamCellTextFromArray([ '{"body":"[]","slots":[]}', '{"body":"[[\\"centralSupply\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":0}],[\\"mintHolder\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":1}],[\\"provisionPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":2}]]","slots":["board0257","board0188","board0639"]}', '{"body":"[[\\"centralSupply\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":0}],[\\"mintHolder\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":1}],[\\"walletFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":2}],[\\"provisionPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":3}]]","slots":["board0257","board0188","board04312","board0639"]}', @@ -103,11 +106,11 @@ const agoricNamesDataEntriesFixture = [ '{"body":"[[\\"centralSupply\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":0}],[\\"mintHolder\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":1}],[\\"walletFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":2}],[\\"provisionPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":3}],[\\"contractGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":4}],[\\"committee\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":5}],[\\"binaryVoteCounter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":6}],[\\"amm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":7}],[\\"VaultFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":8}],[\\"feeDistributor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":9}],[\\"liquidate\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":10}],[\\"stakeFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":11}],[\\"reserve\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":12}],[\\"psm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":13}],[\\"econCommitteeCharter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":14}],[\\"interchainPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":15}],[\\"priceAggregator\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":16}],[\\"scaledPriceAuthority\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":17}]]","slots":["board0257","board05674","board04312","board0639","board04154","board02656","board06458","board01029","board00530","board04431","board02733","board02437","board03935","board01272","board01151","board05736","board00776","board01759"]}', '{"body":"[[\\"centralSupply\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":0}],[\\"mintHolder\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":1}],[\\"walletFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":2}],[\\"provisionPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":3}],[\\"contractGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":4}],[\\"committee\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":5}],[\\"binaryVoteCounter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":6}],[\\"amm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":7}],[\\"VaultFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":8}],[\\"feeDistributor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":9}],[\\"liquidate\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":10}],[\\"stakeFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":11}],[\\"reserve\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":12}],[\\"psm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":13}],[\\"econCommitteeCharter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":14}],[\\"interchainPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":15}],[\\"priceAggregator\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":16}],[\\"scaledPriceAuthority\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":17}]]","slots":["board0257","board05674","board04312","board0639","board04154","board02656","board06458","board01029","board00530","board04431","board02733","board02437","board03935","board01272","board01151","board05736","board00776","board01759"]}', '{"body":"[[\\"centralSupply\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":0}],[\\"mintHolder\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":1}],[\\"walletFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":2}],[\\"provisionPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":3}],[\\"contractGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":4}],[\\"committee\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":5}],[\\"binaryVoteCounter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":6}],[\\"amm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":7}],[\\"VaultFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":8}],[\\"feeDistributor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":9}],[\\"liquidate\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":10}],[\\"stakeFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":11}],[\\"reserve\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":12}],[\\"psm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":13}],[\\"econCommitteeCharter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":14}],[\\"interchainPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":15}],[\\"priceAggregator\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":16}],[\\"scaledPriceAuthority\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: BundleIDInstallation\\",\\"index\\":17}]]","slots":["board0257","board05674","board04312","board0639","board04154","board02656","board06458","board01029","board00530","board04431","board02733","board02437","board03935","board01272","board04277","board05736","board00776","board01759"]}', - ], + ]), ], [ 'published.agoricNames.instance', - [ + streamCellTextFromArray([ '{"body":"[]","slots":[]}', '{"body":"[[\\"econCommitteeCharter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":0}]]","slots":["board00613"]}', '{"body":"[[\\"economicCommittee\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":0}],[\\"econCommitteeCharter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":1}]]","slots":["board04016","board00613"]}', @@ -127,16 +130,19 @@ const agoricNamesDataEntriesFixture = [ '{"body":"[[\\"economicCommittee\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":0}],[\\"amm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":1}],[\\"ammGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":2}],[\\"VaultFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":3}],[\\"reserve\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":4}],[\\"reserveGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":5}],[\\"econCommitteeCharter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":6}],[\\"interchainPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":7}],[\\"provisionPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":8}],[\\"walletFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":9}],[\\"ATOM-USD price feed\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":10}],[\\"psm-IST-USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":11}],[\\"psm-IST-USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":12}],[\\"psm-IST-USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":13}],[\\"psm-IST-USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":14}],[\\"psm-IST-AUSD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":15}]]","slots":["board04016","board00855","board00443","board05970","board02152","board06053","board00613","board05557","board01744","board03523","board06445","board03446","board01547","board00848","board04149","board03850"]}', '{"body":"[[\\"economicCommittee\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":0}],[\\"amm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":1}],[\\"ammGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":2}],[\\"VaultFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":3}],[\\"Treasury\\",{\\"@qclass\\":\\"slot\\",\\"index\\":3}],[\\"reserve\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":4}],[\\"reserveGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":5}],[\\"econCommitteeCharter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":6}],[\\"interchainPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":7}],[\\"provisionPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":8}],[\\"walletFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":9}],[\\"ATOM-USD price feed\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":10}],[\\"psm-IST-USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":11}],[\\"psm-IST-USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":12}],[\\"psm-IST-USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":13}],[\\"psm-IST-USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":14}],[\\"psm-IST-AUSD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":15}]]","slots":["board04016","board00855","board00443","board05970","board02152","board06053","board00613","board05557","board01744","board03523","board06445","board03446","board01547","board00848","board04149","board03850"]}', '{"body":"[[\\"economicCommittee\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":0}],[\\"amm\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":1}],[\\"ammGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":2}],[\\"VaultFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":3}],[\\"Treasury\\",{\\"@qclass\\":\\"slot\\",\\"index\\":3}],[\\"VaultFactoryGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":4}],[\\"reserve\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":5}],[\\"reserveGovernor\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":6}],[\\"econCommitteeCharter\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":7}],[\\"interchainPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":8}],[\\"provisionPool\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":9}],[\\"walletFactory\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":10}],[\\"ATOM-USD price feed\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":11}],[\\"psm-IST-USDC_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":12}],[\\"psm-IST-USDC_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":13}],[\\"psm-IST-USDT_axl\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":14}],[\\"psm-IST-USDT_grv\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":15}],[\\"psm-IST-AUSD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: InstanceHandle\\",\\"index\\":16}]]","slots":["board04016","board00855","board00443","board05970","board03773","board02152","board06053","board00613","board05557","board01744","board03523","board06445","board03446","board01547","board00848","board04149","board03850"]}', - ], + ]), ], [ 'published.agoricNames.oracleBrand', - [ + streamCellTextFromArray([ '{"body":"[]","slots":[]}', '{"body":"[[\\"USD\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: USD brand\\",\\"index\\":0}],[\\"ATOM\\",{\\"@qclass\\":\\"slot\\",\\"iface\\":\\"Alleged: ATOM brand\\",\\"index\\":1}]]","slots":["board02810","board05311"]}', - ], + ]), + ], + [ + 'published.agoricNames.uiConfig', + streamCellTextFromArray(['{"body":"[]","slots":[]}']), ], - ['published.agoricNames.uiConfig', ['{"body":"[]","slots":[]}']], ]; test('makeAgoricNamesRemotesFromFakeStorage', t => { diff --git a/packages/vats/test/test-provisionPool.js b/packages/vats/test/test-provisionPool.js index a58ce0e7d23..50c0e3bcf90 100644 --- a/packages/vats/test/test-provisionPool.js +++ b/packages/vats/test/test-provisionPool.js @@ -25,6 +25,12 @@ import { buildRootObject as buildBankRoot } from '../src/vat-bank.js'; import { PowerFlags } from '../src/walletFlags.js'; import { makeFakeBankKit } from '../tools/bank-utils.js'; +/** + * @typedef {import('../src/vat-bank.js').Bank} Bank + * @typedef {import('@agoric/smart-wallet/src/smartWallet.js').SmartWallet} SmartWallet + * @typedef {import('@agoric/smart-wallet/src/walletFactory.js').WalletReviver} WalletReviver + */ + const pathname = new URL(import.meta.url).pathname; const dirname = path.dirname(pathname); @@ -267,9 +273,9 @@ test('provisionPool trades provided assets for IST', async t => { * out the part that had a bug as `publishDepositFacet` * and we make a mock walletFactory that uses it. * - * @param {string} address + * @param {string[]} addresses */ -const makeWalletFactoryKitFor1 = async address => { +const makeWalletFactoryKitForAddresses = async addresses => { const baggage = makeScalarBigMapStore('bank baggage'); const bankManager = await buildBankRoot( undefined, @@ -286,41 +292,78 @@ const makeWalletFactoryKitFor1 = async address => { return E(E(dest).getPurse(fees.brand)).deposit(pmt); }; - const b1 = bankManager.getBankForAddress(address); - const p1 = E(b1).getPurse(fees.brand); + /** @type {Map>} */ + const banks = new Map( + addresses.map(addr => [addr, bankManager.getBankForAddress(addr)]), + ); + /** @type {Map} */ + // @ts-expect-error + const purses = new Map( + addresses.map(addr => [ + addr, + E(/** @type {Promise} */ (banks.get(addr))).getPurse(fees.brand), + ]), + ); + /** @type {Map} */ + // @ts-expect-error + const wallets = new Map( + addresses.map(addr => { + const purse = /** @type {Purse} */ (purses.get(addr)); + const mockWallet = Far('mock wallet', { + getDepositFacet: () => + Far('mock depositFacet', { + receive: payment => E(purse).deposit(payment), + }), + }); + return [addr, mockWallet]; + }), + ); - /** @type {import('@agoric/smart-wallet/src/smartWallet.js').SmartWallet} */ - // @ts-expect-error mock - const smartWallet = harden({ - getDepositFacet: () => { - pmt => E(p1).deposit(pmt); - }, - }); + /** @type {WalletReviver | undefined} */ + let walletReviver; + /** @param {ERef} walletReviverP */ + const setReviver = async walletReviverP => { + walletReviver = await walletReviverP; + }; const done = new Set(); /** @type {import('@agoric/vats/src/core/startWalletFactory').WalletFactoryStartResult['creatorFacet']} */ - // @ts-expect-error cast mock - const walletFactory = { - provideSmartWallet: async (a, _b, nameAdmin) => { - assert.equal(a, address); - - const created = !done.has(a); - if (created) { - await publishDepositFacet(address, smartWallet, nameAdmin); - done.add(a); + const walletFactory = Far('mock walletFactory', { + provideSmartWallet: async (addr, _b, nameAdmin) => { + const wallet = wallets.get(addr); + assert(wallet); + + let isNew = !done.has(addr); + if (isNew) { + const isRevive = + walletReviver && (await E(walletReviver).ackWallet(addr)); + if (isRevive) { + isNew = false; + } else { + await publishDepositFacet(addr, wallet, nameAdmin); + } + done.add(addr); } - return [smartWallet, created]; + return [wallet, isNew]; }, - }; + }); - return { fees, sendInitialPayment, bankManager, walletFactory, p1 }; + return { + fees, + sendInitialPayment, + bankManager, + walletFactory, + setReviver, + purses, + }; }; test('makeBridgeProvisionTool handles duplicate requests', async t => { const address = 'addr123'; t.log('make a wallet factory just for', address); - const { walletFactory, p1, fees, sendInitialPayment, bankManager } = - await makeWalletFactoryKitFor1(address); + const { walletFactory, purses, fees, sendInitialPayment, bankManager } = + await makeWalletFactoryKitForAddresses([address]); + const purse = /** @type {Purse} */ (purses.get(address)); t.log('use makeBridgeProvisionTool to make a bridge handler'); const { nameHub: namesByAddress, nameAdmin: namesByAddressAdmin } = @@ -344,7 +387,7 @@ test('makeBridgeProvisionTool handles duplicate requests', async t => { }); t.deepEqual( - await E(p1).getCurrentAmount(), + await E(purse).getCurrentAmount(), fees.make(250n), 'received starter funds', ); @@ -365,8 +408,126 @@ test('makeBridgeProvisionTool handles duplicate requests', async t => { 'depositFacet lookup finds the same object', ); t.deepEqual( - await E(p1).getCurrentAmount(), + await E(purse).getCurrentAmount(), fees.make(500n), 'received more starter funds', ); }); + +test('provisionPool revives old wallets', async t => { + const { zoe, installs, storageRoot, committeeCreator } = t.context; + + // make a mock wallet factory and setup its bank + const oldAddr = 'addr_old'; + const newAddr = 'addr_new'; + const { walletFactory, setReviver, purses, fees, bankManager } = + await makeWalletFactoryKitForAddresses([oldAddr, newAddr]); + const oldPurse = /** @type {Purse} */ (purses.get(oldAddr)); + const newPurse = /** @type {Purse} */ (purses.get(newAddr)); + const poolBank = await E(bankManager).getBankForAddress('pool'); + const poolPurse = E(poolBank).getPurse(fees.brand); + await E(poolPurse).deposit( + await E(fees.mint).mintPayment(fees.make(scale6(100))), + ); + + // start a provisionPool contract with mock governance terms + const starterAmount = fees.make(250n); + const initialPoserInvitation = await E(committeeCreator).getPoserInvitation(); + const invitationAmount = await E(E(zoe).getInvitationIssuer()).getAmountOf( + initialPoserInvitation, + ); + const govTerms = { + electionManager: /** @type {any} */ (null), + initialPoserInvitation, + governedParams: { + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: invitationAmount, + }, + PerAccountInitialAmount: { + type: ParamTypes.AMOUNT, + value: starterAmount, + }, + }, + }; + const facets = await E(zoe).startInstance( + installs.provisionPool, + {}, + govTerms, + { + poolBank, + initialPoserInvitation, + storageNode: storageRoot.makeChildNode('provisionPool'), + marshaller: makeFakeBoard().getReadonlyMarshaller(), + }, + ); + const creatorFacet = E(facets.creatorFacet).getLimitedCreatorFacet(); + + // identify the old address + await E(creatorFacet).addRevivableAddresses([oldAddr]); + + // make a bridge handler for provisioning wallets + await E(creatorFacet).setReferences({ + bankManager, + namesByAddressAdmin: makeNameHubKit().nameAdmin, + walletFactory, + }); + const bridgeHandler = await E(creatorFacet).makeHandler(); + + // revive the old wallet and verify absence of new starter funds + const reviverP = E(creatorFacet).getWalletReviver(); + await setReviver(reviverP); + const reviveWallet = addr => E(reviverP).reviveWallet(addr); + await t.throwsAsync( + reviveWallet('addr_unknown'), + undefined, + 'must not revive wallet for unknown address', + ); + const oldWallet = await reviveWallet(oldAddr); + t.deepEqual( + await E(oldPurse).getCurrentAmount(), + fees.make(0n), + 'revived wallet must not receive new starter funds', + ); + const epsilon = fees.make(1n); + /** @type {any} */ + const epsilonPayment = fees.mint.mintPayment(epsilon); + await E(E(oldWallet).getDepositFacet()).receive(epsilonPayment); + t.deepEqual( + await E(oldPurse).getCurrentAmount(), + epsilon, + 'revived wallet must be associated with expected purse', + ); + + // provision a new wallet and verify starter funds + const provisionWallet = address => + E(bridgeHandler).fromBridge({ + type: 'PLEASE_PROVISION', + address, + powerFlags: PowerFlags.SMART_WALLET, + }); + await provisionWallet(newAddr); + t.deepEqual( + await E(newPurse).getCurrentAmount(), + starterAmount, + 'new wallet must receive starter funds', + ); + await t.throwsAsync( + reviveWallet(newAddr), + undefined, + 'must not revive wallet for new address', + ); + + // (re)provision a revived wallet + await provisionWallet(oldAddr); + t.deepEqual( + await E(oldPurse).getCurrentAmount(), + AmountMath.add(epsilon, starterAmount), + 'old wallet received new starter funds', + ); + await t.throwsAsync( + reviveWallet(oldAddr), + undefined, + 'must not re-revive wallet', + ); +}); diff --git a/packages/vats/tools/board-utils.js b/packages/vats/tools/board-utils.js index 1829391b824..b9ff8a96803 100644 --- a/packages/vats/tools/board-utils.js +++ b/packages/vats/tools/board-utils.js @@ -25,8 +25,9 @@ import { Fail } from '@agoric/assert'; import { makeScalarBigMapStore } from '@agoric/vat-data'; +import { unmarshalFromVstorage } from '@agoric/internal/src/lib-chainStorage.js'; import { Far } from '@endo/far'; -import { isObject, makeMarshal } from '@endo/marshal'; +import { makeMarshal } from '@endo/marshal'; import { prepareBoardKit } from '../src/lib-board.js'; /** @@ -34,8 +35,7 @@ import { prepareBoardKit } from '../src/lib-board.js'; * @returns {BoardRemote} */ export const makeBoardRemote = ({ boardId, iface }) => { - const nonalleged = - iface && iface.length ? iface.slice('Alleged: '.length) : ''; + const nonalleged = iface ? iface.replace(/^Alleged: /, '') : ''; return Far(`BoardRemote${nonalleged}`, { getBoardId: () => boardId }); }; @@ -63,14 +63,12 @@ export const makeAgoricNamesRemotesFromFakeStorage = fakeStorageKit => { const reverse = {}; // TODO support vbankAsset which must recur const entries = ['brand', 'instance'].map(kind => { - const key = `published.agoricNames.${kind}`; - - const values = data.get(key); - if (!(values && values.length > 0)) throw Fail`no data for ${key}`; - /** @type {import("@endo/marshal").CapData} */ - const latestCapData = JSON.parse(values.at(-1)); /** @type {Array<[string, import('@agoric/vats/tools/board-utils.js').BoardRemote]>} */ - const parts = fromCapData(latestCapData); + const parts = unmarshalFromVstorage( + data, + `published.agoricNames.${kind}`, + fromCapData, + ); for (const [name, remote] of parts) { reverse[remote.getBoardId()] = name; } @@ -95,55 +93,6 @@ export const boardSlottingMarshaller = (slotToVal = undefined) => { }); }; -/** - * @param {string} cellText - * @returns {unknown[]} - */ -export const parsedValuesFromStreamCellText = cellText => { - assert.typeof(cellText, 'string'); - const cell = /** @type {{blockHeight: string, values: string[]}} */ ( - JSON.parse(cellText) - ); - - assert(isObject(cell)); - const { values } = cell; - - assert(Array.isArray(values)); - const parsedValues = values.map(value => JSON.parse(value)); - - return harden(parsedValues); -}; -harden(parsedValuesFromStreamCellText); - -/** - * @param {unknown} data - * @returns {asserts data is import('@endo/marshal').CapData} - */ -export const assertCapData = data => { - assert.typeof(data, 'object'); - assert(data); - assert.typeof(data.body, 'string'); - assert(Array.isArray(data.slots)); - // XXX check that the .slots array elements are actually strings -}; -harden(assertCapData); - -/** - * Decode vstorage value to CapData - * - * @param {string} cellText - * @returns {import('@endo/marshal').CapData} - */ -export const deserializeVstorageValue = cellText => { - const values = parsedValuesFromStreamCellText(cellText); - - assert.equal(values.length, 1); - const [data] = values; - assertCapData(data); - return data; -}; -harden(deserializeVstorageValue); - /** * Provide access to object graphs serialized in vstorage. * @@ -153,22 +102,19 @@ harden(deserializeVstorageValue); export const makeHistoryReviver = (entries, slotToVal = undefined) => { const board = boardSlottingMarshaller(slotToVal); const vsMap = new Map(entries); - - const getItem = key => { - const raw = vsMap.get(key) || Fail`no ${key}`; - const capData = deserializeVstorageValue(raw); - return harden(board.fromCapData(capData)); - }; + const fromCapData = (...args) => + Reflect.apply(board.fromCapData, board, args); + const getItem = key => unmarshalFromVstorage(vsMap, key, fromCapData); const children = prefix => { prefix.endsWith('.') || Fail`prefix must end with '.'`; - return [ + return harden([ ...new Set( entries .map(([k, _]) => k) .filter(k => k.startsWith(prefix)) .map(k => k.slice(prefix.length).split('.')[0]), ), - ]; + ]); }; return harden({ getItem, children, has: k => vsMap.has(k) }); };