diff --git a/a3p-integration/proposals/s:stake-bld/test-lib/rpc.js b/a3p-integration/proposals/s:stake-bld/test-lib/rpc.js index 2cc3c34913f..1d62aedf1be 100644 --- a/a3p-integration/proposals/s:stake-bld/test-lib/rpc.js +++ b/a3p-integration/proposals/s:stake-bld/test-lib/rpc.js @@ -234,7 +234,7 @@ export const makeAgoricNames = async (ctx, vstorage) => { * @param {{ fetch: typeof window.fetch }} io * @param {MinimalNetworkConfig} config */ -export const makeRpcUtils = async ({ fetch }, config = networkConfig) => { +export const makeVstorageKit = async ({ fetch }, config = networkConfig) => { await null; try { const vstorage = makeVStorage({ fetch }, config); @@ -263,4 +263,4 @@ export const makeRpcUtils = async ({ fetch }, config = networkConfig) => { throw Error(`RPC failure (${config.rpcAddrs}): ${err.message}`); } }; -/** @typedef {Awaited>} RpcUtils */ +/** @typedef {Awaited>} RpcUtils */ diff --git a/a3p-integration/proposals/s:stake-bld/test-lib/wallet.js b/a3p-integration/proposals/s:stake-bld/test-lib/wallet.js index 30ad6b43f26..53d9634e278 100644 --- a/a3p-integration/proposals/s:stake-bld/test-lib/wallet.js +++ b/a3p-integration/proposals/s:stake-bld/test-lib/wallet.js @@ -8,7 +8,7 @@ import { inspect } from 'util'; import { execSwingsetTransaction, pollTx } from './chain.js'; -import { makeRpcUtils } from './rpc.js'; +import { makeVstorageKit } from './rpc.js'; /** * Sign and broadcast a wallet-action. @@ -61,7 +61,7 @@ export const makeWalletUtils = async ( networkConfig, ) => { const { agoricNames, fromBoard, marshaller, readLatestHead, vstorage } = - await makeRpcUtils({ fetch }, networkConfig); + await makeVstorageKit({ fetch }, networkConfig); /** * diff --git a/a3p-integration/proposals/z:acceptance/test-lib/governance.js b/a3p-integration/proposals/z:acceptance/test-lib/governance.js index 64d64fda3e2..ce08264b870 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/governance.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/governance.js @@ -1,12 +1,12 @@ import { agops, agoric, executeOffer } from '@agoric/synthetic-chain'; -import { makeRpcUtils } from '@agoric/client-utils'; +import { makeVstorageKit } from '@agoric/client-utils'; /** * @param {typeof window.fetch} fetch * @param {import('@agoric/client-utils').MinimalNetworkConfig} networkConfig */ export const makeGovernanceDriver = async (fetch, networkConfig) => { - const { readLatestHead, marshaller } = await makeRpcUtils( + const { readLatestHead, marshaller } = await makeVstorageKit( { fetch }, networkConfig, ); diff --git a/a3p-integration/proposals/z:acceptance/test-lib/rpc.js b/a3p-integration/proposals/z:acceptance/test-lib/rpc.js index 5e047204151..907072d2376 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/rpc.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/rpc.js @@ -234,7 +234,7 @@ export const makeAgoricNames = async (ctx, vstorage) => { * @param {{ fetch: typeof window.fetch }} io * @param {MinimalNetworkConfig} config */ -export const makeRpcUtils = async ({ fetch }, config = networkConfig) => { +export const makeVstorageKit = async ({ fetch }, config = networkConfig) => { await null; try { const vstorage = makeVStorage({ fetch }, config); @@ -263,4 +263,4 @@ export const makeRpcUtils = async ({ fetch }, config = networkConfig) => { throw Error(`RPC failure (${config.rpcAddrs}): ${err.message}`); } }; -/** @typedef {Awaited>} RpcUtils */ +/** @typedef {Awaited>} RpcUtils */ diff --git a/packages/agoric-cli/src/commands/auction.js b/packages/agoric-cli/src/commands/auction.js index 241b49ec28c..5a6ba79e84b 100644 --- a/packages/agoric-cli/src/commands/auction.js +++ b/packages/agoric-cli/src/commands/auction.js @@ -2,7 +2,7 @@ /* eslint-env node */ import { InvalidArgumentError } from 'commander'; import { Fail } from '@endo/errors'; -import { makeRpcUtils } from '@agoric/client-utils'; +import { makeVstorageKit } from '@agoric/client-utils'; import { outputActionAndHint } from '../lib/wallet.js'; import { getNetworkConfig } from '../lib/network-config.js'; @@ -89,7 +89,7 @@ export const makeAuctionCommand = ( * }} opts */ async opts => { - const { agoricNames, readLatestHead } = await makeRpcUtils( + const { agoricNames, readLatestHead } = await makeVstorageKit( { fetch }, networkConfig, ); diff --git a/packages/agoric-cli/src/commands/gov.js b/packages/agoric-cli/src/commands/gov.js index fd0efc504ef..26c8a280a25 100644 --- a/packages/agoric-cli/src/commands/gov.js +++ b/packages/agoric-cli/src/commands/gov.js @@ -1,7 +1,7 @@ // @ts-check /* eslint-disable func-names */ /* eslint-env node */ -import { makeRpcUtils } from '@agoric/client-utils'; +import { makeVstorageKit } from '@agoric/client-utils'; import { execFileSync as execFileSyncAmbient } from 'child_process'; import { Command, CommanderError } from 'commander'; import { normalizeAddressWithOptions, pollBlocks } from '../lib/chain.js'; @@ -91,13 +91,13 @@ export const makeGovCommand = (_logger, io = {}) => { * keyringBackend: string, * instanceName?: string, * }} detail - * @param {Awaited>} [optUtils] + * @param {Awaited>} [optUtils] */ const processOffer = async function ( { toOffer, sendFrom, keyringBackend }, optUtils, ) { - const utils = await (optUtils || makeRpcUtils({ fetch }, networkConfig)); + const utils = await (optUtils || makeVstorageKit({ fetch }, networkConfig)); const { agoricNames, readLatestHead } = utils; assert(keyringBackend, 'missing keyring-backend option'); @@ -265,7 +265,7 @@ export const makeGovCommand = (_logger, io = {}) => { ) .requiredOption('--for ', 'description of the invitation') .action(async opts => { - const { agoricNames, readLatestHead } = await makeRpcUtils( + const { agoricNames, readLatestHead } = await makeVstorageKit( { fetch }, networkConfig, ); @@ -294,7 +294,7 @@ export const makeGovCommand = (_logger, io = {}) => { normalizeAddress, ) .action(async opts => { - const { agoricNames, readLatestHead } = await makeRpcUtils( + const { agoricNames, readLatestHead } = await makeVstorageKit( { fetch }, networkConfig, ); @@ -333,7 +333,7 @@ export const makeGovCommand = (_logger, io = {}) => { normalizeAddress, ) .action(async function (opts, options) { - const utils = await makeRpcUtils({ fetch }, networkConfig); + const utils = await makeVstorageKit({ fetch }, networkConfig); const { readLatestHead } = utils; const info = await readLatestHead( diff --git a/packages/agoric-cli/src/commands/oracle.js b/packages/agoric-cli/src/commands/oracle.js index 43392ef912c..dea5ed9e04a 100644 --- a/packages/agoric-cli/src/commands/oracle.js +++ b/packages/agoric-cli/src/commands/oracle.js @@ -2,7 +2,7 @@ /* eslint-disable func-names */ /* eslint-env node */ import { - makeRpcUtils, + makeVstorageKit, makeWalletUtils, storageHelper, } from '@agoric/client-utils'; @@ -87,7 +87,7 @@ export const makeOracleCommand = (logger, io = {}) => { const rpcTools = async () => { // XXX pass fetch to getNetworkConfig() explicitly const networkConfig = await getNetworkConfig({ env: process.env, fetch }); - const utils = await makeRpcUtils({ fetch }, networkConfig); + const utils = await makeVstorageKit({ fetch }, networkConfig); const lookupPriceAggregatorInstance = ([brandIn, brandOut]) => { const name = oracleBrandFeedName(brandIn, brandOut); diff --git a/packages/agoric-cli/src/commands/psm.js b/packages/agoric-cli/src/commands/psm.js index 1d00ec839d0..3a601db299a 100644 --- a/packages/agoric-cli/src/commands/psm.js +++ b/packages/agoric-cli/src/commands/psm.js @@ -1,7 +1,7 @@ // @ts-check /* eslint-disable func-names */ /* eslint-env node */ -import { makeRpcUtils, storageHelper } from '@agoric/client-utils'; +import { makeVstorageKit, storageHelper } from '@agoric/client-utils'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import { Command } from 'commander'; import { getNetworkConfig } from '../lib/network-config.js'; @@ -63,7 +63,7 @@ export const makePsmCommand = logger => { ); const rpcTools = async () => { - const utils = await makeRpcUtils({ fetch }, networkConfig); + const utils = await makeVstorageKit({ fetch }, networkConfig); const lookupPsmInstance = ([minted, anchor]) => { const name = `psm-${minted}-${anchor}`; diff --git a/packages/agoric-cli/src/commands/reserve.js b/packages/agoric-cli/src/commands/reserve.js index cb510283952..7c709d0d27f 100644 --- a/packages/agoric-cli/src/commands/reserve.js +++ b/packages/agoric-cli/src/commands/reserve.js @@ -1,7 +1,7 @@ // @ts-check /* eslint-disable func-names */ /* eslint-env node */ -import { makeRpcUtils } from '@agoric/client-utils'; +import { makeVstorageKit } from '@agoric/client-utils'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import { Command } from 'commander'; import { getNetworkConfig } from '../lib/network-config.js'; @@ -32,7 +32,7 @@ export const makeReserveCommand = (_logger, io = {}) => { * }} opts */ async ({ collateralBrand, ...opts }) => { - const { agoricNames } = await makeRpcUtils({ fetch }, networkConfig); + const { agoricNames } = await makeVstorageKit({ fetch }, networkConfig); const offer = Offers.reserve.AddCollateral(agoricNames, { collateralBrandKey: collateralBrand, @@ -66,7 +66,7 @@ export const makeReserveCommand = (_logger, io = {}) => { 1, ) .action(async function (opts) { - const { agoricNames } = await makeRpcUtils({ fetch }, networkConfig); + const { agoricNames } = await makeVstorageKit({ fetch }, networkConfig); const reserveInstance = agoricNames.instance.reserve; assert(reserveInstance, 'missing reserve in names'); diff --git a/packages/agoric-cli/src/commands/vaults.js b/packages/agoric-cli/src/commands/vaults.js index bc076b0f9bb..185157c1c3d 100644 --- a/packages/agoric-cli/src/commands/vaults.js +++ b/packages/agoric-cli/src/commands/vaults.js @@ -1,7 +1,7 @@ // @ts-check /* eslint-disable func-names */ /* eslint-env node */ -import { makeRpcUtils } from '@agoric/client-utils'; +import { makeVstorageKit } from '@agoric/client-utils'; import { lookupOfferIdForVault, Offers, @@ -39,7 +39,10 @@ export const makeVaultsCommand = logger => { normalizeAddress, ) .action(async function (opts) { - const { readLatestHead } = await makeRpcUtils({ fetch }, networkConfig); + const { readLatestHead } = await makeVstorageKit( + { fetch }, + networkConfig, + ); const current = await getCurrent(opts.from, { readLatestHead, @@ -64,7 +67,7 @@ export const makeVaultsCommand = logger => { .option('--collateralBrand ', 'Collateral brand key', 'ATOM') .action(async function (opts) { logger.warn('running with options', opts); - const { agoricNames } = await makeRpcUtils({ fetch }, networkConfig); + const { agoricNames } = await makeVstorageKit({ fetch }, networkConfig); const offer = Offers.vaults.OpenVault(agoricNames, { giveCollateral: opts.giveCollateral, @@ -99,7 +102,7 @@ export const makeVaultsCommand = logger => { .requiredOption('--vaultId ', 'Key of vault (e.g. vault1)') .action(async function (opts) { logger.warn('running with options', opts); - const { agoricNames, readLatestHead } = await makeRpcUtils( + const { agoricNames, readLatestHead } = await makeVstorageKit( { fetch }, networkConfig, ); @@ -143,7 +146,7 @@ export const makeVaultsCommand = logger => { ) .action(async function (opts) { logger.warn('running with options', opts); - const { agoricNames, readLatestHead } = await makeRpcUtils( + const { agoricNames, readLatestHead } = await makeVstorageKit( { fetch }, networkConfig, ); diff --git a/packages/agoric-cli/src/commands/wallet.js b/packages/agoric-cli/src/commands/wallet.js index a40cd1d1744..480d37f4aa0 100644 --- a/packages/agoric-cli/src/commands/wallet.js +++ b/packages/agoric-cli/src/commands/wallet.js @@ -8,7 +8,7 @@ import { makeLeader, makeLeaderFromRpcAddresses, } from '@agoric/casting'; -import { makeRpcUtils } from '@agoric/client-utils'; +import { makeVstorageKit } from '@agoric/client-utils'; import { execFileSync } from 'child_process'; import fs from 'fs'; import util from 'util'; @@ -105,7 +105,7 @@ export const makeWalletCommand = async command => { .action(async function (opts) { const offerStr = fs.readFileSync(opts.file).toString(); - const { unserializer } = await makeRpcUtils({ fetch }, networkConfig); + const { unserializer } = await makeVstorageKit({ fetch }, networkConfig); const offerObj = unserializer.fromCapData(JSON.parse(offerStr)); console.log(offerObj); @@ -120,7 +120,7 @@ export const makeWalletCommand = async command => { .action(async function (opts) { const offerStr = fs.readFileSync(opts.offer).toString(); - const { unserializer } = await makeRpcUtils({ fetch }, networkConfig); + const { unserializer } = await makeVstorageKit({ fetch }, networkConfig); const offerObj = unserializer.fromCapData(JSON.parse(offerStr)); console.log(offerObj.offer.id); @@ -158,7 +158,7 @@ export const makeWalletCommand = async command => { .command('list') .description('list all wallets in vstorage') .action(async function () { - const { vstorage } = await makeRpcUtils({ fetch }, networkConfig); + const { vstorage } = await makeVstorageKit({ fetch }, networkConfig); const wallets = await vstorage.keys('published.wallet'); process.stdout.write(wallets.join('\n')); }); @@ -172,12 +172,13 @@ export const makeWalletCommand = async command => { normalizeAddress, ) .action(async function (opts) { - const { agoricNames, unserializer, readLatestHead } = await makeRpcUtils( - { - fetch, - }, - networkConfig, - ); + const { agoricNames, unserializer, readLatestHead } = + await makeVstorageKit( + { + fetch, + }, + networkConfig, + ); const leader = makeLeader(networkConfig.rpcAddrs[0]); const follower = await makeFollower( diff --git a/packages/client-utils/src/main.js b/packages/client-utils/src/main.js index 1b82170553a..226227b2e25 100644 --- a/packages/client-utils/src/main.js +++ b/packages/client-utils/src/main.js @@ -1,3 +1,4 @@ export * from './rpc.js'; export * from './sync-tools.js'; +export * from './vstorage-kit.js'; export * from './wallet-utils.js'; diff --git a/packages/client-utils/src/rpc.js b/packages/client-utils/src/rpc.js index 2bfd45e86ac..7ca6330b0d7 100644 --- a/packages/client-utils/src/rpc.js +++ b/packages/client-utils/src/rpc.js @@ -1,14 +1,7 @@ -/* global Buffer */ import { makeTendermintRpcClient } from '@agoric/casting'; -import { - boardSlottingMarshaller, - makeBoardRemote, -} from '@agoric/vats/tools/board-utils.js'; import { StargateClient } from '@cosmjs/stargate'; import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; -export { boardSlottingMarshaller }; - /** * @typedef {{ rpcAddrs: string[], chainName: string }} MinimalNetworkConfig */ @@ -16,240 +9,6 @@ export { boardSlottingMarshaller }; // TODO distribute load export const pickEndpoint = ({ rpcAddrs }) => rpcAddrs[0]; -/** - * @param {object} powers - * @param {typeof window.fetch} powers.fetch - * @param {MinimalNetworkConfig} config - */ -export const makeVStorage = (powers, config) => { - /** @param {string} path */ - const getJSON = path => { - const url = config.rpcAddrs[0] + path; - // console.warn('fetching', url); - return powers.fetch(url, { keepalive: true }).then(res => res.json()); - }; - // height=0 is the same as omitting height and implies the highest block - const url = (path = 'published', { kind = 'children', height = 0 } = {}) => - `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=${height}`; - - const readStorage = (path = 'published', { kind = 'children', height = 0 }) => - getJSON(url(path, { kind, height })) - .catch(err => { - throw Error(`cannot read ${kind} of ${path}: ${err.message}`); - }) - .then(data => { - const { - result: { response }, - } = data; - if (response?.code !== 0) { - /** @type {any} */ - const err = Error( - `error code ${response?.code} reading ${kind} of ${path}: ${response.log}`, - ); - err.code = response?.code; - err.codespace = response?.codespace; - throw err; - } - return data; - }); - - return { - url, - decode({ result: { response } }) { - const { code } = response; - if (code !== 0) { - throw response; - } - const { value } = response; - return Buffer.from(value, 'base64').toString(); - }, - /** - * - * @param {string} path - * @returns {Promise} latest vstorage value at path - */ - async readLatest(path = 'published') { - const raw = await readStorage(path, { kind: 'data' }); - return this.decode(raw); - }, - async keys(path = 'published') { - const raw = await readStorage(path, { kind: 'children' }); - return JSON.parse(this.decode(raw)).children; - }, - /** - * @param {string} path - * @param {number} [height] default is highest - * @returns {Promise<{blockHeight: number, values: string[]}>} - */ - async readAt(path, height = undefined) { - const raw = await readStorage(path, { kind: 'data', height }); - const txt = this.decode(raw); - /** @type {{ value: string }} */ - const { value } = JSON.parse(txt); - return JSON.parse(value); - }, - /** - * Read values going back as far as available - * - * @param {string} path - * @param {number | string} [minHeight] - * @returns {Promise} - */ - async readFully(path, minHeight = undefined) { - const parts = []; - // undefined the first iteration, to query at the highest - let blockHeight; - await null; - do { - // console.debug('READING', { blockHeight }); - let values; - try { - ({ blockHeight, values } = await this.readAt( - path, - blockHeight && Number(blockHeight) - 1, - )); - // console.debug('readAt returned', { blockHeight }); - } catch (err) { - if ( - // CosmosSDK ErrInvalidRequest with particular message text; - // misrepresentation of pruned data - // TODO replace after incorporating a fix to - // https://github.com/cosmos/cosmos-sdk/issues/19992 - err.codespace === 'sdk' && - err.code === 18 && - err.message.match(/pruned/) - ) { - // console.error(err); - break; - } - throw err; - } - parts.push(values); - // console.debug('PUSHED', values); - // console.debug('NEW', { blockHeight, minHeight }); - if (minHeight && Number(blockHeight) <= Number(minHeight)) break; - } while (blockHeight > 0); - return parts.flat(); - }, - }; -}; -/** @typedef {ReturnType} VStorage */ - -/** @deprecated */ -export const makeFromBoard = () => { - const cache = new Map(); - const convertSlotToVal = (boardId, iface) => { - if (cache.has(boardId)) { - return cache.get(boardId); - } - const val = makeBoardRemote({ boardId, iface }); - cache.set(boardId, val); - return val; - }; - return harden({ convertSlotToVal }); -}; -/** @typedef {ReturnType} IdMap */ - -/** @deprecated */ -export const storageHelper = { - /** @param { string } txt */ - parseCapData: txt => { - assert(typeof txt === 'string', typeof txt); - /** @type {{ value: string }} */ - const { value } = JSON.parse(txt); - const specimen = JSON.parse(value); - const { blockHeight, values } = specimen; - assert(values, `empty values in specimen ${value}`); - const capDatas = storageHelper.parseMany(values); - return { blockHeight, capDatas }; - }, - /** - * @param {string} txt - * @param {IdMap} ctx - */ - unserializeTxt: (txt, ctx) => { - const { capDatas } = storageHelper.parseCapData(txt); - return capDatas.map(capData => - boardSlottingMarshaller(ctx.convertSlotToVal).fromCapData(capData), - ); - }, - /** @param {string[]} capDataStrings array of stringified capData */ - parseMany: capDataStrings => { - assert(capDataStrings && capDataStrings.length); - /** @type {{ body: string, slots: string[] }[]} */ - const capDatas = capDataStrings.map(s => JSON.parse(s)); - for (const capData of capDatas) { - assert(typeof capData === 'object' && capData !== null); - assert('body' in capData && 'slots' in capData); - assert(typeof capData.body === 'string'); - assert(Array.isArray(capData.slots)); - } - return capDatas; - }, -}; -harden(storageHelper); - -/** - * @deprecated - * @param {IdMap} ctx - * @param {VStorage} vstorage - * @returns {Promise} - */ -export const makeAgoricNames = async (ctx, vstorage) => { - const reverse = {}; - const entries = await Promise.all( - ['brand', 'instance', 'vbankAsset'].map(async kind => { - const content = await vstorage.readLatest( - `published.agoricNames.${kind}`, - ); - /** @type {Array<[string, import('@agoric/vats/tools/board-utils.js').BoardRemote]>} */ - const parts = storageHelper.unserializeTxt(content, ctx).at(-1); - for (const [name, remote] of parts) { - if ('getBoardId' in remote) { - reverse[remote.getBoardId()] = name; - } - } - return [kind, Object.fromEntries(parts)]; - }), - ); - return { ...Object.fromEntries(entries), reverse }; -}; - -/** - * @param {{ fetch: typeof window.fetch }} io - * @param {MinimalNetworkConfig} config - */ -export const makeRpcUtils = async ({ fetch }, config) => { - await null; - try { - const vstorage = makeVStorage({ fetch }, config); - const fromBoard = makeFromBoard(); - const agoricNames = await makeAgoricNames(fromBoard, vstorage); - - const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); - - /** @type {(txt: string) => unknown} */ - const unserializeHead = txt => - storageHelper.unserializeTxt(txt, fromBoard).at(-1); - - /** @type {(path: string) => Promise} */ - const readLatestHead = path => - vstorage.readLatest(path).then(unserializeHead); - - return { - agoricNames, - fromBoard, - marshaller, - readLatestHead, - unserializeHead, - vstorage, - }; - } catch (err) { - throw Error(`RPC failure (${config.rpcAddrs}): ${err.message}`); - } -}; -/** @typedef {Awaited>} RpcUtils */ - /** * @param {string} endpoint * @param {{ fetch: typeof window.fetch }} io diff --git a/packages/client-utils/src/vstorage-kit.js b/packages/client-utils/src/vstorage-kit.js new file mode 100644 index 00000000000..3dce95e2c64 --- /dev/null +++ b/packages/client-utils/src/vstorage-kit.js @@ -0,0 +1,248 @@ +/* global Buffer */ +import { + boardSlottingMarshaller, + makeBoardRemote, +} from '@agoric/vats/tools/board-utils.js'; + +export { boardSlottingMarshaller }; + +/** + * @import {MinimalNetworkConfig} from './rpc.js'; + */ + +/** + * @param {object} powers + * @param {typeof window.fetch} powers.fetch + * @param {MinimalNetworkConfig} config + */ +export const makeVStorage = (powers, config) => { + /** @param {string} path */ + const getJSON = path => { + const url = config.rpcAddrs[0] + path; + // console.warn('fetching', url); + return powers.fetch(url, { keepalive: true }).then(res => res.json()); + }; + // height=0 is the same as omitting height and implies the highest block + const url = (path = 'published', { kind = 'children', height = 0 } = {}) => + `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=${height}`; + + const readStorage = (path = 'published', { kind = 'children', height = 0 }) => + getJSON(url(path, { kind, height })) + .catch(err => { + throw Error(`cannot read ${kind} of ${path}: ${err.message}`); + }) + .then(data => { + const { + result: { response }, + } = data; + if (response?.code !== 0) { + /** @type {any} */ + const err = Error( + `error code ${response?.code} reading ${kind} of ${path}: ${response.log}`, + ); + err.code = response?.code; + err.codespace = response?.codespace; + throw err; + } + return data; + }); + + return { + url, + decode({ result: { response } }) { + const { code } = response; + if (code !== 0) { + throw response; + } + const { value } = response; + return Buffer.from(value, 'base64').toString(); + }, + /** + * + * @param {string} path + * @returns {Promise} latest vstorage value at path + */ + async readLatest(path = 'published') { + const raw = await readStorage(path, { kind: 'data' }); + return this.decode(raw); + }, + async keys(path = 'published') { + const raw = await readStorage(path, { kind: 'children' }); + return JSON.parse(this.decode(raw)).children; + }, + /** + * @param {string} path + * @param {number} [height] default is highest + * @returns {Promise<{blockHeight: number, values: string[]}>} + */ + async readAt(path, height = undefined) { + const raw = await readStorage(path, { kind: 'data', height }); + const txt = this.decode(raw); + /** @type {{ value: string }} */ + const { value } = JSON.parse(txt); + return JSON.parse(value); + }, + /** + * Read values going back as far as available + * + * @param {string} path + * @param {number | string} [minHeight] + * @returns {Promise} + */ + async readFully(path, minHeight = undefined) { + const parts = []; + // undefined the first iteration, to query at the highest + let blockHeight; + await null; + do { + // console.debug('READING', { blockHeight }); + let values; + try { + ({ blockHeight, values } = await this.readAt( + path, + blockHeight && Number(blockHeight) - 1, + )); + // console.debug('readAt returned', { blockHeight }); + } catch (err) { + if ( + // CosmosSDK ErrInvalidRequest with particular message text; + // misrepresentation of pruned data + // TODO replace after incorporating a fix to + // https://github.com/cosmos/cosmos-sdk/issues/19992 + err.codespace === 'sdk' && + err.code === 18 && + err.message.match(/pruned/) + ) { + // console.error(err); + break; + } + throw err; + } + parts.push(values); + // console.debug('PUSHED', values); + // console.debug('NEW', { blockHeight, minHeight }); + if (minHeight && Number(blockHeight) <= Number(minHeight)) break; + } while (blockHeight > 0); + return parts.flat(); + }, + }; +}; +/** @typedef {ReturnType} VStorage */ + +/** @deprecated */ +export const makeFromBoard = () => { + const cache = new Map(); + const convertSlotToVal = (boardId, iface) => { + if (cache.has(boardId)) { + return cache.get(boardId); + } + const val = makeBoardRemote({ boardId, iface }); + cache.set(boardId, val); + return val; + }; + return harden({ convertSlotToVal }); +}; +/** @typedef {ReturnType} IdMap */ + +/** @deprecated */ +export const storageHelper = { + /** @param { string } txt */ + parseCapData: txt => { + assert(typeof txt === 'string', typeof txt); + /** @type {{ value: string }} */ + const { value } = JSON.parse(txt); + const specimen = JSON.parse(value); + const { blockHeight, values } = specimen; + assert(values, `empty values in specimen ${value}`); + const capDatas = storageHelper.parseMany(values); + return { blockHeight, capDatas }; + }, + /** + * @param {string} txt + * @param {IdMap} ctx + */ + unserializeTxt: (txt, ctx) => { + const { capDatas } = storageHelper.parseCapData(txt); + return capDatas.map(capData => + boardSlottingMarshaller(ctx.convertSlotToVal).fromCapData(capData), + ); + }, + /** @param {string[]} capDataStrings array of stringified capData */ + parseMany: capDataStrings => { + assert(capDataStrings && capDataStrings.length); + /** @type {{ body: string, slots: string[] }[]} */ + const capDatas = capDataStrings.map(s => JSON.parse(s)); + for (const capData of capDatas) { + assert(typeof capData === 'object' && capData !== null); + assert('body' in capData && 'slots' in capData); + assert(typeof capData.body === 'string'); + assert(Array.isArray(capData.slots)); + } + return capDatas; + }, +}; +harden(storageHelper); + +/** + * @deprecated + * @param {IdMap} ctx + * @param {VStorage} vstorage + * @returns {Promise} + */ +export const makeAgoricNames = async (ctx, vstorage) => { + const reverse = {}; + const entries = await Promise.all( + ['brand', 'instance', 'vbankAsset'].map(async kind => { + const content = await vstorage.readLatest( + `published.agoricNames.${kind}`, + ); + /** @type {Array<[string, import('@agoric/vats/tools/board-utils.js').BoardRemote]>} */ + const parts = storageHelper.unserializeTxt(content, ctx).at(-1); + for (const [name, remote] of parts) { + if ('getBoardId' in remote) { + reverse[remote.getBoardId()] = name; + } + } + return [kind, Object.fromEntries(parts)]; + }), + ); + return { ...Object.fromEntries(entries), reverse }; +}; + +/** + * @param {{ fetch: typeof window.fetch }} io + * @param {MinimalNetworkConfig} config + */ +export const makeVstorageKit = async ({ fetch }, config) => { + await null; + try { + const vstorage = makeVStorage({ fetch }, config); + const fromBoard = makeFromBoard(); + const agoricNames = await makeAgoricNames(fromBoard, vstorage); + + const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); + + /** @type {(txt: string) => unknown} */ + const unserializeHead = txt => + storageHelper.unserializeTxt(txt, fromBoard).at(-1); + + /** + * Read latest at path and unmarshal it + * @type {(path: string) => Promise} + */ + const readLatestHead = path => + vstorage.readLatest(path).then(unserializeHead); + + return { + agoricNames, + fromBoard, + marshaller, + readLatestHead, + unserializeHead, + vstorage, + }; + } catch (err) { + throw Error(`RPC failure (${config.rpcAddrs}): ${err.message}`); + } +}; +/** @typedef {Awaited>} VstorageKit */ diff --git a/packages/client-utils/src/wallet-utils.js b/packages/client-utils/src/wallet-utils.js index c06b7c010b0..582b57a035a 100644 --- a/packages/client-utils/src/wallet-utils.js +++ b/packages/client-utils/src/wallet-utils.js @@ -1,10 +1,7 @@ import { makeWalletStateCoalescer } from '@agoric/smart-wallet/src/utils.js'; import { pollBlocks } from './chain.js'; -import { - boardSlottingMarshaller, - makeRpcUtils, - makeStargateClient, -} from './rpc.js'; +import { makeStargateClient } from './rpc.js'; +import { boardSlottingMarshaller, makeVstorageKit } from './vstorage-kit.js'; /** * @import {Amount, Brand} from '@agoric/ertp/src/types.js' @@ -20,7 +17,7 @@ import { */ export const makeWalletUtils = async ({ fetch, delay }, networkConfig) => { const { agoricNames, fromBoard, marshaller, readLatestHead, vstorage } = - await makeRpcUtils({ fetch }, networkConfig); + await makeVstorageKit({ fetch }, networkConfig); const m = boardSlottingMarshaller(fromBoard.convertSlotToVal); const client = await makeStargateClient(networkConfig, { fetch }); diff --git a/packages/client-utils/test/snapshots/exports.test.js.md b/packages/client-utils/test/snapshots/exports.test.js.md index a63af29bc31..8a1c268100a 100644 --- a/packages/client-utils/test/snapshots/exports.test.js.md +++ b/packages/client-utils/test/snapshots/exports.test.js.md @@ -12,10 +12,10 @@ Generated by [AVA](https://avajs.dev). 'boardSlottingMarshaller', 'makeAgoricNames', 'makeFromBoard', - 'makeRpcUtils', 'makeStargateClient', 'makeTendermint34Client', 'makeVStorage', + 'makeVstorageKit', 'makeWalletUtils', 'pickEndpoint', 'retryUntilCondition', diff --git a/packages/client-utils/test/snapshots/exports.test.js.snap b/packages/client-utils/test/snapshots/exports.test.js.snap index 32b53384a2c..831d79980ba 100644 Binary files a/packages/client-utils/test/snapshots/exports.test.js.snap and b/packages/client-utils/test/snapshots/exports.test.js.snap differ