diff --git a/upgrade-test-scripts/lib/agd-lib.js b/upgrade-test-scripts/lib/agd-lib.js new file mode 100644 index 00000000..8d2220e4 --- /dev/null +++ b/upgrade-test-scripts/lib/agd-lib.js @@ -0,0 +1,119 @@ +// @ts-check +// @jessie-check + +const { freeze } = Object; + +const agdBinary = 'agd'; + +/** @param {{ execFileSync: typeof import('child_process').execFileSync }} io */ +export const makeAgd = ({ execFileSync }) => { + console.warn('XXX is sync IO essential?'); + + /** @param {{ home?: string, keyringBackend?: string, rpcAddrs?: string[] }} keyringOpts */ + const make = ({ home, keyringBackend, rpcAddrs } = {}) => { + const keyringArgs = [ + ...(home ? ['--home', home] : []), + ...(keyringBackend ? [`--keyring-backend`, keyringBackend] : []), + ]; + console.warn('XXX: rpcAddrs after [0] are ignored'); + const nodeArgs = [...(rpcAddrs ? [`--node`, rpcAddrs[0]] : [])]; + + // TODO: verbose option + const l = a => { + console.log(a); // XXX unilateral logging by a library... iffy + return a; + }; + + /** + * @param {string[]} args + * @param {*} [opts] + */ + const exec = (args, opts) => execFileSync(agdBinary, args, opts).toString(); + + const outJson = ['--output', 'json']; + + const ro = freeze({ + status: async () => JSON.parse(exec([...nodeArgs, 'status'])), + /** + * @param { + * | [kind: 'tx', txhash: string] + * | [mod: 'vstorage', kind: 'data' | 'children', path: string] + * } qArgs + */ + query: async qArgs => { + const out = await exec(['query', ...qArgs, ...nodeArgs, ...outJson], { + stdio: ['ignore', 'pipe', 'ignore'], + }); + try { + return JSON.parse(out); + } catch (e) { + console.error(e); + console.info('output:', out); + } + }, + }); + const nameHub = freeze({ + /** + * @param {string[]} path + * NOTE: synchronous I/O + */ + lookup: (...path) => { + if (!Array.isArray(path)) { + // TODO: use COND || Fail`` + throw TypeError(); + } + if (path.length !== 1) { + throw Error(`path length limited to 1: ${path.length}`); + } + const [name] = path; + const txt = exec(['keys', 'show', `--address`, name, ...keyringArgs]); + return txt.trim(); + }, + }); + const rw = freeze({ + /** + * TODO: gas + * + * @param {string[]} txArgs + * @param {{ chainId: string, from: string, yes?: boolean }} opts + */ + tx: async (txArgs, { chainId, from, yes }) => { + const yesArg = yes ? ['--yes'] : []; + const args = [ + ...nodeArgs, + ...[`--chain-id`, chainId], + ...keyringArgs, + ...[`--from`, from], + 'tx', + ...txArgs, + ...['--broadcast-mode', 'block'], + ...yesArg, + ...outJson, + ]; + const out = exec(args); + try { + return JSON.parse(out); + } catch (e) { + console.error(e); + console.info('output:', out); + } + }, + ...ro, + ...nameHub, + readOnly: () => ro, + nameHub: () => nameHub, + keys: { + add: (name, mnemonic) => { + return execFileSync( + agdBinary, + [...keyringArgs, 'keys', 'add', name, '--recover'], + { input: mnemonic }, + ).toString(); + }, + }, + withOpts: opts => make({ home, keyringBackend, rpcAddrs, ...opts }), + }); + return rw; + }; + return make(); +}; diff --git a/upgrade-test-scripts/lib/assert.js b/upgrade-test-scripts/lib/assert.js new file mode 100644 index 00000000..27599462 --- /dev/null +++ b/upgrade-test-scripts/lib/assert.js @@ -0,0 +1,21 @@ +export const Fail = (template, ...args) => { + throw Error(String.raw(template, ...args.map(val => String(val)))); +}; + +export const assert = (cond, msg = 'check failed') => { + if (!cond) { + throw Error(msg); + } +}; + +assert.typeof = (val, type) => { + if (typeof val !== type) { + throw Error(`expected ${type}, got ${typeof val}`); + } +}; + +/** @type {(val: T | undefined) => T} */ +export const NonNullish = val => { + if (!val) throw Error('required'); + return val; +}; diff --git a/upgrade-test-scripts/cliHelper.js b/upgrade-test-scripts/lib/cliHelper.js similarity index 70% rename from upgrade-test-scripts/cliHelper.js rename to upgrade-test-scripts/lib/cliHelper.js index 4c3e0f4b..cb3c3adf 100644 --- a/upgrade-test-scripts/cliHelper.js +++ b/upgrade-test-scripts/lib/cliHelper.js @@ -129,3 +129,54 @@ export const bundleSource = async (filePath, bundleName) => { console.log(output.stderr); return `/tmp/bundle-${bundleName}.json`; }; + +export const wellKnownIdentities = async (io = {}) => { + const { agoric: { follow = agoric.follow } = {} } = io; + const zip = (xs, ys) => xs.map((x, i) => [x, ys[i]]); + const fromSmallCapsEntries = txt => { + const { body, slots } = JSON.parse(txt); + const theEntries = zip(JSON.parse(body.slice(1)), slots).map( + ([[name, ref], boardID]) => { + const iface = ref.replace(/^\$\d+\./, ''); + return [name, { iface, boardID }]; + }, + ); + return Object.fromEntries(theEntries); + }; + + const installation = fromSmallCapsEntries( + await follow('-lF', ':published.agoricNames.installation', '-o', 'text'), + ); + + const instance = fromSmallCapsEntries( + await follow('-lF', ':published.agoricNames.instance', '-o', 'text'), + ); + + const brand = fromSmallCapsEntries( + await follow('-lF', ':published.agoricNames.brand', '-o', 'text'), + ); + + return { brand, installation, instance }; +}; + +export const smallCapsContext = () => { + const slots = []; // XXX global mutable state + const smallCaps = { + Nat: n => `+${n}`, + // XXX mutates obj + ref: obj => { + if (obj.ix) return obj.ix; + const ix = slots.length; + slots.push(obj.boardID); + obj.ix = `$${ix}.Alleged: ${obj.iface}`; + return obj.ix; + }, + }; + + const toCapData = body => { + const capData = { body: `#${JSON.stringify(body)}`, slots }; + return JSON.stringify(capData); + }; + + return { smallCaps, toCapData }; +}; diff --git a/upgrade-test-scripts/commonUpgradeHelpers.js b/upgrade-test-scripts/lib/commonUpgradeHelpers.js similarity index 94% rename from upgrade-test-scripts/commonUpgradeHelpers.js rename to upgrade-test-scripts/lib/commonUpgradeHelpers.js index dd631468..2c7945d5 100644 --- a/upgrade-test-scripts/commonUpgradeHelpers.js +++ b/upgrade-test-scripts/lib/commonUpgradeHelpers.js @@ -177,16 +177,19 @@ export const voteLatestProposalAndWait = async () => { 'test', ); - let status = ''; - - do { - const proposalData = await agd.query('gov', 'proposal', lastProposalId); - status = proposalData.status; - console.log(`Waiting for proposal to pass (status=${status})`); - } while ( - status !== 'PROPOSAL_STATUS_REJECTED' && - status !== 'PROPOSAL_STATUS_PASSED' - ); + let info = {}; + for ( + ; + info.status !== 'PROPOSAL_STATUS_REJECTED' && + info.status !== 'PROPOSAL_STATUS_PASSED'; + await waitForBlock() + ) { + info = await agd.query('gov', 'proposal', lastProposalId); + console.log( + `Waiting for proposal ${lastProposalId} to pass (status=${info.status})`, + ); + } + return info; }; const Fail = (template, ...args) => { diff --git a/upgrade-test-scripts/constants.js b/upgrade-test-scripts/lib/constants.js similarity index 100% rename from upgrade-test-scripts/constants.js rename to upgrade-test-scripts/lib/constants.js diff --git a/upgrade-test-scripts/econHelpers.js b/upgrade-test-scripts/lib/econHelpers.js similarity index 92% rename from upgrade-test-scripts/econHelpers.js rename to upgrade-test-scripts/lib/econHelpers.js index ced1dff0..fb7f604d 100644 --- a/upgrade-test-scripts/econHelpers.js +++ b/upgrade-test-scripts/lib/econHelpers.js @@ -54,7 +54,7 @@ export const closeVault = (address, vaultId, mint) => { ); }; -export const mintIST = async (addr, sendValue, giveCollateral, wantMinted) => { +export const mintIST = async (addr, sendValue, wantMinted, giveCollateral) => { await agd.tx( 'bank', 'send', @@ -69,5 +69,5 @@ export const mintIST = async (addr, sendValue, giveCollateral, wantMinted) => { 'test', '--yes', ); - await openVault(addr, giveCollateral, wantMinted); + await openVault(addr, wantMinted, giveCollateral); }; diff --git a/upgrade-test-scripts/lib/unmarshal.js b/upgrade-test-scripts/lib/unmarshal.js new file mode 100644 index 00000000..f4f56b61 --- /dev/null +++ b/upgrade-test-scripts/lib/unmarshal.js @@ -0,0 +1,143 @@ +// @ts-check +'use strict'; + +const { + create, + entries, + fromEntries, + freeze, + keys, + setPrototypeOf, + prototype: objectPrototype, +} = Object; +const { isArray } = Array; + +const sigilDoc = { + '!': 'escaped string', + '+': `non-negative bigint`, + '-': `negative bigint`, + '#': `manifest constant`, + '%': `symbol`, + $: `remotable`, + '&': `promise`, +}; +const sigils = keys(sigilDoc).join(''); + +/** @type {(obj: Record, f: (v: V) => U) => Record} */ +const objMap = (obj, f) => + fromEntries(entries(obj).map(([p, v]) => [f(p), f(v)])); + +const { freeze: harden } = Object; // XXX + +const makeMarshal = (_v2s, convertSlotToVal = (s, _i) => s) => { + const fromCapData = ({ body, slots }) => { + const recur = v => { + switch (typeof v) { + case 'boolean': + case 'number': + return v; + case 'string': + if (v === '') return v; + const sigil = v.slice(0, 1); + if (!sigils.includes(sigil)) return v; + switch (sigil) { + case '!': + return v.slice(1); + case '+': + return BigInt(v.slice(1)); + case '-': + return -BigInt(v.slice(1)); + case '$': { + const [ix, iface] = v.slice(1).split('.'); + return convertSlotToVal(slots[Number(ix)], iface); + } + case '#': + switch (v) { + case '#undefined': + return undefined; + case '#Infinity': + return Infinity; + case '#NaN': + return Infinity; + default: + throw RangeError(`Unexpected constant ${v}`); + } + case '%': + // TODO: @@asyncIterator + return Symbol.for(v.slice(1)); + default: + throw RangeError(`Unexpected sigil ${sigil}`); + } + case 'object': + if (v === null) return v; + if (isArray(v)) { + return freeze(v.map(recur)); + } + return freeze(objMap(v, recur)); + default: + throw RangeError(`Unexpected value type ${typeof v}`); + } + }; + const encoding = JSON.parse(body.replace(/^#/, '')); + return recur(encoding); + }; + + const toCapData = () => { + throw Error('not implemented'); + }; + + return harden({ + fromCapData, + unserialize: fromCapData, + toCapData, + serialize: toCapData, + }); +}; + +const PASS_STYLE = Symbol.for('passStyle'); +export const Far = (iface, methods) => { + const proto = freeze( + create(objectPrototype, { + [PASS_STYLE]: { value: 'remotable' }, + [Symbol.toStringTag]: { value: iface }, + }), + ); + setPrototypeOf(methods, proto); + freeze(methods); + return methods; +}; + +// #region marshal-table +const makeSlot1 = (val, serial) => { + const prefix = Promise.resolve(val) === val ? 'promise' : 'object'; + return `${prefix}${serial}`; +}; + +const makeTranslationTable = (makeSlot, makeVal) => { + const valToSlot = new Map(); + const slotToVal = new Map(); + + const convertValToSlot = val => { + if (valToSlot.has(val)) return valToSlot.get(val); + const slot = makeSlot(val, valToSlot.size); + valToSlot.set(val, slot); + slotToVal.set(slot, val); + return slot; + }; + + const convertSlotToVal = (slot, iface) => { + if (slotToVal.has(slot)) return slotToVal.get(slot); + if (makeVal) { + const val = makeVal(slot, iface); + valToSlot.set(val, slot); + slotToVal.set(slot, val); + return val; + } + throw Error(`no such ${iface}: ${slot}`); + }; + + return harden({ convertValToSlot, convertSlotToVal }); +}; +// #endregion marshal-table + +export { makeMarshal, makeTranslationTable }; diff --git a/upgrade-test-scripts/lib/vat-status.js b/upgrade-test-scripts/lib/vat-status.js new file mode 100644 index 00000000..cd968911 --- /dev/null +++ b/upgrade-test-scripts/lib/vat-status.js @@ -0,0 +1,92 @@ +// @ts-check +import dbOpenAmbient from 'better-sqlite3'; +import { HOME } from './constants.js'; + +/** + * @file look up vat incarnation from kernel DB + * @see {getIncarnation} + */ + +const swingstorePath = '~/.agoric/data/agoric/swingstore.sqlite'; + +/** + * SQL short-hand + * + * @param {import('better-sqlite3').Database} db + */ +export const dbTool = db => { + const prepare = (strings, ...params) => { + const dml = strings.join('?'); + return { stmt: db.prepare(dml), params }; + }; + const sql = (strings, ...args) => { + const { stmt, params } = prepare(strings, ...args); + return stmt.all(...params); + }; + sql.get = (strings, ...args) => { + const { stmt, params } = prepare(strings, ...args); + return stmt.get(...params); + }; + return sql; +}; + +/** + * @param {import('better-sqlite3').Database} db + */ +const makeSwingstore = db => { + const sql = dbTool(db); + + /** @param {string} key */ + const kvGet = key => sql.get`select * from kvStore where key = ${key}`.value; + /** @param {string} key */ + const kvGetJSON = key => JSON.parse(kvGet(key)); + + /** @param {string} vatID */ + const lookupVat = vatID => { + return Object.freeze({ + source: () => kvGetJSON(`${vatID}.source`), + options: () => kvGetJSON(`${vatID}.options`), + currentSpan: () => + sql.get`select * from transcriptSpans where isCurrent = 1 and vatID = ${vatID}`, + }); + }; + + return Object.freeze({ + /** @param {string} vatName */ + findVat: vatName => { + /** @type {string[]} */ + const dynamicIDs = kvGetJSON('vat.dynamicIDs'); + const targetVat = dynamicIDs.find( + vatID => lookupVat(vatID).options().name === vatName, + ); + if (!targetVat) throw Error(vatName); + return targetVat; + }, + lookupVat, + }); +}; + +/** @type {(val: T | undefined) => T} */ +const NonNullish = val => { + if (!val) throw Error('required'); + return val; +}; + +/** + * @param {string} vatName + */ +export const getIncarnation = async vatName => { + const fullPath = swingstorePath.replace(/^~/, NonNullish(HOME)); + const kStore = makeSwingstore(dbOpenAmbient(fullPath, { readonly: true })); + + const vatID = kStore.findVat(vatName); + const vatInfo = kStore.lookupVat(vatID); + + const source = vatInfo.source(); + const { incarnation } = vatInfo.currentSpan(); + + // misc info to stderr + console.error(JSON.stringify({ vatName, vatID, incarnation, ...source })); + + return incarnation; +}; diff --git a/upgrade-test-scripts/lib/vstorage.js b/upgrade-test-scripts/lib/vstorage.js new file mode 100644 index 00000000..cd1437d4 --- /dev/null +++ b/upgrade-test-scripts/lib/vstorage.js @@ -0,0 +1,42 @@ +// @ts-check +/* global Buffer */ +import { assert, Fail } from './assert.js'; + +const { freeze: harden } = Object; // XXX + +// from '@agoric/internal/src/lib-chainStorage.js'; +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); + +/** + * Extract one value from a the vstorage stream cell in a QueryDataResponse + * + * @param {object} data + * @param {number} [index] index of the desired value in a deserialized stream cell + * + * XXX import('@agoric/cosmic-proto/vstorage/query').QueryDataResponse doesn't worksomehow + * @typedef {Awaited>} QueryDataResponseT + */ +export const extractStreamCellValue = (data, index = -1) => { + const { value: serialized } = data; + + serialized.length > 0 || Fail`no StreamCell values: ${data}`; + + 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 value = values.at(index); + assert.typeof(value, 'string'); + return value; +}; +harden(extractStreamCellValue); diff --git a/upgrade-test-scripts/lib/webAsset.js b/upgrade-test-scripts/lib/webAsset.js new file mode 100644 index 00000000..bb9514d7 --- /dev/null +++ b/upgrade-test-scripts/lib/webAsset.js @@ -0,0 +1,200 @@ +// @ts-check +import { tmpName } from 'tmp'; + +const dbg = label => x => { + label; + // console.log(label, x); + return x; +}; + +/** + * + * @param {string} root + * @param {{ fetch: typeof fetch }} io + * + * @typedef {ReturnType} TextRd + */ +export const makeWebRd = (root, { fetch }) => { + /** @param {string} there */ + const make = there => { + const join = (...segments) => { + dbg('web.join')({ there, segments }); + let out = there; + for (const segment of segments) { + out = `${new URL(segment, out)}`; + } + return out; + }; + const self = { + toString: () => there, + /** @param {string[]} segments */ + join: (...segments) => make(join(...segments)), + readText: async () => { + console.log('WebRd fetch:', there); + const res = await fetch(there); + if (!res.ok) { + throw Error(`${res.statusText} @ ${there}`); + } + return res.text(); + }, + }; + return self; + }; + return make(root); +}; + +/** + * Reify file read access as an object. + * + * @param {string} root + * @param {object} io + * @param {Pick} io.fsp + * @param {Pick} io.path + * + * @typedef {ReturnType} FileRd + */ +export const makeFileRd = (root, { fsp, path }) => { + /** @param {string} there */ + const make = there => { + const self = { + toString: () => there, + /** @param {string[]} segments */ + join: (...segments) => make(path.join(there, ...segments)), + stat: () => fsp.stat(there), + readText: () => fsp.readFile(there, 'utf8'), + }; + return self; + }; + return make(root); +}; + +/** + * Reify file read/write access as an object. + * + * @param {string} root + * @param {object} io + * @param {Pick} io.fsp + * @param {Pick} io.path + * + * @typedef {ReturnType} FileRW + */ +export const makeFileRW = (root, { fsp, path }) => { + /** @param {string} there */ + const make = there => { + const ro = makeFileRd(there, { fsp, path }); + const self = { + toString: () => there, + readOnly: () => ro, + /** @param {string[]} segments */ + join: (...segments) => + make(dbg('FileRW join')(path.join(there, ...segments))), + writeText: text => fsp.writeFile(there, text, 'utf8'), + unlink: () => fsp.unlink(there), + mkdir: () => fsp.mkdir(there, { recursive: true }), + rmdir: () => fsp.rmdir(there), + }; + return self; + }; + return make(root); +}; + +/** + * @param {TextRd} src + * @param {FileRW} dest + * + * @typedef {ReturnType} WebCache + */ +export const makeWebCache = (src, dest) => { + /** @type {Map>} */ + const saved = new Map(); + + /** @param {string} segment */ + const getFileP = segment => { + const target = src.join(segment); + const addr = `${target}`; + const cached = saved.get(addr); + if (cached) return cached; + + const f = dest.join(segment); + /** @type {Promise} */ + const p = new Promise((resolve, reject) => + target + .readText() + .then(txt => + dest + .mkdir() + .then(() => f.writeText(txt).then(_ => resolve(f.readOnly()))), + ) + .catch(reject), + ); + saved.set(addr, p); + return p; + }; + + const remove = async () => { + await Promise.all([...saved.values()].map(p => p.then(f => f.unlink()))); + await dest.rmdir(); + }; + + const self = { + toString: () => `${src} -> ${dest}`, + /** @param {string} segment */ + getText: async segment => { + const fr = await getFileP(segment); + return fr.readText(); + }, + /** @param {string} segment */ + storedPath: segment => getFileP(segment).then(f => f.toString()), + /** @param {string} segment */ + size: async segment => { + const fr = await getFileP(segment); + const info = await fr.stat(); + return info.size; + }, + remove, + }; + return self; +}; + +const buildInfo = [ + { + evals: [ + { + permit: 'kread-invite-committee-permit.json', + script: 'kread-invite-committee.js', + }, + ], + bundles: [ + 'b1-51085a4ad4ac3448ccf039c0b54b41bd11e9367dfbd641deda38e614a7f647d7f1c0d34e55ba354d0331b1bf54c999fca911e6a796c90c30869f7fb8887b3024.json', + 'b1-a724453e7bfcaae1843be4532e18c1236c3d6d33bf6c44011f2966e155bc7149b904573014e583fdcde2b9cf2913cb8b337fc9daf79c59a38a37c99030fcf7dc.json', + ], + }, + { + evals: [{ permit: 'start-kread-permit.json', script: 'start-kread.js' }], + bundles: [ + '/Users/wietzes/.agoric/cache/b1-853acd6ba3993f0f19d6c5b0a88c9a722c9b41da17cf7f98ff7705e131860c4737d7faa758ca2120773632dbaf949e4bcce2a2cbf2db224fa09cd165678f64ac.json', + '/Users/wietzes/.agoric/cache/b1-0c3363b8737677076e141a84b84c8499012f6ba79c0871fc906c8be1bb6d11312a7d14d5a3356828a1de6baa4bee818a37b7cb1ca2064f6eecbabc0a40d28136.json', + ], + }, +]; + +const main = async () => { + const td = await new Promise((resolve, reject) => + tmpName({ prefix: 'assets' }, (err, x) => (err ? reject(err) : resolve(x))), + ); + const src = makeWebRd( + 'https://github.com/Kryha/KREAd/releases/download/KREAd-rc1/', + { fetch }, + ); + const fsp = await import('fs/promises'); + const path = await import('path'); + const dest = makeFileRW(td, { fsp, path }); + const assets = makeWebCache(src, dest); + const segment = buildInfo[0].bundles[0]; + const info = await assets.size(segment); + console.log(`${segment}:`, info); +}; + +// main().catch(err => console.error(err));