From 77f6d12746048a75c237594e44aad99c8ce29f3b Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 20 Aug 2022 15:50:59 -0500 Subject: [PATCH 01/11] feat(inter-protocol): psm-tool CLI formats spendAction refactor: use types of observedSpendAction feat: miniMarshal chore: separate flags from opts fixup: typeof fetch comment fixup: types for main refactor: parseCapData feat: getOfferState (WIP) fixup: opts / flags --- packages/inter-protocol/scripts/psm-tool.js | 293 ++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 packages/inter-protocol/scripts/psm-tool.js diff --git a/packages/inter-protocol/scripts/psm-tool.js b/packages/inter-protocol/scripts/psm-tool.js new file mode 100644 index 00000000000..354a342c284 --- /dev/null +++ b/packages/inter-protocol/scripts/psm-tool.js @@ -0,0 +1,293 @@ +#!/usr/bin/env node +// @ts-check +/* global atob */ +/* global process, fetch */ +import assert from 'assert'; + +const RPC_BASE = 'https://xnet.rpc.agoric.net/'; + +const USAGE = ` +Usage: + psm-tool --lookup [--verbose] + psm-tool --give ANCHOR_TOKENS --want IST_TOKENS --boardId BOARD_ID + +NOTE: --lookup may need --experimental-fetch node option. + +For example: + +psmInstance=$(psm-tool --lookup) +psm-tool --give 101 --want 100 --boardId $psmInstance >,psm-offer-action.json +agd --node=${RPC_BASE} --chain-id=agoricxnet-13 \ + --from=LEDGER_KEY_NAME --sign-mode=amino-json \ + tx swingset wallet-action --allow-spend "$(cat ,psm-offer-action.json)" +`; + +/** + * Petnames depend on names issued by VBANK. + */ +const vBankPetName = { + purse: { + anchor: 'AUSD', + ist: 'Agoric stable local currency', + }, +}; + +const COSMOS_UNIT = 1_000_000n; + +// eslint-disable-next-line no-unused-vars +const observedSpendAction = { + type: 'acceptOffer', + data: { + id: '1661031322225', + instancePetname: 'instance@board03040', + requestContext: { + dappOrigin: 'https://amm.agoric.app', + origin: 'https://amm.agoric.app', + }, + meta: { id: '1661031322225', creationStamp: 1661031322225 }, + status: 'proposed', + instanceHandleBoardId: 'board03040', + invitationMaker: { method: 'makeSwapInInvitation' }, + proposalTemplate: { + give: { + In: { pursePetname: 'Agoric stable local currency', value: 5000000 }, + }, + want: { Out: { pursePetname: 'ATOM', value: 2478 } }, + }, + }, +}; + +/** + * @param {{ + * give: bigint, + * want: bigint, + * giveUnit?: bigint, + * wantUnit?: bigint, + * }} cliOffer offer expressed in whole anchor/ist tokens + * @param {typeof vBankPetName} [pet] + * @returns {typeof observedSpendAction.data.proposalTemplate} + */ +const makeBuyISTProposalTemplate = (cliOffer, pet = vBankPetName) => { + const { + give, + want, + giveUnit = COSMOS_UNIT, + wantUnit = COSMOS_UNIT, + } = cliOffer; + return { + // NOTE: proposalTemplate uses Number rather than bigint + // presumably to avoid JSON problems + give: { + In: { pursePetname: pet.purse.anchor, value: Number(give * giveUnit) }, + }, + want: { + Out: { pursePetname: pet.purse.ist, value: Number(want * wantUnit) }, + }, + }; +}; + +/** + * @param {{ give: bigint, want: bigint }} cliProposal + * @param {string} boardId of PSM instance + * @param {number} timeStamp + * @returns {typeof observedSpendAction} + */ +const makeBuyISTSpendAction = ({ give, want }, boardId, timeStamp) => { + const origin = 'unknown'; // we're not in a web origin + const method = 'makeSwapInvitation'; // ref psm.js + const proposalTemplate = makeBuyISTProposalTemplate({ give, want }); + + // cribbed from ScopedBridge.js#L49-L61 + // https://github.com/Agoric/agoric-sdk/blob/master/packages/wallet/ui/src/service/ScopedBridge.js#L49-L61 + const id = `${timeStamp}`; + const offer = { + id, + instancePetname: `instance@${boardId}`, + requestContext: { dappOrigin: origin, origin }, + meta: { id, creationStamp: timeStamp }, + status: 'proposed', + invitationMaker: { method }, + instanceHandleBoardId: boardId, + proposalTemplate, + }; + + const spendAction = { + type: 'acceptOffer', + data: offer, + }; + return spendAction; +}; + +const vstorage = { + url: (path = 'published', { kind = 'children' } = {}) => + `${RPC_BASE}/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=0`, + decode: ({ + result: { + response: { value }, + }, + }) => atob(value), +}; + +const miniMarshal = (slotToVal = (s, i) => s) => ({ + unserialze: ({ body, slots }) => { + const reviver = (_key, obj) => { + const qclass = obj !== null && typeof obj === 'object' && obj['@qclass']; + // NOTE: hilbert hotel not impl + switch (qclass) { + case 'slot': { + const { index, iface } = obj; + return slotToVal(slots[index], iface); + } + case 'bigint': + return BigInt(obj.digits); + case 'undefined': + return undefined; + default: + return obj; + } + }; + return JSON.parse(body, reviver); + }, +}); + +const storageNode = { + /** @param { string } txt */ + parseCapData: txt => { + /** @type {{ value: string }} */ + const { value } = JSON.parse(txt); + /** @type {{ body: string, slots: string[] }} */ + return JSON.parse(value); + }, + /** @param { string } txt */ + parseEntries: txt => { + const { body, slots } = storageNode.parseCapData(txt); + const entries = JSON.parse(body); + return Object.fromEntries( + entries.map(([name, { index }]) => [name, slots[index]]), + ); + }, +}; + +/** + * @param {object} io + * @param {typeof fetch} io.fetch + * @param {boolean} [io.verbose] + */ +const lookupPSMInstance = async ({ fetch, verbose }) => { + // console.log({ fetch }); + /** @param {string} url */ + const getJSON = async url => (await fetch(url)).json(); + + if (verbose) { + const status = await getJSON(`${RPC_BASE}/status?`); + console.log({ status }); + const top = await getJSON(vstorage.url()); + console.error('vstorage published.*', vstorage.decode(top)); + } + + const instanceRaw = await getJSON( + vstorage.url('published.agoricNames.instance', { kind: 'data' }), + ); + const instance = storageNode.parseEntries(vstorage.decode(instanceRaw)); + return instance.psm; +}; + +/** + * @param {string} addr + * @param {object} io + * @param {typeof fetch} io.fetch + */ +const getOfferState = async (addr, { fetch }) => { + /** @param {string} url */ + const getJSON = async url => (await fetch(url)).json(); + + const instanceRaw = await getJSON( + vstorage.url(`published.wallet.${addr}`, { kind: 'data' }), + ); + const txt = vstorage.decode(instanceRaw); + const capData = storageNode.parseCapData(txt); + const state = miniMarshal().unserialze(capData); + const { offers } = state; + + console.log('wallet state', JSON.stringify(offers, null, 2)); + console.log(Object.keys(state)); + // throw? + console.error('@@@NOT IMPL'); +}; + +/** + * @param {string[]} argv + * @param {string[]} [flagNames] options that don't take values + */ +const parseArgs = (argv, flagNames = []) => { + /** @type {string[]} */ + const args = []; + /** @type {Record} */ + const opts = {}; + /** @type {Record} */ + const flags = {}; + + let ix = 0; + while (ix < argv.length) { + const arg = argv[ix]; + if (arg.startsWith('--')) { + const opt = arg.slice('--'.length); + if (flagNames.includes(arg)) { + flags[opt] = true; + } else { + ix += 1; + const val = argv[ix]; + opts[opt] = val; + } + } else { + args.push(arg); + } + ix += 1; + } + return { args, opts, flags }; +}; + +/** + * @param {string[]} argv + * @param {object} io + * @param {typeof fetch} [io.fetch] + * @param {() => Date} io.clock + */ +const main = async (argv, { fetch, clock }) => { + const { opts, flags } = parseArgs(argv, ['--lookup', '--verbose']); + + if (flags.lookup) { + assert(fetch); + const boardId = await lookupPSMInstance({ fetch, verbose: flags.verbose }); + flags.verbose && console.error('psm instance board id', boardId); + console.info(boardId); + return 0; + } else if (opts.addr) { + assert(fetch); + await getOfferState(opts.addr, { fetch }); + return 0; + } + + if (!(opts.give && opts.want && opts.boardId)) { + console.error(USAGE); + return 1; + } + const [give, want] = [BigInt(opts.give), BigInt(opts.want)]; + + const spendAction = makeBuyISTSpendAction( + { give, want }, + opts.boardId, + clock().valueOf(), + ); + console.log(JSON.stringify(spendAction, null, 2)); + return 0; +}; + +main([...process.argv], { + // support pre-fetch node for some modes + fetch: typeof fetch === 'function' ? fetch : undefined, + clock: () => new Date(), +}).then( + code => process.exit(code), + err => console.error(err), +); From 93f97ee7c02b16ecdfda736ef83c3717648583b7 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sat, 20 Aug 2022 18:00:09 -0500 Subject: [PATCH 02/11] feat(psm-tool): getWalletState fixup: docs for --addr feat(psm-tool): show purse balances scaled, with petnames - refactor: vstorage.read, storageNode.unserialize, fromBoard feat(psm-tool): format offers nicely fix!(psm-tool): --addr => --wallet, --lookup => --contract - format fee ratios with asPercent() feat!(psm-tool): --wantStable, --giveStable, --feePct fix(psm-tool): include Fee when showing WantStableFee chore(psm-tool): updated PSM, vstorage APIs - PSM API: - split invitation making methods - published.psm.IST.AUSD.governance storage path - handle multi-valued vstorage - a bit of error handling in vstorage query - support local, ollinet, xnet networks - avoid assert module dependency - assert .at(-1) doesn't return undefined --- packages/inter-protocol/scripts/psm-tool.js | 532 ++++++++++++++++---- 1 file changed, 432 insertions(+), 100 deletions(-) mode change 100644 => 100755 packages/inter-protocol/scripts/psm-tool.js diff --git a/packages/inter-protocol/scripts/psm-tool.js b/packages/inter-protocol/scripts/psm-tool.js old mode 100644 new mode 100755 index 354a342c284..fe8214090c3 --- a/packages/inter-protocol/scripts/psm-tool.js +++ b/packages/inter-protocol/scripts/psm-tool.js @@ -2,26 +2,56 @@ // @ts-check /* global atob */ /* global process, fetch */ -import assert from 'assert'; -const RPC_BASE = 'https://xnet.rpc.agoric.net/'; +const networks = { + local: { rpc: 'http://0.0.0.0:26657', chainId: 'agoric' }, + xnet: { rpc: 'https://xnet.rpc.agoric.net:443', chainId: 'agoricxnet-13' }, + ollinet: { + rpc: 'https://ollinet.rpc.agoric.net:443', + chainId: 'agoricollinet-21', + }, +}; const USAGE = ` Usage: - psm-tool --lookup [--verbose] - psm-tool --give ANCHOR_TOKENS --want IST_TOKENS --boardId BOARD_ID +to get contract instance boardId and, optionally, fees + psm-tool --contract [--verbose] +to write an offer to stdout + psm-tool --wantStable ANCHOR_TOKENS --boardId BOARD_ID [--feePct PCT] + psm-tool --giveStable IST_TOKENS --boardId BOARD_ID [--feePct PCT] +to get succinct offer status and purse balances + psm-tool --wallet AGORIC_ADDRESS -NOTE: --lookup may need --experimental-fetch node option. +NOTE: --contract and --wallet may need --experimental-fetch node option. For example: -psmInstance=$(psm-tool --lookup) -psm-tool --give 101 --want 100 --boardId $psmInstance >,psm-offer-action.json -agd --node=${RPC_BASE} --chain-id=agoricxnet-13 \ +psmInstance=$(psm-tool --contract) +psm-tool --contract --verbose # to get fees +psm-tool --wantStable 100 --boardId $psmInstance --feePct 0.01 >,psm-offer-action.json + +# sign and send +agd --node=${networks.xnet.rpc} --chain-id=agoricxnet-13 \ --from=LEDGER_KEY_NAME --sign-mode=amino-json \ tx swingset wallet-action --allow-spend "$(cat ,psm-offer-action.json)" + +# check results +psm-tool --wallet agoric1.... `; +/** + * @param {unknown} cond + * @param {unknown} [msg] + */ +// @ts-expect-error agoric-sdk code presumes assert from ses +const assert = (cond, msg = undefined) => { + if (!cond) { + throw typeof msg === 'string' ? Error(msg || 'check failed') : msg; + } +}; + +const { freeze } = Object; // IOU harden + /** * Petnames depend on names issued by VBANK. */ @@ -34,6 +64,9 @@ const vBankPetName = { const COSMOS_UNIT = 1_000_000n; +const bigIntReplacer = (_key, val) => + typeof val === 'bigint' ? Number(val) : val; + // eslint-disable-next-line no-unused-vars const observedSpendAction = { type: 'acceptOffer', @@ -58,56 +91,63 @@ const observedSpendAction = { }; /** - * @param {{ - * give: bigint, - * want: bigint, - * giveUnit?: bigint, - * wantUnit?: bigint, - * }} cliOffer offer expressed in whole anchor/ist tokens + * @param {({ wantStable: string } | { giveStable: string })} opts + * @param {number} [fee=1] multiplier * @param {typeof vBankPetName} [pet] * @returns {typeof observedSpendAction.data.proposalTemplate} */ -const makeBuyISTProposalTemplate = (cliOffer, pet = vBankPetName) => { - const { - give, - want, - giveUnit = COSMOS_UNIT, - wantUnit = COSMOS_UNIT, - } = cliOffer; +const makePSMProposalTemplate = (opts, fee = 1, pet = vBankPetName) => { + const brand = + 'wantStable' in opts + ? { in: pet.purse.anchor, out: pet.purse.ist } + : { in: pet.purse.ist, out: pet.purse.anchor }; + // NOTE: proposalTemplate uses Number rather than bigint + // presumably to avoid JSON problems + const value = + Number('wantStable' in opts ? opts.wantStable : opts.giveStable) * + Number(COSMOS_UNIT); + const adjusted = { + in: Math.round('wantStable' in opts ? value * fee : value), + out: Math.round('giveStable' in opts ? value * fee : value), + }; return { - // NOTE: proposalTemplate uses Number rather than bigint - // presumably to avoid JSON problems give: { - In: { pursePetname: pet.purse.anchor, value: Number(give * giveUnit) }, + In: { pursePetname: brand.in, value: adjusted.in }, }, want: { - Out: { pursePetname: pet.purse.ist, value: Number(want * wantUnit) }, + Out: { pursePetname: brand.out, value: adjusted.out }, }, }; }; /** - * @param {{ give: bigint, want: bigint }} cliProposal - * @param {string} boardId of PSM instance + * @param {{ boardId: string, feePct?: string } & + * ({ wantStable: string } | { giveStable: string })} opts * @param {number} timeStamp * @returns {typeof observedSpendAction} */ -const makeBuyISTSpendAction = ({ give, want }, boardId, timeStamp) => { +const makePSMSpendAction = (opts, timeStamp) => { const origin = 'unknown'; // we're not in a web origin - const method = 'makeSwapInvitation'; // ref psm.js - const proposalTemplate = makeBuyISTProposalTemplate({ give, want }); + const method = + 'wantStable' in opts + ? 'makeWantStableInvitation' + : 'makeGiveStableInvitation'; // ref psm.js + const proposalTemplate = makePSMProposalTemplate( + opts, + opts.feePct ? 1 + Number(opts.feePct) / 100 : undefined, + ); // cribbed from ScopedBridge.js#L49-L61 // https://github.com/Agoric/agoric-sdk/blob/master/packages/wallet/ui/src/service/ScopedBridge.js#L49-L61 const id = `${timeStamp}`; const offer = { id, - instancePetname: `instance@${boardId}`, + instancePetname: `instance@${opts.boardId}`, requestContext: { dappOrigin: origin, origin }, meta: { id, creationStamp: timeStamp }, status: 'proposed', invitationMaker: { method }, - instanceHandleBoardId: boardId, + instanceHandleBoardId: opts.boardId, proposalTemplate, }; @@ -120,12 +160,23 @@ const makeBuyISTSpendAction = ({ give, want }, boardId, timeStamp) => { const vstorage = { url: (path = 'published', { kind = 'children' } = {}) => - `${RPC_BASE}/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=0`, - decode: ({ - result: { - response: { value }, - }, - }) => atob(value), + `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=0`, + decode: ({ result: { response } }) => { + const { code } = response; + if (code !== 0) { + throw response; + } + const { value } = response; + return atob(value); + }, + /** + * @param {string} path + * @param {(url: string) => Promise} getJSON + */ + read: async (path = 'published', getJSON) => { + const raw = await getJSON(vstorage.url(path, { kind: 'data' })); + return vstorage.decode(raw); + }, }; const miniMarshal = (slotToVal = (s, i) => s) => ({ @@ -150,69 +201,292 @@ const miniMarshal = (slotToVal = (s, i) => s) => ({ }, }); +const makeFromBoard = (slotKey = 'boardId') => { + const cache = new Map(); + const convertSlotToVal = (slot, iface) => { + if (cache.has(slot)) { + return cache.get(slot); + } + const val = freeze({ [slotKey]: slot, iface }); + cache.set(slot, val); + return val; + }; + return freeze({ convertSlotToVal }); +}; +/** @typedef {ReturnType} IdMap */ + const storageNode = { /** @param { string } txt */ parseCapData: txt => { + assert(typeof txt === 'string', typeof txt); /** @type {{ value: string }} */ const { value } = JSON.parse(txt); - /** @type {{ body: string, slots: string[] }} */ - return JSON.parse(value); + const specimen = JSON.parse(value); + // without blockHeight, it's the pre-vstreams style + /** @type {{ body: string, slots: string[] }[]} */ + const capDatas = + 'blockHeight' in specimen + ? specimen.values.map(s => JSON.parse(s)) + : [JSON.parse(specimen.value)]; + for (const capData of capDatas) { + try { + assert(typeof capData === 'object' && capData !== null, capData); + assert('body' in capData && 'slots' in capData, capData); + assert(typeof capData.body === 'string', capData); + assert(Array.isArray(capData.slots), capData); + } catch (err) { + console.error(JSON.stringify(['parseCapData', `${err}`, specimen])); + throw err; + } + } + return capDatas; }, - /** @param { string } txt */ - parseEntries: txt => { - const { body, slots } = storageNode.parseCapData(txt); - const entries = JSON.parse(body); - return Object.fromEntries( - entries.map(([name, { index }]) => [name, slots[index]]), + unserialize: (txt, ctx) => { + const capDatas = storageNode.parseCapData(txt); + return capDatas.map(capData => + miniMarshal(ctx.convertSlotToVal).unserialze(capData), ); }, }; /** - * @param {object} io - * @param {typeof fetch} io.fetch - * @param {boolean} [io.verbose] + * @template K, V + * @typedef {[key: K, val: V]} Entry */ -const lookupPSMInstance = async ({ fetch, verbose }) => { - // console.log({ fetch }); - /** @param {string} url */ - const getJSON = async url => (await fetch(url)).json(); - - if (verbose) { - const status = await getJSON(`${RPC_BASE}/status?`); - console.log({ status }); - const top = await getJSON(vstorage.url()); - console.error('vstorage published.*', vstorage.decode(top)); - } - const instanceRaw = await getJSON( - vstorage.url('published.agoricNames.instance', { kind: 'data' }), +/** + * @param {IdMap} ctx + * @param {(url: string) => Promise} getJSON + * @param {string[]} [kinds] + */ +const makeAgoricNames = async (ctx, getJSON, kinds = ['brand', 'instance']) => { + const entries = await Promise.all( + kinds.map(async kind => { + const content = await vstorage.read( + `published.agoricNames.${kind}`, + getJSON, + ); + const parts = storageNode.unserialize(content, ctx).at(-1); + + /** @type {Entry>} */ + const entry = [kind, Object.fromEntries(parts)]; + return entry; + }), ); - const instance = storageNode.parseEntries(vstorage.decode(instanceRaw)); - return instance.psm; + return Object.fromEntries(entries); +}; + +// eslint-disable-next-line no-unused-vars +const examplePurseState = { + brand: { + boardId: 'board0074', + iface: 'Alleged: IST brand', + }, + brandPetname: 'IST', + currentAmount: { + brand: { + kind: 'brand', + petname: 'IST', + }, + value: 125989900, + }, + displayInfo: { + assetKind: 'nat', + decimalPlaces: 6, + }, + pursePetname: 'Agoric stable local currency', +}; +/** @typedef {typeof examplePurseState} PurseState */ + +/** @param {PurseState[]} purses */ +const makeAmountFormatter = purses => amt => { + const { + brand: { petname }, + value, + } = amt; + const purse = purses.find(p => p.brandPetname === petname); + if (!purse) return [NaN, petname]; + const { + brandPetname, + displayInfo: { decimalPlaces }, + } = purse; + /** @type {[qty: number, petname: string]} */ + const scaled = [Number(value) / 10 ** decimalPlaces, brandPetname]; + return scaled; +}; + +const asPercent = ratio => { + const { numerator, denominator } = ratio; + assert(numerator.brand === denominator.brand); + return (100 * Number(numerator.value)) / Number(denominator.value); +}; + +/** @param {PurseState[]} purses */ +const simplePurseBalances = purses => { + const fmt = makeAmountFormatter(purses); + return purses.map(p => fmt(p.currentAmount)); }; +// eslint-disable-next-line no-unused-vars +const exampleOffer = { + id: 'unknown#1661035705180', + installationPetname: 'unnamed-2', + instanceHandleBoardId: 'board00530', + instancePetname: 'unnamed-1', + invitationDetails: { + description: 'swap', + handle: { + kind: 'unnamed', + petname: 'unnamed-5', + }, + installation: { + kind: 'unnamed', + petname: 'unnamed-2', + }, + instance: { + kind: 'unnamed', + petname: 'unnamed-1', + }, + }, + invitationMaker: { + method: 'makeSwapInvitation', + }, + meta: { + creationStamp: 1661035705180, + id: '1661035705180', + }, + proposalForDisplay: { + exit: { + onDemand: null, + }, + give: { + In: { + amount: { + brand: { + kind: 'brand', + petname: 'AUSD', + }, + displayInfo: { + assetKind: 'nat', + decimalPlaces: 6, + }, + value: 101000000, + }, + purse: { + boardId: 'unknown:10', + iface: 'Alleged: Virtual Purse', + }, + pursePetname: 'AUSD', + }, + }, + want: { + Out: { + amount: { + brand: { + kind: 'brand', + petname: 'IST', + }, + displayInfo: { + assetKind: 'nat', + decimalPlaces: 6, + }, + value: 100000000, + }, + purse: { + boardId: 'unknown:8', + iface: 'Alleged: Virtual Purse', + }, + pursePetname: 'Agoric stable local currency', + }, + }, + }, + proposalTemplate: { + give: { + In: { + pursePetname: 'AUSD', + value: 101000000, + }, + }, + want: { + Out: { + pursePetname: 'Agoric stable local currency', + value: 100000000, + }, + }, + }, + rawId: '1661035705180', + requestContext: { + dappOrigin: 'unknown', + origin: 'unknown', + }, + status: 'accept', +}; +/** @typedef {typeof exampleOffer} OfferDetail */ + +/** + * @param {{ purses: PurseState[], offers: OfferDetail[]}} state + * @param {Awaited>} agoricNames + */ +const simpleOffers = (state, agoricNames) => { + const { purses, offers } = state; + const fmt = makeAmountFormatter(purses); + const fmtRecord = r => + Object.fromEntries( + Object.entries(r).map(([kw, { amount }]) => [kw, fmt(amount)]), + ); + return offers.map(o => { + const { + // id, + meta, + instanceHandleBoardId, + invitationDetails: { description: invitationDescription }, + proposalForDisplay: { give, want }, + status, + } = o; + // console.log({ give: JSON.stringify(give), want: JSON.stringify(want) }); + const instanceEntry = Object.entries(agoricNames.instance).find( + ([_name, { boardId }]) => boardId === instanceHandleBoardId, + ); + const instanceName = instanceEntry + ? instanceEntry[0] + : instanceHandleBoardId; + return [ + // id, + meta?.creationStamp ? new Date(meta.creationStamp).toISOString() : null, + status, + instanceName, + invitationDescription, + { + give: fmtRecord(give), + want: fmtRecord(want), + }, + ]; + }); +}; + +const dieTrying = msg => { + throw Error(msg); +}; /** * @param {string} addr + * @param {IdMap} ctx * @param {object} io - * @param {typeof fetch} io.fetch + * @param {(url: string) => Promise} io.getJSON */ -const getOfferState = async (addr, { fetch }) => { - /** @param {string} url */ - const getJSON = async url => (await fetch(url)).json(); - - const instanceRaw = await getJSON( - vstorage.url(`published.wallet.${addr}`, { kind: 'data' }), - ); - const txt = vstorage.decode(instanceRaw); - const capData = storageNode.parseCapData(txt); - const state = miniMarshal().unserialze(capData); - const { offers } = state; - - console.log('wallet state', JSON.stringify(offers, null, 2)); - console.log(Object.keys(state)); - // throw? - console.error('@@@NOT IMPL'); +const getWalletState = async (addr, ctx, { getJSON }) => { + const txt = await vstorage.read(`published.wallet.${addr}`, getJSON); + /** @type {{ purses: PurseState[], offers: OfferDetail[] }[]} */ + const states = storageNode.unserialize(txt, ctx); + const offerById = new Map(); + states.forEach(state => { + const { offers } = state; + offers.forEach(offer => { + const { id } = offer; + offerById.set(id, offer); + }); + }); + const { purses } = states.at(-1) || dieTrying(); + return { purses, offers: [...offerById.values()] }; }; /** @@ -247,6 +521,76 @@ const parseArgs = (argv, flagNames = []) => { return { args, opts, flags }; }; +// const log = label => x => { +// console.error(label, x); +// return x; +// }; +const log = label => x => x; + +/** + * @param {{wallet?: string, net?: string}} opts + * @param {{contract?: boolean, verbose?: boolean}} flags + * @param {object} io + * @param {typeof fetch} io.fetch + */ +const online = async (opts, flags, { fetch }) => { + const net = networks[opts.net || 'local']; + assert(net, opts.net); + const getJSON = async url => (await fetch(log('url')(net.rpc + url))).json(); + + if (flags.verbose) { + // const status = await getJSON(`${RPC_BASE}/status?`); + // console.log({ status }); + const raw = await getJSON(vstorage.url()); + const top = vstorage.decode(raw); + console.error( + JSON.stringify(['vstorage published.*', JSON.parse(top).children]), + ); + } + + const fromBoard = makeFromBoard(); + const agoricNames = await makeAgoricNames(fromBoard, getJSON); + + if (flags.contract) { + const govContent = await vstorage.read( + 'published.psm.IST.AUSD.governance', + getJSON, + ); + const { current: governance } = storageNode + .unserialize(govContent, fromBoard) + .at(-1); + const { + instance: { psm: instance }, + } = agoricNames; + flags.verbose && console.error('psm', instance, Object.keys(governance)); + flags.verbose && + console.error( + 'WantStableFee', + asPercent(governance.WantStableFee.value), + '%', + 'GiveStableFee', + asPercent(governance.GiveStableFee.value), + '%', + ); + console.info(instance.boardId); + return 0; + } else if (opts.wallet) { + const state = await getWalletState(opts.wallet, fromBoard, { + getJSON, + }); + const { purses } = state; + // console.log(JSON.stringify(offers, null, 2)); + // console.log(JSON.stringify({ offers, purses }, bigIntReplacer, 2)); + console.log({ + balances: simplePurseBalances(purses).map(b => JSON.stringify(b)), + offers: simpleOffers(state, agoricNames).map(o => JSON.stringify(o)), + }); + return 0; + } + + return 1; +}; + /** * @param {string[]} argv * @param {object} io @@ -254,31 +598,19 @@ const parseArgs = (argv, flagNames = []) => { * @param {() => Date} io.clock */ const main = async (argv, { fetch, clock }) => { - const { opts, flags } = parseArgs(argv, ['--lookup', '--verbose']); + const { opts, flags } = parseArgs(argv, ['--contract', '--verbose']); - if (flags.lookup) { - assert(fetch); - const boardId = await lookupPSMInstance({ fetch, verbose: flags.verbose }); - flags.verbose && console.error('psm instance board id', boardId); - console.info(boardId); - return 0; - } else if (opts.addr) { - assert(fetch); - await getOfferState(opts.addr, { fetch }); - return 0; + if (flags.contract || opts.wallet) { + assert(fetch, 'missing fetch API; try --experimental-fetch?'); + return online(opts, flags, { fetch }); } - if (!(opts.give && opts.want && opts.boardId)) { + if (!((opts.wantStable || opts.giveStable) && opts.boardId)) { console.error(USAGE); return 1; } - const [give, want] = [BigInt(opts.give), BigInt(opts.want)]; - - const spendAction = makeBuyISTSpendAction( - { give, want }, - opts.boardId, - clock().valueOf(), - ); + // @ts-expect-error opts.boardId was tested above + const spendAction = makePSMSpendAction(opts, clock().valueOf()); console.log(JSON.stringify(spendAction, null, 2)); return 0; }; From 664de421faeaf08b24a56c3e17dc1e5c4da13d6a Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sun, 28 Aug 2022 13:20:00 -0500 Subject: [PATCH 03/11] build(psm-tool): new package --- packages/psm-tool/LICENSE | 201 ++++++++++++++++++++++++++++++++ packages/psm-tool/NEWS.md | 0 packages/psm-tool/README.md | 0 packages/psm-tool/SECURITY.md | 38 ++++++ packages/psm-tool/jsconfig.json | 11 ++ packages/psm-tool/package.json | 75 ++++++++++++ 6 files changed, 325 insertions(+) create mode 100644 packages/psm-tool/LICENSE create mode 100644 packages/psm-tool/NEWS.md create mode 100644 packages/psm-tool/README.md create mode 100644 packages/psm-tool/SECURITY.md create mode 100644 packages/psm-tool/jsconfig.json create mode 100644 packages/psm-tool/package.json diff --git a/packages/psm-tool/LICENSE b/packages/psm-tool/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/psm-tool/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/psm-tool/NEWS.md b/packages/psm-tool/NEWS.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/psm-tool/README.md b/packages/psm-tool/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/psm-tool/SECURITY.md b/packages/psm-tool/SECURITY.md new file mode 100644 index 00000000000..9365d51e125 --- /dev/null +++ b/packages/psm-tool/SECURITY.md @@ -0,0 +1,38 @@ +# Security Policy + +## Supported Versions + +The SES package and associated Endo packages are still undergoing development and security review, and all +users are encouraged to use the latest version available. Security fixes will +be made for the most recent branch only. + +## Coordinated Vulnerability Disclosure of Security Bugs + +SES stands for fearless cooperation, and strong security requires strong collaboration with security researchers. If you believe that you have found a security sensitive bug that should not be disclosed until a fix has been made available, we encourage you to report it. To report a bug in HardenedJS, you have several options that include: + +* Reporting the issue to the [Agoric HackerOne vulnerability rewards program](hackerone.com/agoric). + +* Sending an email to security at (@) agoric.com., encrypted or unencrypted. To encrypt, please use @Warner’s personal GPG key [A476E2E6 11880C98 5B3C3A39 0386E81B 11CAA07A](http://www.lothar.com/warner-gpg.html) . + +* Sending a message on Keybase to `@agoric_security`, or sharing code and other log files via Keybase’s encrypted file system. ((_keybase_private/agoric_security,$YOURNAME). + +* It is important to be able to provide steps that reproduce the issue and demonstrate its impact with a Proof of Concept example in an initial bug report. Before reporting a bug, a reporter may want to have another trusted individual reproduce the issue. + +* A bug reporter can expect acknowledgment of a potential vulnerability reported through [security@agoric.com](mailto:security@agoric.com) within one business day of submitting a report. If an acknowledgement of an issue is not received within this time frame, especially during a weekend or holiday period, please reach out again. Any issues reported to the HackerOne program will be acknowledged within the time frames posted on the program page. + * The bug triage team and Agoric code maintainers are primarily located in the San Francisco Bay Area with business hours in [Pacific Time](https://www.timeanddate.com/worldclock/usa/san-francisco) . + +* For the safety and security of those who depend on the code, bug reporters should avoid publicly sharing the details of a security bug on Twitter, Discord, Telegram, or in public Github issues during the coordination process. + +* Once a vulnerability report has been received and triaged: + * Agoric code maintainers will confirm whether it is valid, and will provide updates to the reporter on validity of the report. + * It may take up to 72 hours for an issue to be validated, especially if reported during holidays or on weekends. + +* When the Agoric team has verified an issue, remediation steps and patch release timeline information will be shared with the reporter. + * Complexity, severity, impact, and likelihood of exploitation are all vital factors that determine the amount of time required to remediate an issue and distribute a software patch. + * If an issue is Critical or High Severity, Agoric code maintainers will release a security advisory to notify impacted parties to prepare for an emergency patch. + * While the current industry standard for vulnerability coordination resolution is 90 days, Agoric code maintainers will strive to release a patch as quickly as possible. + +When a bug patch is included in a software release, the Agoric code maintainers will: + * Confirm the version and date of the software release with the reporter. + * Provide information about the security issue that the software release resolves. + * Credit the bug reporter for discovery by adding thanks in release notes, securing a CVE designation, or adding the researcher’s name to a Hall of Fame. diff --git a/packages/psm-tool/jsconfig.json b/packages/psm-tool/jsconfig.json new file mode 100644 index 00000000000..5d2fe839773 --- /dev/null +++ b/packages/psm-tool/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "noEmit": true, + "downlevelIteration": true, + "strictNullChecks": true, + "moduleResolution": "node" + }, + "include": ["**/*.js", "**/*.ts"] +} diff --git a/packages/psm-tool/package.json b/packages/psm-tool/package.json new file mode 100644 index 00000000000..765a2f78562 --- /dev/null +++ b/packages/psm-tool/package.json @@ -0,0 +1,75 @@ +{ + "name": "@agoric/psm-tool", + "version": "0.1.0", + "private": false, + "description": "Paritity Stability Module Tool.", + "keywords": [], + "author": "Agoric", + "license": "Apache-2.0", + "homepage": "https://github.com/Agoric/agoric-sdk/tree/master/packages/psm-tool#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/Agoric/agoric-sdk.git" + }, + "bugs": { + "url": "https://github.com/Agoric/agoric-sdk/issues" + }, + "type": "module", + "bin": { + "ag-psm-tool": "./scripts/psm-tool.js" + }, + "main": "./scripts/psm-tool.js", + "module": "./scripts/psm-tool.js", + "browser": null, + "unpkg": null, + "exports": { + ".": "./scripts/psm-tool.js", + "./package.json": "./package.json" + }, + "scripts": { + "build": "exit 0", + "lint": "yarn lint:types && yarn lint:js", + "lint-fix": "eslint --fix .", + "lint:js": "eslint .", + "lint:types": "tsc -p jsconfig.json", + "test": "ava" + }, + "dependencies": {}, + "devDependencies": { + "@endo/eslint-config": "^0.3.6", + "ava": "^3.12.1", + "babel-eslint": "^10.0.3", + "eslint": "^7.23.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-config-prettier": "^6.9.0", + "eslint-plugin-eslint-comments": "^3.1.2", + "eslint-plugin-import": "^2.19.1", + "eslint-plugin-prettier": "^3.4.1", + "prettier": "^1.19.1", + "typescript": "~4.6.2" + }, + "files": [ + "*.js", + "*.ts", + "LICENSE*", + "src" + ], + "publishConfig": { + "access": "public" + }, + "eslintConfig": { + "extends": [ + "@endo" + ] + }, + "prettier": { + "trailingComma": "all", + "singleQuote": true + }, + "ava": { + "files": [ + "test/**/test-*.js" + ], + "timeout": "2m" + } +} From a5eea8fbab8de64d569727106a6ca39409d66ac1 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Sun, 28 Aug 2022 13:20:53 -0500 Subject: [PATCH 04/11] chore(psm-tool): move out of packages/inter-protocol --- packages/{inter-protocol => psm-tool}/scripts/psm-tool.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/{inter-protocol => psm-tool}/scripts/psm-tool.js (100%) diff --git a/packages/inter-protocol/scripts/psm-tool.js b/packages/psm-tool/scripts/psm-tool.js similarity index 100% rename from packages/inter-protocol/scripts/psm-tool.js rename to packages/psm-tool/scripts/psm-tool.js From 0acf4fb2de9a3fec211cf7aba9233307a41848b3 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 1 Sep 2022 17:35:34 -0500 Subject: [PATCH 05/11] fix(psm-tool): JSON output chore(psm-tool): capData debugging fix(psm-tool): fee handling --- packages/psm-tool/scripts/psm-tool.js | 42 ++++++++++++++++----------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/psm-tool/scripts/psm-tool.js b/packages/psm-tool/scripts/psm-tool.js index fe8214090c3..908ccfb162b 100755 --- a/packages/psm-tool/scripts/psm-tool.js +++ b/packages/psm-tool/scripts/psm-tool.js @@ -92,7 +92,7 @@ const observedSpendAction = { /** * @param {({ wantStable: string } | { giveStable: string })} opts - * @param {number} [fee=1] multiplier + * @param {number} [fee=0] * @param {typeof vBankPetName} [pet] * @returns {typeof observedSpendAction.data.proposalTemplate} */ @@ -107,8 +107,8 @@ const makePSMProposalTemplate = (opts, fee = 1, pet = vBankPetName) => { Number('wantStable' in opts ? opts.wantStable : opts.giveStable) * Number(COSMOS_UNIT); const adjusted = { - in: Math.round('wantStable' in opts ? value * fee : value), - out: Math.round('giveStable' in opts ? value * fee : value), + in: Math.ceil('wantStable' in opts ? value / (1 - fee) : value), + out: Math.ceil('giveStable' in opts ? value * (1 - fee) : value), }; return { give: { @@ -134,7 +134,7 @@ const makePSMSpendAction = (opts, timeStamp) => { : 'makeGiveStableInvitation'; // ref psm.js const proposalTemplate = makePSMProposalTemplate( opts, - opts.feePct ? 1 + Number(opts.feePct) / 100 : undefined, + opts.feePct ? Number(opts.feePct) / 100 : undefined, ); // cribbed from ScopedBridge.js#L49-L61 @@ -229,15 +229,10 @@ const storageNode = { ? specimen.values.map(s => JSON.parse(s)) : [JSON.parse(specimen.value)]; for (const capData of capDatas) { - try { - assert(typeof capData === 'object' && capData !== null, capData); - assert('body' in capData && 'slots' in capData, capData); - assert(typeof capData.body === 'string', capData); - assert(Array.isArray(capData.slots), capData); - } catch (err) { - console.error(JSON.stringify(['parseCapData', `${err}`, specimen])); - throw err; - } + assert(typeof capData === 'object' && capData !== null, capData); + assert('body' in capData && 'slots' in capData, capData); + assert(typeof capData.body === 'string', capData); + assert(Array.isArray(capData.slots), capData); } return capDatas; }, @@ -527,6 +522,18 @@ const parseArgs = (argv, flagNames = []) => { // }; const log = label => x => x; +const fmtRecordOfLines = record => { + const { stringify } = JSON; + const groups = Object.entries(record).map(([key, items]) => [ + key, + items.map(item => ` ${stringify(item)}`), + ]); + const lineEntries = groups.map( + ([key, lines]) => ` ${stringify(key)}: [\n${lines.join(',\n')}\n ]`, + ); + return `{\n${lineEntries.join(',\n')}\n}`; +}; + /** * @param {{wallet?: string, net?: string}} opts * @param {{contract?: boolean, verbose?: boolean}} flags @@ -581,10 +588,11 @@ const online = async (opts, flags, { fetch }) => { const { purses } = state; // console.log(JSON.stringify(offers, null, 2)); // console.log(JSON.stringify({ offers, purses }, bigIntReplacer, 2)); - console.log({ - balances: simplePurseBalances(purses).map(b => JSON.stringify(b)), - offers: simpleOffers(state, agoricNames).map(o => JSON.stringify(o)), - }); + const summary = { + balances: simplePurseBalances(purses), + offers: simpleOffers(state, agoricNames), + }; + console.log(fmtRecordOfLines(summary)); return 0; } From 1b3e20f0c4ab8410c1630d46764a9fc5152c9886 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 1 Sep 2022 17:49:09 -0500 Subject: [PATCH 06/11] build(psm-tool): clasp: google apps script dev tool chore(psm-tool): sync with Google sheet chore(psm-tool): move psm-tool.js to src/ chore(psm-tool): split between script and lib chore(psm-tool): rename Code.js from google sheets refactor(psm-tool): move CLI details out of src/psm-lib.js chore(psm-tool): move Google apps scripts stuff to app/ fixup(psm-tool): duplicate assert in sheets-lib.js feat(psm-tool): simpleWalletState for use in Google Sheets - theWeb fix(psm-tool): avoid xs.last() in Google Apps Script also: - 1_000_000 - 123n build(psm-tool): build, clasp:push chore(psm-tool): ignore google apps scripts config --- packages/psm-tool/.gitignore | 1 + packages/psm-tool/app/appsscript.json | 7 + packages/psm-tool/app/psm-sheets-lib.js | 45 +++ packages/psm-tool/package.json | 4 +- packages/psm-tool/scripts/psm-tool.js | 468 +----------------------- packages/psm-tool/src/psm-lib.js | 462 +++++++++++++++++++++++ 6 files changed, 530 insertions(+), 457 deletions(-) create mode 100644 packages/psm-tool/.gitignore create mode 100644 packages/psm-tool/app/appsscript.json create mode 100644 packages/psm-tool/app/psm-sheets-lib.js create mode 100755 packages/psm-tool/src/psm-lib.js diff --git a/packages/psm-tool/.gitignore b/packages/psm-tool/.gitignore new file mode 100644 index 00000000000..a0e3405863a --- /dev/null +++ b/packages/psm-tool/.gitignore @@ -0,0 +1 @@ +app/.clasp.json diff --git a/packages/psm-tool/app/appsscript.json b/packages/psm-tool/app/appsscript.json new file mode 100644 index 00000000000..50abad2edb8 --- /dev/null +++ b/packages/psm-tool/app/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/Chicago", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/packages/psm-tool/app/psm-sheets-lib.js b/packages/psm-tool/app/psm-sheets-lib.js new file mode 100644 index 00000000000..b79cc409e62 --- /dev/null +++ b/packages/psm-tool/app/psm-sheets-lib.js @@ -0,0 +1,45 @@ +// https://developers.google.com/apps-script/reference/utilities/utilities?hl=en#base64Decode(String) + +// decode base64 to string +const atob = encoded => { + const decoded = Utilities.base64Decode(encoded); + return Utilities.newBlob(decoded).getDataAsString(); +}; + +/** WARNING: ambient */ +const theWeb = origin => { + const getJSON = url => { + const text = UrlFetchApp.fetch(origin + url); + return JSON.parse(text); + }; + return { getJSON }; +}; + +const fromBoard = makeFromBoard(); // WARNING: global mutable state + +const simpleWalletState = async (addr, origin) => { + const state = await getWalletState(addr, fromBoard, theWeb(origin)); + return [ + ...simplePurseBalances(state.purses).map(([val, brand]) => [ + 'purse', + val, + brand, + ]), + ...simpleOffers(state), + ]; +}; + +const testSimpleWalletState = async () => { + const simple = await simpleWalletState( + 'agoric1ggkanx6sv492tr3j2j4lf8kvlw9vtutwgr2fhn', + ); + console.log(simple); +}; + +function votingPower() { + const stuff = UrlFetchApp.fetch('https://ollinet.rpc.agoric.net/status?'); + const data = JSON.parse(stuff); + + console.log('is this thing on?', data); + return data.result.validator_info.voting_power; +} diff --git a/packages/psm-tool/package.json b/packages/psm-tool/package.json index 765a2f78562..ba62bc4921a 100644 --- a/packages/psm-tool/package.json +++ b/packages/psm-tool/package.json @@ -27,7 +27,8 @@ "./package.json": "./package.json" }, "scripts": { - "build": "exit 0", + "build": "mkdir -p app/build; sed 's/^export .*//' src/psm-lib.js >app/build/psm-lib.js", + "clasp:push": "yarn build && cd app; clasp push", "lint": "yarn lint:types && yarn lint:js", "lint-fix": "eslint --fix .", "lint:js": "eslint .", @@ -37,6 +38,7 @@ "dependencies": {}, "devDependencies": { "@endo/eslint-config": "^0.3.6", + "@google/clasp": "^2.4.1", "ava": "^3.12.1", "babel-eslint": "^10.0.3", "eslint": "^7.23.0", diff --git a/packages/psm-tool/scripts/psm-tool.js b/packages/psm-tool/scripts/psm-tool.js index 908ccfb162b..071fc58724f 100755 --- a/packages/psm-tool/scripts/psm-tool.js +++ b/packages/psm-tool/scripts/psm-tool.js @@ -1,16 +1,16 @@ #!/usr/bin/env node // @ts-check -/* global atob */ -/* global process, fetch */ - -const networks = { - local: { rpc: 'http://0.0.0.0:26657', chainId: 'agoric' }, - xnet: { rpc: 'https://xnet.rpc.agoric.net:443', chainId: 'agoricxnet-13' }, - ollinet: { - rpc: 'https://ollinet.rpc.agoric.net:443', - chainId: 'agoricollinet-21', - }, -}; +import { + assert, + asPercent, + getWalletState, + makeAgoricNames, + makeFromBoard, + makePSMSpendAction, + networks, + storageNode, + vstorage, +} from '../src/psm-lib.js'; const USAGE = ` Usage: @@ -39,451 +39,6 @@ agd --node=${networks.xnet.rpc} --chain-id=agoricxnet-13 \ psm-tool --wallet agoric1.... `; -/** - * @param {unknown} cond - * @param {unknown} [msg] - */ -// @ts-expect-error agoric-sdk code presumes assert from ses -const assert = (cond, msg = undefined) => { - if (!cond) { - throw typeof msg === 'string' ? Error(msg || 'check failed') : msg; - } -}; - -const { freeze } = Object; // IOU harden - -/** - * Petnames depend on names issued by VBANK. - */ -const vBankPetName = { - purse: { - anchor: 'AUSD', - ist: 'Agoric stable local currency', - }, -}; - -const COSMOS_UNIT = 1_000_000n; - -const bigIntReplacer = (_key, val) => - typeof val === 'bigint' ? Number(val) : val; - -// eslint-disable-next-line no-unused-vars -const observedSpendAction = { - type: 'acceptOffer', - data: { - id: '1661031322225', - instancePetname: 'instance@board03040', - requestContext: { - dappOrigin: 'https://amm.agoric.app', - origin: 'https://amm.agoric.app', - }, - meta: { id: '1661031322225', creationStamp: 1661031322225 }, - status: 'proposed', - instanceHandleBoardId: 'board03040', - invitationMaker: { method: 'makeSwapInInvitation' }, - proposalTemplate: { - give: { - In: { pursePetname: 'Agoric stable local currency', value: 5000000 }, - }, - want: { Out: { pursePetname: 'ATOM', value: 2478 } }, - }, - }, -}; - -/** - * @param {({ wantStable: string } | { giveStable: string })} opts - * @param {number} [fee=0] - * @param {typeof vBankPetName} [pet] - * @returns {typeof observedSpendAction.data.proposalTemplate} - */ -const makePSMProposalTemplate = (opts, fee = 1, pet = vBankPetName) => { - const brand = - 'wantStable' in opts - ? { in: pet.purse.anchor, out: pet.purse.ist } - : { in: pet.purse.ist, out: pet.purse.anchor }; - // NOTE: proposalTemplate uses Number rather than bigint - // presumably to avoid JSON problems - const value = - Number('wantStable' in opts ? opts.wantStable : opts.giveStable) * - Number(COSMOS_UNIT); - const adjusted = { - in: Math.ceil('wantStable' in opts ? value / (1 - fee) : value), - out: Math.ceil('giveStable' in opts ? value * (1 - fee) : value), - }; - return { - give: { - In: { pursePetname: brand.in, value: adjusted.in }, - }, - want: { - Out: { pursePetname: brand.out, value: adjusted.out }, - }, - }; -}; - -/** - * @param {{ boardId: string, feePct?: string } & - * ({ wantStable: string } | { giveStable: string })} opts - * @param {number} timeStamp - * @returns {typeof observedSpendAction} - */ -const makePSMSpendAction = (opts, timeStamp) => { - const origin = 'unknown'; // we're not in a web origin - const method = - 'wantStable' in opts - ? 'makeWantStableInvitation' - : 'makeGiveStableInvitation'; // ref psm.js - const proposalTemplate = makePSMProposalTemplate( - opts, - opts.feePct ? Number(opts.feePct) / 100 : undefined, - ); - - // cribbed from ScopedBridge.js#L49-L61 - // https://github.com/Agoric/agoric-sdk/blob/master/packages/wallet/ui/src/service/ScopedBridge.js#L49-L61 - const id = `${timeStamp}`; - const offer = { - id, - instancePetname: `instance@${opts.boardId}`, - requestContext: { dappOrigin: origin, origin }, - meta: { id, creationStamp: timeStamp }, - status: 'proposed', - invitationMaker: { method }, - instanceHandleBoardId: opts.boardId, - proposalTemplate, - }; - - const spendAction = { - type: 'acceptOffer', - data: offer, - }; - return spendAction; -}; - -const vstorage = { - url: (path = 'published', { kind = 'children' } = {}) => - `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=0`, - decode: ({ result: { response } }) => { - const { code } = response; - if (code !== 0) { - throw response; - } - const { value } = response; - return atob(value); - }, - /** - * @param {string} path - * @param {(url: string) => Promise} getJSON - */ - read: async (path = 'published', getJSON) => { - const raw = await getJSON(vstorage.url(path, { kind: 'data' })); - return vstorage.decode(raw); - }, -}; - -const miniMarshal = (slotToVal = (s, i) => s) => ({ - unserialze: ({ body, slots }) => { - const reviver = (_key, obj) => { - const qclass = obj !== null && typeof obj === 'object' && obj['@qclass']; - // NOTE: hilbert hotel not impl - switch (qclass) { - case 'slot': { - const { index, iface } = obj; - return slotToVal(slots[index], iface); - } - case 'bigint': - return BigInt(obj.digits); - case 'undefined': - return undefined; - default: - return obj; - } - }; - return JSON.parse(body, reviver); - }, -}); - -const makeFromBoard = (slotKey = 'boardId') => { - const cache = new Map(); - const convertSlotToVal = (slot, iface) => { - if (cache.has(slot)) { - return cache.get(slot); - } - const val = freeze({ [slotKey]: slot, iface }); - cache.set(slot, val); - return val; - }; - return freeze({ convertSlotToVal }); -}; -/** @typedef {ReturnType} IdMap */ - -const storageNode = { - /** @param { string } txt */ - parseCapData: txt => { - assert(typeof txt === 'string', typeof txt); - /** @type {{ value: string }} */ - const { value } = JSON.parse(txt); - const specimen = JSON.parse(value); - // without blockHeight, it's the pre-vstreams style - /** @type {{ body: string, slots: string[] }[]} */ - const capDatas = - 'blockHeight' in specimen - ? specimen.values.map(s => JSON.parse(s)) - : [JSON.parse(specimen.value)]; - for (const capData of capDatas) { - assert(typeof capData === 'object' && capData !== null, capData); - assert('body' in capData && 'slots' in capData, capData); - assert(typeof capData.body === 'string', capData); - assert(Array.isArray(capData.slots), capData); - } - return capDatas; - }, - unserialize: (txt, ctx) => { - const capDatas = storageNode.parseCapData(txt); - return capDatas.map(capData => - miniMarshal(ctx.convertSlotToVal).unserialze(capData), - ); - }, -}; - -/** - * @template K, V - * @typedef {[key: K, val: V]} Entry - */ - -/** - * @param {IdMap} ctx - * @param {(url: string) => Promise} getJSON - * @param {string[]} [kinds] - */ -const makeAgoricNames = async (ctx, getJSON, kinds = ['brand', 'instance']) => { - const entries = await Promise.all( - kinds.map(async kind => { - const content = await vstorage.read( - `published.agoricNames.${kind}`, - getJSON, - ); - const parts = storageNode.unserialize(content, ctx).at(-1); - - /** @type {Entry>} */ - const entry = [kind, Object.fromEntries(parts)]; - return entry; - }), - ); - return Object.fromEntries(entries); -}; - -// eslint-disable-next-line no-unused-vars -const examplePurseState = { - brand: { - boardId: 'board0074', - iface: 'Alleged: IST brand', - }, - brandPetname: 'IST', - currentAmount: { - brand: { - kind: 'brand', - petname: 'IST', - }, - value: 125989900, - }, - displayInfo: { - assetKind: 'nat', - decimalPlaces: 6, - }, - pursePetname: 'Agoric stable local currency', -}; -/** @typedef {typeof examplePurseState} PurseState */ - -/** @param {PurseState[]} purses */ -const makeAmountFormatter = purses => amt => { - const { - brand: { petname }, - value, - } = amt; - const purse = purses.find(p => p.brandPetname === petname); - if (!purse) return [NaN, petname]; - const { - brandPetname, - displayInfo: { decimalPlaces }, - } = purse; - /** @type {[qty: number, petname: string]} */ - const scaled = [Number(value) / 10 ** decimalPlaces, brandPetname]; - return scaled; -}; - -const asPercent = ratio => { - const { numerator, denominator } = ratio; - assert(numerator.brand === denominator.brand); - return (100 * Number(numerator.value)) / Number(denominator.value); -}; - -/** @param {PurseState[]} purses */ -const simplePurseBalances = purses => { - const fmt = makeAmountFormatter(purses); - return purses.map(p => fmt(p.currentAmount)); -}; - -// eslint-disable-next-line no-unused-vars -const exampleOffer = { - id: 'unknown#1661035705180', - installationPetname: 'unnamed-2', - instanceHandleBoardId: 'board00530', - instancePetname: 'unnamed-1', - invitationDetails: { - description: 'swap', - handle: { - kind: 'unnamed', - petname: 'unnamed-5', - }, - installation: { - kind: 'unnamed', - petname: 'unnamed-2', - }, - instance: { - kind: 'unnamed', - petname: 'unnamed-1', - }, - }, - invitationMaker: { - method: 'makeSwapInvitation', - }, - meta: { - creationStamp: 1661035705180, - id: '1661035705180', - }, - proposalForDisplay: { - exit: { - onDemand: null, - }, - give: { - In: { - amount: { - brand: { - kind: 'brand', - petname: 'AUSD', - }, - displayInfo: { - assetKind: 'nat', - decimalPlaces: 6, - }, - value: 101000000, - }, - purse: { - boardId: 'unknown:10', - iface: 'Alleged: Virtual Purse', - }, - pursePetname: 'AUSD', - }, - }, - want: { - Out: { - amount: { - brand: { - kind: 'brand', - petname: 'IST', - }, - displayInfo: { - assetKind: 'nat', - decimalPlaces: 6, - }, - value: 100000000, - }, - purse: { - boardId: 'unknown:8', - iface: 'Alleged: Virtual Purse', - }, - pursePetname: 'Agoric stable local currency', - }, - }, - }, - proposalTemplate: { - give: { - In: { - pursePetname: 'AUSD', - value: 101000000, - }, - }, - want: { - Out: { - pursePetname: 'Agoric stable local currency', - value: 100000000, - }, - }, - }, - rawId: '1661035705180', - requestContext: { - dappOrigin: 'unknown', - origin: 'unknown', - }, - status: 'accept', -}; -/** @typedef {typeof exampleOffer} OfferDetail */ - -/** - * @param {{ purses: PurseState[], offers: OfferDetail[]}} state - * @param {Awaited>} agoricNames - */ -const simpleOffers = (state, agoricNames) => { - const { purses, offers } = state; - const fmt = makeAmountFormatter(purses); - const fmtRecord = r => - Object.fromEntries( - Object.entries(r).map(([kw, { amount }]) => [kw, fmt(amount)]), - ); - return offers.map(o => { - const { - // id, - meta, - instanceHandleBoardId, - invitationDetails: { description: invitationDescription }, - proposalForDisplay: { give, want }, - status, - } = o; - // console.log({ give: JSON.stringify(give), want: JSON.stringify(want) }); - const instanceEntry = Object.entries(agoricNames.instance).find( - ([_name, { boardId }]) => boardId === instanceHandleBoardId, - ); - const instanceName = instanceEntry - ? instanceEntry[0] - : instanceHandleBoardId; - return [ - // id, - meta?.creationStamp ? new Date(meta.creationStamp).toISOString() : null, - status, - instanceName, - invitationDescription, - { - give: fmtRecord(give), - want: fmtRecord(want), - }, - ]; - }); -}; - -const dieTrying = msg => { - throw Error(msg); -}; -/** - * @param {string} addr - * @param {IdMap} ctx - * @param {object} io - * @param {(url: string) => Promise} io.getJSON - */ -const getWalletState = async (addr, ctx, { getJSON }) => { - const txt = await vstorage.read(`published.wallet.${addr}`, getJSON); - /** @type {{ purses: PurseState[], offers: OfferDetail[] }[]} */ - const states = storageNode.unserialize(txt, ctx); - const offerById = new Map(); - states.forEach(state => { - const { offers } = state; - offers.forEach(offer => { - const { id } = offer; - offerById.set(id, offer); - }); - }); - const { purses } = states.at(-1) || dieTrying(); - return { purses, offers: [...offerById.values()] }; -}; - /** * @param {string[]} argv * @param {string[]} [flagNames] options that don't take values @@ -610,6 +165,7 @@ const main = async (argv, { fetch, clock }) => { if (flags.contract || opts.wallet) { assert(fetch, 'missing fetch API; try --experimental-fetch?'); + // @ts-ignore what's up with typeof fetch??? return online(opts, flags, { fetch }); } diff --git a/packages/psm-tool/src/psm-lib.js b/packages/psm-tool/src/psm-lib.js new file mode 100755 index 00000000000..d04146821a7 --- /dev/null +++ b/packages/psm-tool/src/psm-lib.js @@ -0,0 +1,462 @@ +// @ts-check +/* global atob */ + +const networks = { + local: { rpc: 'http://0.0.0.0:26657', chainId: 'agoric' }, + xnet: { rpc: 'https://xnet.rpc.agoric.net:443', chainId: 'agoricxnet-13' }, + ollinet: { + rpc: 'https://ollinet.rpc.agoric.net:443', + chainId: 'agoricollinet-21', + }, +}; + +/** + * @param {unknown} cond + * @param {unknown} [msg] + */ +const assert = (cond, msg = undefined) => { + if (!cond) { + throw typeof msg === 'string' ? Error(msg || 'check failed') : msg; + } +}; + +const { freeze } = Object; // IOU harden + +/** + * Petnames depend on names issued by VBANK. + */ +const vBankPetName = { + purse: { + anchor: 'AUSD', + ist: 'Agoric stable local currency', + }, +}; + +const COSMOS_UNIT = BigInt(1000000); + +const bigIntReplacer = (_key, val) => + typeof val === 'bigint' ? Number(val) : val; + +// eslint-disable-next-line no-unused-vars +const observedSpendAction = { + type: 'acceptOffer', + data: { + id: '1661031322225', + instancePetname: 'instance@board03040', + requestContext: { + dappOrigin: 'https://amm.agoric.app', + origin: 'https://amm.agoric.app', + }, + meta: { id: '1661031322225', creationStamp: 1661031322225 }, + status: 'proposed', + instanceHandleBoardId: 'board03040', + invitationMaker: { method: 'makeSwapInInvitation' }, + proposalTemplate: { + give: { + In: { pursePetname: 'Agoric stable local currency', value: 5000000 }, + }, + want: { Out: { pursePetname: 'ATOM', value: 2478 } }, + }, + }, +}; + +/** + * @param {({ wantStable: string } | { giveStable: string })} opts + * @param {number} [fee=0] + * @param {typeof vBankPetName} [pet] + * @returns {typeof observedSpendAction.data.proposalTemplate} + */ +const makePSMProposalTemplate = (opts, fee = 1, pet = vBankPetName) => { + const brand = + 'wantStable' in opts + ? { in: pet.purse.anchor, out: pet.purse.ist } + : { in: pet.purse.ist, out: pet.purse.anchor }; + // NOTE: proposalTemplate uses Number rather than bigint + // presumably to avoid JSON problems + const value = + Number('wantStable' in opts ? opts.wantStable : opts.giveStable) * + Number(COSMOS_UNIT); + const adjusted = { + in: Math.ceil('wantStable' in opts ? value / (1 - fee) : value), + out: Math.ceil('giveStable' in opts ? value * (1 - fee) : value), + }; + return { + give: { + In: { pursePetname: brand.in, value: adjusted.in }, + }, + want: { + Out: { pursePetname: brand.out, value: adjusted.out }, + }, + }; +}; + +/** + * @param {{ boardId: string, feePct?: string } & + * ({ wantStable: string } | { giveStable: string })} opts + * @param {number} timeStamp + * @returns {typeof observedSpendAction} + */ +const makePSMSpendAction = (opts, timeStamp) => { + const origin = 'unknown'; // we're not in a web origin + const method = + 'wantStable' in opts + ? 'makeWantStableInvitation' + : 'makeGiveStableInvitation'; // ref psm.js + const proposalTemplate = makePSMProposalTemplate( + opts, + opts.feePct ? Number(opts.feePct) / 100 : undefined, + ); + + // cribbed from ScopedBridge.js#L49-L61 + // https://github.com/Agoric/agoric-sdk/blob/master/packages/wallet/ui/src/service/ScopedBridge.js#L49-L61 + const id = `${timeStamp}`; + const offer = { + id, + instancePetname: `instance@${opts.boardId}`, + requestContext: { dappOrigin: origin, origin }, + meta: { id, creationStamp: timeStamp }, + status: 'proposed', + invitationMaker: { method }, + instanceHandleBoardId: opts.boardId, + proposalTemplate, + }; + + const spendAction = { + type: 'acceptOffer', + data: offer, + }; + return spendAction; +}; + +const vstorage = { + url: (path = 'published', { kind = 'children' } = {}) => + `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=0`, + decode: ({ result: { response } }) => { + const { code } = response; + if (code !== 0) { + throw response; + } + const { value } = response; + return atob(value); + }, + /** + * @param {string} path + * @param {(url: string) => Promise} getJSON + */ + read: async (path = 'published', getJSON) => { + const raw = await getJSON(vstorage.url(path, { kind: 'data' })); + return vstorage.decode(raw); + }, +}; + +const miniMarshal = (slotToVal = (s, i) => s) => ({ + unserialze: ({ body, slots }) => { + const reviver = (_key, obj) => { + const qclass = obj !== null && typeof obj === 'object' && obj['@qclass']; + // NOTE: hilbert hotel not impl + switch (qclass) { + case 'slot': { + const { index, iface } = obj; + return slotToVal(slots[index], iface); + } + case 'bigint': + return BigInt(obj.digits); + case 'undefined': + return undefined; + default: + return obj; + } + }; + return JSON.parse(body, reviver); + }, +}); + +const makeFromBoard = (slotKey = 'boardId') => { + const cache = new Map(); + const convertSlotToVal = (slot, iface) => { + if (cache.has(slot)) { + return cache.get(slot); + } + const val = freeze({ [slotKey]: slot, iface }); + cache.set(slot, val); + return val; + }; + return freeze({ convertSlotToVal }); +}; +/** @typedef {ReturnType} IdMap */ + +const storageNode = { + /** @param { string } txt */ + parseCapData: txt => { + assert(typeof txt === 'string', typeof txt); + /** @type {{ value: string }} */ + const { value } = JSON.parse(txt); + const specimen = JSON.parse(value); + // without blockHeight, it's the pre-vstreams style + /** @type {{ body: string, slots: string[] }[]} */ + const capDatas = + 'blockHeight' in specimen + ? specimen.values.map(s => JSON.parse(s)) + : [JSON.parse(specimen.value)]; + for (const capData of capDatas) { + assert(typeof capData === 'object' && capData !== null, capData); + assert('body' in capData && 'slots' in capData, capData); + assert(typeof capData.body === 'string', capData); + assert(Array.isArray(capData.slots), capData); + } + return capDatas; + }, + unserialize: (txt, ctx) => { + const capDatas = storageNode.parseCapData(txt); + return capDatas.map(capData => + miniMarshal(ctx.convertSlotToVal).unserialze(capData), + ); + }, +}; + +/** + * @template K, V + * @typedef {[key: K, val: V]} Entry + */ + +const last = xs => xs[xs.length - 1]; + +/** + * @param {IdMap} ctx + * @param {(url: string) => Promise} getJSON + * @param {string[]} [kinds] + */ +const makeAgoricNames = async (ctx, getJSON, kinds = ['brand', 'instance']) => { + const entries = await Promise.all( + kinds.map(async kind => { + const content = await vstorage.read( + `published.agoricNames.${kind}`, + getJSON, + ); + const parts = last(storageNode.unserialize(content, ctx)); + + /** @type {Entry>} */ + const entry = [kind, Object.fromEntries(parts)]; + return entry; + }), + ); + return Object.fromEntries(entries); +}; + +// eslint-disable-next-line no-unused-vars +const examplePurseState = { + brand: { + boardId: 'board0074', + iface: 'Alleged: IST brand', + }, + brandPetname: 'IST', + currentAmount: { + brand: { + kind: 'brand', + petname: 'IST', + }, + value: 125989900, + }, + displayInfo: { + assetKind: 'nat', + decimalPlaces: 6, + }, + pursePetname: 'Agoric stable local currency', +}; +/** @typedef {typeof examplePurseState} PurseState */ + +/** @param {PurseState[]} purses */ +const makeAmountFormatter = purses => amt => { + const { + brand: { petname }, + value, + } = amt; + const purse = purses.find(p => p.brandPetname === petname); + if (!purse) return [NaN, petname]; + const { + brandPetname, + displayInfo: { decimalPlaces }, + } = purse; + /** @type {[qty: number, petname: string]} */ + const scaled = [Number(value) / 10 ** decimalPlaces, brandPetname]; + return scaled; +}; + +const asPercent = ratio => { + const { numerator, denominator } = ratio; + assert(numerator.brand === denominator.brand); + return (100 * Number(numerator.value)) / Number(denominator.value); +}; + +/** @param {PurseState[]} purses */ +const simplePurseBalances = purses => { + const fmt = makeAmountFormatter(purses); + return purses.map(p => fmt(p.currentAmount)); +}; + +// eslint-disable-next-line no-unused-vars +const exampleOffer = { + id: 'unknown#1661035705180', + installationPetname: 'unnamed-2', + instanceHandleBoardId: 'board00530', + instancePetname: 'unnamed-1', + invitationDetails: { + description: 'swap', + handle: { + kind: 'unnamed', + petname: 'unnamed-5', + }, + installation: { + kind: 'unnamed', + petname: 'unnamed-2', + }, + instance: { + kind: 'unnamed', + petname: 'unnamed-1', + }, + }, + invitationMaker: { + method: 'makeSwapInvitation', + }, + meta: { + creationStamp: 1661035705180, + id: '1661035705180', + }, + proposalForDisplay: { + exit: { + onDemand: null, + }, + give: { + In: { + amount: { + brand: { + kind: 'brand', + petname: 'AUSD', + }, + displayInfo: { + assetKind: 'nat', + decimalPlaces: 6, + }, + value: 101000000, + }, + purse: { + boardId: 'unknown:10', + iface: 'Alleged: Virtual Purse', + }, + pursePetname: 'AUSD', + }, + }, + want: { + Out: { + amount: { + brand: { + kind: 'brand', + petname: 'IST', + }, + displayInfo: { + assetKind: 'nat', + decimalPlaces: 6, + }, + value: 100000000, + }, + purse: { + boardId: 'unknown:8', + iface: 'Alleged: Virtual Purse', + }, + pursePetname: 'Agoric stable local currency', + }, + }, + }, + proposalTemplate: { + give: { + In: { + pursePetname: 'AUSD', + value: 101000000, + }, + }, + want: { + Out: { + pursePetname: 'Agoric stable local currency', + value: 100000000, + }, + }, + }, + rawId: '1661035705180', + requestContext: { + dappOrigin: 'unknown', + origin: 'unknown', + }, + status: 'accept', +}; +/** @typedef {typeof exampleOffer} OfferDetail */ + +/** + * @param {{ purses: PurseState[], offers: OfferDetail[]}} state + * @param {Awaited>} agoricNames + */ +const simpleOffers = (state, agoricNames) => { + const { purses, offers } = state; + const fmt = makeAmountFormatter(purses); + const fmtRecord = r => + Object.fromEntries( + Object.entries(r).map(([kw, { amount }]) => [kw, fmt(amount)]), + ); + return offers.map(o => { + const { + // id, + meta, + instanceHandleBoardId, + invitationDetails: { description: invitationDescription }, + proposalForDisplay: { give, want }, + status, + } = o; + // console.log({ give: JSON.stringify(give), want: JSON.stringify(want) }); + const instanceEntry = Object.entries(agoricNames.instance).find( + ([_name, { boardId }]) => boardId === instanceHandleBoardId, + ); + const instanceName = instanceEntry + ? instanceEntry[0] + : instanceHandleBoardId; + return [ + // id, + meta?.creationStamp ? new Date(meta.creationStamp).toISOString() : null, + status, + instanceName, + invitationDescription, + { + give: fmtRecord(give), + want: fmtRecord(want), + }, + ]; + }); +}; + +const dieTrying = msg => { + throw Error(msg); +}; +/** + * @param {string} addr + * @param {IdMap} ctx + * @param {object} io + * @param {(url: string) => Promise} io.getJSON + */ +const getWalletState = async (addr, ctx, { getJSON }) => { + const txt = await vstorage.read(`published.wallet.${addr}`, getJSON); + /** @type {{ purses: PurseState[], offers: OfferDetail[] }[]} */ + const states = storageNode.unserialize(txt, ctx); + const offerById = new Map(); + states.forEach(state => { + const { offers } = state; + offers.forEach(offer => { + const { id } = offer; + offerById.set(id, offer); + }); + }); + const { purses } = last(states) || dieTrying(); + return { purses, offers: [...offerById.values()] }; +}; + +// lines starting with 'export ' are stripped for use in Google Apps Scripts +export { assert, asPercent, getWalletState }; +export { makeAgoricNames, makeFromBoard, makePSMSpendAction }; +export { networks, storageNode, vstorage }; From 9eeaf0621c593f2bcbb91240d17ccc3474a7ebaa Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 1 Sep 2022 22:43:32 -0500 Subject: [PATCH 07/11] build(psm-tool): lint config, files --- packages/psm-tool/package.json | 37 ++++++++++------------------------ 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/packages/psm-tool/package.json b/packages/psm-tool/package.json index ba62bc4921a..4b10952f353 100644 --- a/packages/psm-tool/package.json +++ b/packages/psm-tool/package.json @@ -18,55 +18,40 @@ "bin": { "ag-psm-tool": "./scripts/psm-tool.js" }, - "main": "./scripts/psm-tool.js", - "module": "./scripts/psm-tool.js", + "main": "./src/psm-lib.js", + "module": "./src/psm-lib.js", "browser": null, "unpkg": null, "exports": { - ".": "./scripts/psm-tool.js", + ".": "./src/psm-lib.js", "./package.json": "./package.json" }, "scripts": { "build": "mkdir -p app/build; sed 's/^export .*//' src/psm-lib.js >app/build/psm-lib.js", "clasp:push": "yarn build && cd app; clasp push", - "lint": "yarn lint:types && yarn lint:js", - "lint-fix": "eslint --fix .", - "lint:js": "eslint .", - "lint:types": "tsc -p jsconfig.json", - "test": "ava" + "test": "ava", + "lint-fix": "yarn lint:eslint --fix", + "lint": "run-s --continue-on-error lint:*", + "lint:types": "tsc --maxNodeModuleJsDepth 4 -p jsconfig.json", + "lint:eslint": "eslint ." }, "dependencies": {}, "devDependencies": { - "@endo/eslint-config": "^0.3.6", "@google/clasp": "^2.4.1", - "ava": "^3.12.1", - "babel-eslint": "^10.0.3", - "eslint": "^7.23.0", - "eslint-config-airbnb-base": "^14.0.0", - "eslint-config-prettier": "^6.9.0", - "eslint-plugin-eslint-comments": "^3.1.2", - "eslint-plugin-import": "^2.19.1", - "eslint-plugin-prettier": "^3.4.1", - "prettier": "^1.19.1", - "typescript": "~4.6.2" + "ava": "^4.3.1" }, "files": [ "*.js", "*.ts", "LICENSE*", + "app", "src" ], "publishConfig": { "access": "public" }, "eslintConfig": { - "extends": [ - "@endo" - ] - }, - "prettier": { - "trailingComma": "all", - "singleQuote": true + "ignorePatterns": "build/*" }, "ava": { "files": [ From 29efcdc50e75664fa0e789ab9d7186678ba1e4c4 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Thu, 1 Sep 2022 22:23:18 -0500 Subject: [PATCH 08/11] feat(psm-tool): getContractEntries, offerAction for Google Sheets test(psm-tool): testFormatOffer apps script style(psm-tool): lint refactor(psm-tool): getContractState --- packages/psm-tool/app/psm-sheets-lib.js | 43 ++++++++++++++++++++++++- packages/psm-tool/scripts/psm-tool.js | 24 +++++++------- packages/psm-tool/src/psm-lib.js | 23 +++++++++++-- 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/packages/psm-tool/app/psm-sheets-lib.js b/packages/psm-tool/app/psm-sheets-lib.js index b79cc409e62..e04d12cfad8 100644 --- a/packages/psm-tool/app/psm-sheets-lib.js +++ b/packages/psm-tool/app/psm-sheets-lib.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ +/* global Utilities, UrlFetchApp */ // https://developers.google.com/apps-script/reference/utilities/utilities?hl=en#base64Decode(String) // decode base64 to string @@ -6,7 +8,11 @@ const atob = encoded => { return Utilities.newBlob(decoded).getDataAsString(); }; -/** WARNING: ambient */ +/** + * WARNING: ambient + * + * @param {string} origin + */ const theWeb = origin => { const getJSON = url => { const text = UrlFetchApp.fetch(origin + url); @@ -17,6 +23,27 @@ const theWeb = origin => { const fromBoard = makeFromBoard(); // WARNING: global mutable state +const getContractEntries = async origin => { + const { getJSON } = theWeb(origin); + const agoricNames = await makeAgoricNames(fromBoard, getJSON); + const { instance, governance: gov } = await getContractState( + fromBoard, + agoricNames, + { getJSON }, + ); + return Object.entries({ + boardId: instance.boardId, + WantStableFee: asPercent(gov.WantStableFee.value) / 100, + GiveStableFee: asPercent(gov.GiveStableFee.value) / 100, + }); +}; + +const testContract = async () => { + const origin = 'https://ollinet.rpc.agoric.net:443'; + const c = await getContractEntries(origin); + console.log(c); +}; + const simpleWalletState = async (addr, origin) => { const state = await getWalletState(addr, fromBoard, theWeb(origin)); return [ @@ -36,6 +63,20 @@ const testSimpleWalletState = async () => { console.log(simple); }; +const offerAction = (direction, qty, fee, boardId, indent = 0) => { + const id = Date.now(); + const offer = makePSMSpendAction( + { boardId, feePct: fee * 100, [direction]: qty }, + id, + ); + return JSON.stringify(offer, null, indent); +}; + +const testFormatOffer = () => { + const offer = offerAction('wantStable', 10, 0.0001, 'board123', 2); + console.log(offer); +}; + function votingPower() { const stuff = UrlFetchApp.fetch('https://ollinet.rpc.agoric.net/status?'); const data = JSON.parse(stuff); diff --git a/packages/psm-tool/scripts/psm-tool.js b/packages/psm-tool/scripts/psm-tool.js index 071fc58724f..2b048fa3816 100755 --- a/packages/psm-tool/scripts/psm-tool.js +++ b/packages/psm-tool/scripts/psm-tool.js @@ -1,14 +1,17 @@ #!/usr/bin/env node +/* global process, fetch */ // @ts-check import { assert, asPercent, + getContractState, getWalletState, makeAgoricNames, makeFromBoard, makePSMSpendAction, networks, - storageNode, + simpleOffers, + simplePurseBalances, vstorage, } from '../src/psm-lib.js'; @@ -75,7 +78,7 @@ const parseArgs = (argv, flagNames = []) => { // console.error(label, x); // return x; // }; -const log = label => x => x; +const log = _label => x => x; const fmtRecordOfLines = record => { const { stringify } = JSON; @@ -114,16 +117,13 @@ const online = async (opts, flags, { fetch }) => { const agoricNames = await makeAgoricNames(fromBoard, getJSON); if (flags.contract) { - const govContent = await vstorage.read( - 'published.psm.IST.AUSD.governance', - getJSON, + const { instance, governance } = await getContractState( + fromBoard, + agoricNames, + { + getJSON, + }, ); - const { current: governance } = storageNode - .unserialize(govContent, fromBoard) - .at(-1); - const { - instance: { psm: instance }, - } = agoricNames; flags.verbose && console.error('psm', instance, Object.keys(governance)); flags.verbose && console.error( @@ -165,7 +165,7 @@ const main = async (argv, { fetch, clock }) => { if (flags.contract || opts.wallet) { assert(fetch, 'missing fetch API; try --experimental-fetch?'); - // @ts-ignore what's up with typeof fetch??? + // @ts-expect-error what's up with typeof fetch??? return online(opts, flags, { fetch }); } diff --git a/packages/psm-tool/src/psm-lib.js b/packages/psm-tool/src/psm-lib.js index d04146821a7..42d66657e31 100755 --- a/packages/psm-tool/src/psm-lib.js +++ b/packages/psm-tool/src/psm-lib.js @@ -34,6 +34,7 @@ const vBankPetName = { const COSMOS_UNIT = BigInt(1000000); +// eslint-disable-next-line no-unused-vars const bigIntReplacer = (_key, val) => typeof val === 'bigint' ? Number(val) : val; @@ -149,7 +150,7 @@ const vstorage = { }, }; -const miniMarshal = (slotToVal = (s, i) => s) => ({ +const miniMarshal = (slotToVal = (s, _i) => s) => ({ unserialze: ({ body, slots }) => { const reviver = (_key, obj) => { const qclass = obj !== null && typeof obj === 'object' && obj['@qclass']; @@ -456,7 +457,23 @@ const getWalletState = async (addr, ctx, { getJSON }) => { return { purses, offers: [...offerById.values()] }; }; +const getContractState = async (fromBoard, agoricNames, { getJSON }) => { + const govContent = await vstorage.read( + 'published.psm.IST.AUSD.governance', + getJSON, + ); + const { current: governance } = last( + storageNode.unserialize(govContent, fromBoard), + ); + const { + instance: { psm: instance }, + } = agoricNames; + + return { instance, governance }; +}; + // lines starting with 'export ' are stripped for use in Google Apps Scripts -export { assert, asPercent, getWalletState }; +export { assert, asPercent }; +export { getContractState, getWalletState, simpleOffers, simplePurseBalances }; export { makeAgoricNames, makeFromBoard, makePSMSpendAction }; -export { networks, storageNode, vstorage }; +export { networks, vstorage }; From 14ea58fccb7f5e8f0562734506140afe48d082ec Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 2 Sep 2022 00:33:24 -0500 Subject: [PATCH 09/11] feat(psm-tool): format agd commands to execute PSM trade feat(psm-tool): updateClock to trigger wallet update --- packages/psm-tool/app/psm-sheets-lib.js | 26 +++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/psm-tool/app/psm-sheets-lib.js b/packages/psm-tool/app/psm-sheets-lib.js index e04d12cfad8..504cba04adb 100644 --- a/packages/psm-tool/app/psm-sheets-lib.js +++ b/packages/psm-tool/app/psm-sheets-lib.js @@ -45,14 +45,16 @@ const testContract = async () => { }; const simpleWalletState = async (addr, origin) => { - const state = await getWalletState(addr, fromBoard, theWeb(origin)); + const { getJSON } = theWeb(origin); + const state = await getWalletState(addr, fromBoard, { getJSON }); + const agoricNames = await makeAgoricNames(fromBoard, getJSON); return [ ...simplePurseBalances(state.purses).map(([val, brand]) => [ 'purse', val, brand, ]), - ...simpleOffers(state), + ...simpleOffers(state, agoricNames), ]; }; @@ -77,6 +79,20 @@ const testFormatOffer = () => { console.log(offer); }; +const formatCommands = (action, fromAddr, node, chainId) => { + const script = `read -r -d '' psmTrade <<'EOF' +${action} +EOF +agd tx swingset wallet-action --allow-spend "$psmTrade" --from ${fromAddr} --node=${node} --chain-id=${chainId} +`; + return script; +}; + +function testFormatCommands() { + const actual = formatCommands('{give}', 'agoric123', 'https://rpc'); + console.log(actual); +} + function votingPower() { const stuff = UrlFetchApp.fetch('https://ollinet.rpc.agoric.net/status?'); const data = JSON.parse(stuff); @@ -84,3 +100,9 @@ function votingPower() { console.log('is this thing on?', data); return data.result.validator_info.voting_power; } + +function updateClock(ref = 'A13') { + const sheet = SpreadsheetApp.getActiveSheet(); + const t = new Date(); + sheet.getRange(ref).setValue(t); +} From f6a70f6b4e6d8303b93529b7cf679ed44134a16c Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Tue, 6 Sep 2022 23:38:40 -0500 Subject: [PATCH 10/11] chore(psm-tool): use plain capData a la smart-wallet --- packages/psm-tool/scripts/psm-tool.js | 21 ++- packages/psm-tool/src/psm-lib.js | 237 +++++++++----------------- 2 files changed, 96 insertions(+), 162 deletions(-) diff --git a/packages/psm-tool/scripts/psm-tool.js b/packages/psm-tool/scripts/psm-tool.js index 2b048fa3816..95d181b8658 100755 --- a/packages/psm-tool/scripts/psm-tool.js +++ b/packages/psm-tool/scripts/psm-tool.js @@ -9,6 +9,7 @@ import { makeAgoricNames, makeFromBoard, makePSMSpendAction, + miniMarshal, networks, simpleOffers, simplePurseBalances, @@ -169,13 +170,25 @@ const main = async (argv, { fetch, clock }) => { return online(opts, flags, { fetch }); } - if (!((opts.wantStable || opts.giveStable) && opts.boardId)) { + if (!(opts.wantStable || opts.giveStable)) { console.error(USAGE); return 1; } - // @ts-expect-error opts.boardId was tested above - const spendAction = makePSMSpendAction(opts, clock().valueOf()); - console.log(JSON.stringify(spendAction, null, 2)); + + const fromBoard = makeFromBoard(); + const net = networks[opts.net || 'local']; + assert(net, opts.net); + const getJSON = async url => (await fetch(log('url')(net.rpc + url))).json(); + const agoricNames = await makeAgoricNames(fromBoard, getJSON); + const instance = agoricNames.instance.psm; + const spendAction = makePSMSpendAction( + instance, + agoricNames.brand, + // @ts-expect-error + opts, + clock().valueOf(), + ); + console.log(miniMarshal().serialize(spendAction)); return 0; }; diff --git a/packages/psm-tool/src/psm-lib.js b/packages/psm-tool/src/psm-lib.js index 42d66657e31..558d063a4c5 100755 --- a/packages/psm-tool/src/psm-lib.js +++ b/packages/psm-tool/src/psm-lib.js @@ -22,109 +22,95 @@ const assert = (cond, msg = undefined) => { const { freeze } = Object; // IOU harden -/** - * Petnames depend on names issued by VBANK. - */ -const vBankPetName = { - purse: { - anchor: 'AUSD', - ist: 'Agoric stable local currency', - }, -}; - const COSMOS_UNIT = BigInt(1000000); // eslint-disable-next-line no-unused-vars const bigIntReplacer = (_key, val) => typeof val === 'bigint' ? Number(val) : val; -// eslint-disable-next-line no-unused-vars -const observedSpendAction = { - type: 'acceptOffer', - data: { - id: '1661031322225', - instancePetname: 'instance@board03040', - requestContext: { - dappOrigin: 'https://amm.agoric.app', - origin: 'https://amm.agoric.app', - }, - meta: { id: '1661031322225', creationStamp: 1661031322225 }, - status: 'proposed', - instanceHandleBoardId: 'board03040', - invitationMaker: { method: 'makeSwapInInvitation' }, - proposalTemplate: { - give: { - In: { pursePetname: 'Agoric stable local currency', value: 5000000 }, - }, - want: { Out: { pursePetname: 'ATOM', value: 2478 } }, - }, - }, -}; +/** + * zoe/ERTP types + * + * @typedef {Record} AmountKeywordRecord + * @typedef {string} Keyword + * @typedef {Partial} Proposal + * + * @typedef {{give: AmountKeywordRecord, + * want: AmountKeywordRecord, + * exit: ExitRule + * }} ProposalRecord + * + * @typedef {unknown} ExitRule + */ + +/** @typedef {import('@agoric/smart-wallet/src/offers.js').OfferSpec} OfferSpec */ +/** @typedef {import('@agoric/smart-wallet/src/smartWallet.js').BridgeAction} BridgeAction */ +/** @template T @typedef {import('@agoric/smart-wallet/src/types.js').WalletCapData} WalletCapData */ /** + * @param {Record} brands * @param {({ wantStable: string } | { giveStable: string })} opts * @param {number} [fee=0] - * @param {typeof vBankPetName} [pet] - * @returns {typeof observedSpendAction.data.proposalTemplate} + * @param {string} [anchor] + * @returns {ProposalRecord} */ -const makePSMProposalTemplate = (opts, fee = 1, pet = vBankPetName) => { +const makePSMProposal = (brands, opts, fee = 0, anchor = 'AUSD') => { const brand = 'wantStable' in opts - ? { in: pet.purse.anchor, out: pet.purse.ist } - : { in: pet.purse.ist, out: pet.purse.anchor }; - // NOTE: proposalTemplate uses Number rather than bigint - // presumably to avoid JSON problems + ? { in: brands[anchor], out: brands.IST } + : { in: brands.IST, out: brands[anchor] }; const value = Number('wantStable' in opts ? opts.wantStable : opts.giveStable) * Number(COSMOS_UNIT); const adjusted = { - in: Math.ceil('wantStable' in opts ? value / (1 - fee) : value), - out: Math.ceil('giveStable' in opts ? value * (1 - fee) : value), + in: BigInt(Math.ceil('wantStable' in opts ? value / (1 - fee) : value)), + out: BigInt(Math.ceil('giveStable' in opts ? value * (1 - fee) : value)), }; return { give: { - In: { pursePetname: brand.in, value: adjusted.in }, + In: { brand: brand.in, value: adjusted.in }, }, want: { - Out: { pursePetname: brand.out, value: adjusted.out }, + Out: { brand: brand.out, value: adjusted.out }, }, + exit: {}, }; }; /** - * @param {{ boardId: string, feePct?: string } & + * @param {Record} brands + * @param {unknown} instance + * @param {{ feePct?: string } & * ({ wantStable: string } | { giveStable: string })} opts * @param {number} timeStamp - * @returns {typeof observedSpendAction} + * @returns {BridgeAction} */ -const makePSMSpendAction = (opts, timeStamp) => { - const origin = 'unknown'; // we're not in a web origin +const makePSMSpendAction = (instance, brands, opts, timeStamp) => { const method = 'wantStable' in opts ? 'makeWantStableInvitation' : 'makeGiveStableInvitation'; // ref psm.js - const proposalTemplate = makePSMProposalTemplate( + const proposal = makePSMProposal( + brands, opts, opts.feePct ? Number(opts.feePct) / 100 : undefined, ); - // cribbed from ScopedBridge.js#L49-L61 - // https://github.com/Agoric/agoric-sdk/blob/master/packages/wallet/ui/src/service/ScopedBridge.js#L49-L61 - const id = `${timeStamp}`; + /** @type {OfferSpec} */ const offer = { - id, - instancePetname: `instance@${opts.boardId}`, - requestContext: { dappOrigin: origin, origin }, - meta: { id, creationStamp: timeStamp }, - status: 'proposed', - invitationMaker: { method }, - instanceHandleBoardId: opts.boardId, - proposalTemplate, + id: timeStamp, + invitationSpec: { + source: 'contract', + instance, + publicInvitationMaker: method, + }, + proposal, }; + /** @type {BridgeAction} */ const spendAction = { - type: 'acceptOffer', - data: offer, + method: 'executeOffer', + offer, }; return spendAction; }; @@ -150,7 +136,7 @@ const vstorage = { }, }; -const miniMarshal = (slotToVal = (s, _i) => s) => ({ +export const miniMarshal = (slotToVal = (s, _i) => s) => ({ unserialze: ({ body, slots }) => { const reviver = (_key, obj) => { const qclass = obj !== null && typeof obj === 'object' && obj['@qclass']; @@ -170,6 +156,37 @@ const miniMarshal = (slotToVal = (s, _i) => s) => ({ }; return JSON.parse(body, reviver); }, + serialize: whole => { + const seen = new Map(); + const slotIndex = v => { + if (seen.has(v)) { + return seen.get(v); + } + const index = seen.size; + seen.set(v, index); + return { index, iface: v.iface }; + }; + const recur = part => { + if (part === null) return null; + if (typeof part === 'bigint') { + return { '@qclass': 'bigint', digits: `${part}` }; + } + if (Array.isArray(part)) { + return part.map(recur); + } + if (typeof part === 'object') { + if ('boardId' in part) { + return { '@qclass': 'slot', ...slotIndex(part.boardId) }; + } + return Object.fromEntries( + Object.entries(part).map(([k, v]) => [k, recur(v)]), + ); + } + return part; + }; + const after = recur(whole); + return { body: JSON.stringify(after), slots: [...seen.keys()] }; + }, }); const makeFromBoard = (slotKey = 'boardId') => { @@ -295,102 +312,6 @@ const simplePurseBalances = purses => { return purses.map(p => fmt(p.currentAmount)); }; -// eslint-disable-next-line no-unused-vars -const exampleOffer = { - id: 'unknown#1661035705180', - installationPetname: 'unnamed-2', - instanceHandleBoardId: 'board00530', - instancePetname: 'unnamed-1', - invitationDetails: { - description: 'swap', - handle: { - kind: 'unnamed', - petname: 'unnamed-5', - }, - installation: { - kind: 'unnamed', - petname: 'unnamed-2', - }, - instance: { - kind: 'unnamed', - petname: 'unnamed-1', - }, - }, - invitationMaker: { - method: 'makeSwapInvitation', - }, - meta: { - creationStamp: 1661035705180, - id: '1661035705180', - }, - proposalForDisplay: { - exit: { - onDemand: null, - }, - give: { - In: { - amount: { - brand: { - kind: 'brand', - petname: 'AUSD', - }, - displayInfo: { - assetKind: 'nat', - decimalPlaces: 6, - }, - value: 101000000, - }, - purse: { - boardId: 'unknown:10', - iface: 'Alleged: Virtual Purse', - }, - pursePetname: 'AUSD', - }, - }, - want: { - Out: { - amount: { - brand: { - kind: 'brand', - petname: 'IST', - }, - displayInfo: { - assetKind: 'nat', - decimalPlaces: 6, - }, - value: 100000000, - }, - purse: { - boardId: 'unknown:8', - iface: 'Alleged: Virtual Purse', - }, - pursePetname: 'Agoric stable local currency', - }, - }, - }, - proposalTemplate: { - give: { - In: { - pursePetname: 'AUSD', - value: 101000000, - }, - }, - want: { - Out: { - pursePetname: 'Agoric stable local currency', - value: 100000000, - }, - }, - }, - rawId: '1661035705180', - requestContext: { - dappOrigin: 'unknown', - origin: 'unknown', - }, - status: 'accept', -}; -/** @typedef {typeof exampleOffer} OfferDetail */ - /** * @param {{ purses: PurseState[], offers: OfferDetail[]}} state * @param {Awaited>} agoricNames From ab4c975eeb80afb06636bdbcb65a15aafb9c2120 Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Wed, 7 Sep 2022 00:22:52 -0500 Subject: [PATCH 11/11] build(psm-tool): pasteImports reconstructs single-file script - ignore Google Apps Scripts config --- packages/psm-tool/app/.npmignore | 1 + packages/psm-tool/package.json | 10 +++++-- packages/psm-tool/scripts/pasteImports.js | 33 +++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 packages/psm-tool/app/.npmignore create mode 100644 packages/psm-tool/scripts/pasteImports.js diff --git a/packages/psm-tool/app/.npmignore b/packages/psm-tool/app/.npmignore new file mode 100644 index 00000000000..48f089089fe --- /dev/null +++ b/packages/psm-tool/app/.npmignore @@ -0,0 +1 @@ +.clasp.json diff --git a/packages/psm-tool/package.json b/packages/psm-tool/package.json index 4b10952f353..e3684e75b48 100644 --- a/packages/psm-tool/package.json +++ b/packages/psm-tool/package.json @@ -27,7 +27,9 @@ "./package.json": "./package.json" }, "scripts": { - "build": "mkdir -p app/build; sed 's/^export .*//' src/psm-lib.js >app/build/psm-lib.js", + "build": "yarn build:script && yarn build:app", + "build:script": "mkdir -p dist; node scripts/pasteImports.js dist/psm-tool src/psm-lib.js scripts/psm-tool.js", + "build:app": "mkdir -p app/build; sed 's/^export .*//' src/psm-lib.js >app/build/psm-lib.js", "clasp:push": "yarn build && cd app; clasp push", "test": "ava", "lint-fix": "yarn lint:eslint --fix", @@ -45,13 +47,17 @@ "*.ts", "LICENSE*", "app", + "dist", "src" ], "publishConfig": { "access": "public" }, "eslintConfig": { - "ignorePatterns": "build/*" + "ignorePatterns": [ + "dist/*", + "build/*" + ] }, "ava": { "files": [ diff --git a/packages/psm-tool/scripts/pasteImports.js b/packages/psm-tool/scripts/pasteImports.js new file mode 100644 index 00000000000..e54f1c9d07e --- /dev/null +++ b/packages/psm-tool/scripts/pasteImports.js @@ -0,0 +1,33 @@ +// @ts-check + +const dieTrying = () => { + throw Error(); +}; + +/** + * + * @param {string[]} argv + * @param {object} io + * @param {typeof import('fs').promises.readFile} io.readFile + * @param {typeof import('fs').promises.writeFile} io.writeFile + */ +const main = async (argv, { readFile, writeFile }) => { + const dest = argv.shift() || dieTrying(); + const mainFn = argv.pop() || dieTrying(); + const parts = []; + for await (const fn of argv) { + const txt = await readFile(fn, 'utf-8'); + parts.push(txt.replace(/^export /gm, '')); + } + const txt = await readFile(mainFn, 'utf-8'); + const script = txt.replace(/import {[^}]*} from [^;]+;/m, parts.join('\n')); + + await writeFile(dest, script); + console.info('built', dest, 'from', mainFn, 'with', argv); +}; + +(async () => { + // eslint-disable-next-line no-undef + const { argv } = process; + import('fs').then(fs => main(argv.slice(2), fs.promises)); +})().catch(err => console.error(err));