diff --git a/packages/agoric-cli/src/commands/inter.js b/packages/agoric-cli/src/commands/inter.js index 9b0f6b65cc3..62dd0cd6246 100644 --- a/packages/agoric-cli/src/commands/inter.js +++ b/packages/agoric-cli/src/commands/inter.js @@ -10,6 +10,7 @@ import { bigintReplacer, getNetworkConfig, makeAmountFormatter, + makeWalletUtils, } from '@agoric/client-utils'; import { makeOfferSpecShape } from '@agoric/inter-protocol/src/auction/auctionBook.js'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; @@ -17,12 +18,7 @@ import { objectMap } from '@agoric/internal'; import { M, matches } from '@endo/patterns'; import { normalizeAddressWithOptions, pollBlocks } from '../lib/chain.js'; -import { - getCurrent, - makeWalletUtils, - outputActionAndHint, - sendAction, -} from '../lib/wallet.js'; +import { getCurrent, outputActionAndHint, sendAction } from '../lib/wallet.js'; const { values } = Object; @@ -238,7 +234,7 @@ export const makeInterCommand = ( // XXX pass fetch to getNetworkConfig() explicitly // await null above makes this await safe const networkConfig = await getNetworkConfig(env); - return makeWalletUtils({ fetch, execFileSync, delay }, networkConfig); + return makeWalletUtils({ fetch, delay }, networkConfig); } catch (err) { // CommanderError is a class constructor, and so // must be invoked with `new`. diff --git a/packages/agoric-cli/src/commands/oracle.js b/packages/agoric-cli/src/commands/oracle.js index 3873420cc73..c2679698a2a 100644 --- a/packages/agoric-cli/src/commands/oracle.js +++ b/packages/agoric-cli/src/commands/oracle.js @@ -5,6 +5,7 @@ import { bigintReplacer, getNetworkConfig, makeRpcUtils, + makeWalletUtils, storageHelper, } from '@agoric/client-utils'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; @@ -17,7 +18,6 @@ import { inspect } from 'util'; import { normalizeAddressWithOptions } from '../lib/chain.js'; import { getCurrent, - makeWalletUtils, outputAction, sendAction, sendHint, @@ -271,10 +271,7 @@ export const makeOracleCommand = (logger, io = {}) => { ) => { const { readLatestHead, networkConfig, lookupPriceAggregatorInstance } = await rpcTools(); - const wutil = await makeWalletUtils( - { fetch, execFileSync, delay }, - networkConfig, - ); + const wutil = await makeWalletUtils({ fetch, delay }, networkConfig); const unitPrice = scaleDecimals(price); const feedPath = `published.priceFeed.${pair[0]}-${pair[1]}_price_feed`; diff --git a/packages/agoric-cli/src/commands/test-upgrade.js b/packages/agoric-cli/src/commands/test-upgrade.js index b173e2991cd..1c05d611ba3 100644 --- a/packages/agoric-cli/src/commands/test-upgrade.js +++ b/packages/agoric-cli/src/commands/test-upgrade.js @@ -1,10 +1,14 @@ // @ts-check /* eslint-env node */ -import { bigintReplacer, getNetworkConfig } from '@agoric/client-utils'; +import { + bigintReplacer, + getNetworkConfig, + makeWalletUtils, +} from '@agoric/client-utils'; import { Fail } from '@endo/errors'; import { CommanderError } from 'commander'; import { normalizeAddressWithOptions } from '../lib/chain.js'; -import { makeWalletUtils, sendAction } from '../lib/wallet.js'; +import { sendAction } from '../lib/wallet.js'; /** * Make commands for testing. @@ -38,7 +42,7 @@ export const makeTestCommand = ( // XXX pass fetch to getNetworkConfig() explicitly // await null above makes this await safe const networkConfig = await getNetworkConfig(env); - return makeWalletUtils({ fetch, execFileSync, delay }, networkConfig); + return makeWalletUtils({ fetch, delay }, networkConfig); } catch (err) { // CommanderError is a class constructor, and so // must be invoked with `new`. diff --git a/packages/agoric-cli/src/lib/wallet.js b/packages/agoric-cli/src/lib/wallet.js index e4728597591..63ac4597373 100644 --- a/packages/agoric-cli/src/lib/wallet.js +++ b/packages/agoric-cli/src/lib/wallet.js @@ -2,10 +2,10 @@ /* eslint-env node */ import { iterateReverse } from '@agoric/casting'; -import { boardSlottingMarshaller, makeRpcUtils } from '@agoric/client-utils'; +import { boardSlottingMarshaller } from '@agoric/client-utils'; import { makeWalletStateCoalescer } from '@agoric/smart-wallet/src/utils.js'; import { Fail } from '@endo/errors'; -import { execSwingsetTransaction, pollBlocks, pollTx } from './chain.js'; +import { execSwingsetTransaction, pollTx } from './chain.js'; /** * @import {CurrentWalletRecord} from '@agoric/smart-wallet/src/smartWallet.js'; @@ -25,7 +25,7 @@ const emptyCurrentRecord = { /** * @param {string} addr - * @param {Pick} io + * @param {Pick} io * @returns {Promise} */ export const getCurrent = async (addr, { readLatestHead }) => { @@ -60,7 +60,7 @@ export const getCurrent = async (addr, { readLatestHead }) => { /** * @param {string} addr - * @param {Pick} io + * @param {Pick} io * @returns {Promise} */ export const getLastUpdate = (addr, { readLatestHead }) => { @@ -145,7 +145,7 @@ export const coalesceWalletState = async (follower, invitationBrand) => { * * @throws { Error & { code: number } } if transaction fails * @param {import('@agoric/smart-wallet/src/smartWallet.js').BridgeAction} bridgeAction - * @param {import('@agoric/client-utils').MinimalNetworkConfig & { + * @param {MinimalNetworkConfig & { * from: string, * fees?: string, * verbose?: boolean, @@ -214,77 +214,3 @@ export const findContinuingIds = (current, agoricNames) => { } return found; }; - -export const makeWalletUtils = async ( - { fetch, execFileSync, delay }, - networkConfig, -) => { - const { agoricNames, fromBoard, readLatestHead, vstorage } = - await makeRpcUtils({ fetch }, networkConfig); - /** - * @param {string} from - * @param {number|string} [minHeight] - */ - const storedWalletState = async (from, minHeight = undefined) => { - const m = boardSlottingMarshaller(fromBoard.convertSlotToVal); - - const history = await vstorage.readFully( - `published.wallet.${from}`, - minHeight, - ); - - /** @type {{ Invitation: Brand<'set'> }} */ - // @ts-expect-error XXX how to narrow AssetKind to set? - const { Invitation } = agoricNames.brand; - const coalescer = makeWalletStateCoalescer(Invitation); - // update with oldest first - for (const txt of history.reverse()) { - const { body, slots } = JSON.parse(txt); - const record = m.fromCapData({ body, slots }); - coalescer.update(record); - } - const coalesced = coalescer.state; - harden(coalesced); - return coalesced; - }; - - /** - * Get OfferStatus by id, polling until available. - * - * @param {string} from - * @param {string|number} id - * @param {number|string} minHeight - * @param {boolean} [untilNumWantsSatisfied] - */ - const pollOffer = async ( - from, - id, - minHeight, - untilNumWantsSatisfied = false, - ) => { - const lookup = async () => { - const { offerStatuses } = await storedWalletState(from, minHeight); - const offerStatus = [...offerStatuses.values()].find(s => s.id === id); - if (!offerStatus) throw Error('retry'); - harden(offerStatus); - if (untilNumWantsSatisfied && !('numWantsSatisfied' in offerStatus)) { - throw Error('retry (no numWantsSatisfied yet)'); - } - return offerStatus; - }; - const retryMessage = 'offer not in wallet at block'; - const opts = { ...networkConfig, execFileSync, delay, retryMessage }; - return pollBlocks(opts)(lookup); - }; - - return { - networkConfig, - agoricNames, - fromBoard, - vstorage, - readLatestHead, - storedWalletState, - pollOffer, - }; -}; -/** @typedef {Awaited>} WalletUtils */ diff --git a/packages/client-utils/src/chain.js b/packages/client-utils/src/chain.js new file mode 100644 index 00000000000..3b9caca79e3 --- /dev/null +++ b/packages/client-utils/src/chain.js @@ -0,0 +1,53 @@ +/** + * @import {MinimalNetworkConfig} from '@agoric/client-utils'; + */ + +import { StargateClient } from '@cosmjs/stargate'; + +/** + * @param {MinimalNetworkConfig} config + * @returns {Promise} + */ +export const makeStargateClient = async config => { + // TODO distribute load + const endpoint = config.rpcAddrs.at(-1); + assert(endpoint, 'no endpoints'); + + return StargateClient.connect(endpoint); +}; + +/** + * @param {{ + * client: StargateClient, + * delay: (ms: number) => Promise, + * period?: number, + * retryMessage?: string, + * }} opts + * @returns {(l: (b: { time: string, height: number }) => Promise) => Promise} + */ +export const pollBlocks = opts => async lookup => { + const { client, delay, period = 3 * 1000 } = opts; + const { retryMessage } = opts; + + await null; // separate sync prologue + + for (;;) { + const status = await client.getBlock(); + const { + header: { time, height }, + } = status; + try { + // see await null above + const result = await lookup({ time, height }); + return result; + } catch (_err) { + console.error( + time, + retryMessage || 'not in block', + height, + 'retrying...', + ); + await delay(period); + } + } +}; diff --git a/packages/client-utils/src/main.js b/packages/client-utils/src/main.js index dca5f6810db..4e0632a2045 100644 --- a/packages/client-utils/src/main.js +++ b/packages/client-utils/src/main.js @@ -1,2 +1,3 @@ export * from './format.js'; export * from './rpc.js'; +export * from './wallet-utils.js'; diff --git a/packages/client-utils/src/wallet-utils.js b/packages/client-utils/src/wallet-utils.js new file mode 100644 index 00000000000..9ad670093d8 --- /dev/null +++ b/packages/client-utils/src/wallet-utils.js @@ -0,0 +1,96 @@ +import { makeWalletStateCoalescer } from '@agoric/smart-wallet/src/utils.js'; +import { makeStargateClient, pollBlocks } from './chain.js'; +import { boardSlottingMarshaller, makeRpcUtils } from './rpc.js'; + +/** + * @import {Amount, Brand} from '@agoric/ertp/src/types.js' + */ + +export const makeWalletUtils = async ({ fetch, delay }, networkConfig) => { + const { agoricNames, fromBoard, marshaller, readLatestHead, vstorage } = + await makeRpcUtils({ fetch }, networkConfig); + + const client = await makeStargateClient(networkConfig); + + /** + * @param {string} from + * @param {number|string} [minHeight] + */ + const storedWalletState = async (from, minHeight = undefined) => { + const m = boardSlottingMarshaller(fromBoard.convertSlotToVal); + + const history = await vstorage.readFully( + `published.wallet.${from}`, + minHeight, + ); + + /** @type {{ Invitation: Brand<'set'> }} */ + // @ts-expect-error XXX how to narrow AssetKind to set? + const { Invitation } = agoricNames.brand; + const coalescer = makeWalletStateCoalescer(Invitation); + // update with oldest first + for (const txt of history.reverse()) { + const { body, slots } = JSON.parse(txt); + const record = m.fromCapData({ body, slots }); + coalescer.update(record); + } + const coalesced = coalescer.state; + harden(coalesced); + return coalesced; + }; + + /** + * Get OfferStatus by id, polling until available. + * + * @param {string} from + * @param {string|number} id + * @param {number|string} minHeight + * @param {boolean} [untilNumWantsSatisfied] + */ + const pollOffer = async ( + from, + id, + minHeight, + untilNumWantsSatisfied = false, + ) => { + const poll = pollBlocks({ + client, + delay, + retryMessage: 'offer not in wallet at block', + }); + + const lookup = async () => { + const { offerStatuses } = await storedWalletState(from, minHeight); + const offerStatus = [...offerStatuses.values()].find(s => s.id === id); + if (!offerStatus) throw Error('retry'); + harden(offerStatus); + if (untilNumWantsSatisfied && !('numWantsSatisfied' in offerStatus)) { + throw Error('retry (no numWantsSatisfied yet)'); + } + return offerStatus; + }; + return poll(lookup); + }; + + /** + * @param {string} addr + * @returns {Promise} + */ + const getLastUpdate = addr => { + // @ts-expect-error cast + return readLatestHead(`published.wallet.${addr}`); + }; + + return { + networkConfig, + agoricNames, + fromBoard, + marshaller, + vstorage, + getLastUpdate, + readLatestHead, + storedWalletState, + pollOffer, + }; +}; +/** @typedef {Awaited>} WalletUtils */