Skip to content

Commit

Permalink
feat: makeWalletUtils wo/spawn
Browse files Browse the repository at this point in the history
  • Loading branch information
turadg committed Nov 6, 2024
1 parent 8582373 commit 20083ae
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 94 deletions.
10 changes: 3 additions & 7 deletions packages/agoric-cli/src/commands/inter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,15 @@ 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';
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;

Expand Down Expand Up @@ -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`.
Expand Down
7 changes: 2 additions & 5 deletions packages/agoric-cli/src/commands/oracle.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
bigintReplacer,
getNetworkConfig,
makeRpcUtils,
makeWalletUtils,
storageHelper,
} from '@agoric/client-utils';
import { Offers } from '@agoric/inter-protocol/src/clientSupport.js';
Expand All @@ -17,7 +18,6 @@ import { inspect } from 'util';
import { normalizeAddressWithOptions } from '../lib/chain.js';
import {
getCurrent,
makeWalletUtils,
outputAction,
sendAction,
sendHint,
Expand Down Expand Up @@ -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`;
Expand Down
10 changes: 7 additions & 3 deletions packages/agoric-cli/src/commands/test-upgrade.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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`.
Expand Down
84 changes: 5 additions & 79 deletions packages/agoric-cli/src/lib/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,7 +25,7 @@ const emptyCurrentRecord = {

/**
* @param {string} addr
* @param {Pick<import('@agoric/client-utils').RpcUtils, 'readLatestHead'>} io
* @param {Pick<RpcUtils, 'readLatestHead'>} io
* @returns {Promise<import('@agoric/smart-wallet/src/smartWallet.js').CurrentWalletRecord>}
*/
export const getCurrent = async (addr, { readLatestHead }) => {
Expand Down Expand Up @@ -60,7 +60,7 @@ export const getCurrent = async (addr, { readLatestHead }) => {

/**
* @param {string} addr
* @param {Pick<import('@agoric/client-utils').RpcUtils, 'readLatestHead'>} io
* @param {Pick<RpcUtils, 'readLatestHead'>} io
* @returns {Promise<import('@agoric/smart-wallet/src/smartWallet.js').UpdateRecord>}
*/
export const getLastUpdate = (addr, { readLatestHead }) => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<ReturnType<typeof makeWalletUtils>>} WalletUtils */
53 changes: 53 additions & 0 deletions packages/client-utils/src/chain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @import {MinimalNetworkConfig} from '@agoric/client-utils';
*/

import { StargateClient } from '@cosmjs/stargate';

/**
* @param {MinimalNetworkConfig} config
* @returns {Promise<StargateClient>}
*/
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<void>,
* period?: number,
* retryMessage?: string,
* }} opts
* @returns {<T>(l: (b: { time: string, height: number }) => Promise<T>) => Promise<T>}
*/
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);
}
}
};
1 change: 1 addition & 0 deletions packages/client-utils/src/main.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './format.js';
export * from './rpc.js';
export * from './wallet-utils.js';
96 changes: 96 additions & 0 deletions packages/client-utils/src/wallet-utils.js
Original file line number Diff line number Diff line change
@@ -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<import('@agoric/smart-wallet/src/smartWallet.js').UpdateRecord>}
*/
const getLastUpdate = addr => {
// @ts-expect-error cast
return readLatestHead(`published.wallet.${addr}`);
};

return {
networkConfig,
agoricNames,
fromBoard,
marshaller,
vstorage,
getLastUpdate,
readLatestHead,
storedWalletState,
pollOffer,
};
};
/** @typedef {Awaited<ReturnType<typeof makeWalletUtils>>} WalletUtils */

0 comments on commit 20083ae

Please sign in to comment.