agoric 0.18.4 → 0.18.5-dev-800c2bb.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/rpc.js CHANGED
@@ -3,10 +3,12 @@
3
3
  /* global Buffer, fetch, process */
4
4
 
5
5
  import { NonNullish } from '@agoric/assert';
6
+ import {
7
+ boardSlottingMarshaller,
8
+ makeBoardRemote,
9
+ } from '@agoric/vats/tools/board-utils.js';
6
10
 
7
- /**
8
- * @typedef {{boardId: string, iface: string}} RpcRemote
9
- */
11
+ export { boardSlottingMarshaller };
10
12
 
11
13
  export const networkConfigUrl = agoricNetSubdomain =>
12
14
  `https://${agoricNetSubdomain}.agoric.net/network-config`;
@@ -29,29 +31,63 @@ const fromAgoricNet = str => {
29
31
  return fetch(networkConfigUrl(netName)).then(res => res.json());
30
32
  };
31
33
 
34
+ /**
35
+ * @param {typeof process.env} env
36
+ * @returns {Promise<MinimalNetworkConfig>}
37
+ */
38
+ export const getNetworkConfig = async env => {
39
+ if (!('AGORIC_NET' in env) || env.AGORIC_NET === 'local') {
40
+ return { rpcAddrs: ['http://0.0.0.0:26657'], chainName: 'agoriclocal' };
41
+ }
42
+
43
+ return fromAgoricNet(NonNullish(env.AGORIC_NET)).catch(err => {
44
+ throw Error(
45
+ `cannot get network config (${env.AGORIC_NET || 'local'}): ${
46
+ err.message
47
+ }`,
48
+ );
49
+ });
50
+ };
51
+
32
52
  /** @type {MinimalNetworkConfig} */
33
- export const networkConfig =
34
- 'AGORIC_NET' in process.env && process.env.AGORIC_NET !== 'local'
35
- ? await fromAgoricNet(NonNullish(process.env.AGORIC_NET))
36
- : { rpcAddrs: ['http://0.0.0.0:26657'], chainName: 'agoriclocal' };
53
+ export const networkConfig = await getNetworkConfig(process.env);
37
54
  // console.warn('networkConfig', networkConfig);
38
55
 
39
56
  /**
40
- *
41
57
  * @param {object} powers
42
58
  * @param {typeof window.fetch} powers.fetch
59
+ * @param {MinimalNetworkConfig} config
43
60
  */
44
- export const makeVStorage = powers => {
61
+ export const makeVStorage = (powers, config = networkConfig) => {
62
+ /** @param {string} path */
45
63
  const getJSON = path => {
46
- const url = networkConfig.rpcAddrs[0] + path;
64
+ const url = config.rpcAddrs[0] + path;
47
65
  // console.warn('fetching', url);
48
66
  return powers.fetch(url, { keepalive: true }).then(res => res.json());
49
67
  };
68
+ // height=0 is the same as omitting height and implies the highest block
69
+ const url = (path = 'published', { kind = 'children', height = 0 } = {}) =>
70
+ `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=${height}`;
71
+
72
+ const readStorage = (path = 'published', { kind = 'children', height = 0 }) =>
73
+ getJSON(url(path, { kind, height }))
74
+ .catch(err => {
75
+ throw Error(`cannot read ${kind} of ${path}: ${err.message}`);
76
+ })
77
+ .then(data => {
78
+ const {
79
+ result: { response },
80
+ } = data;
81
+ if (response?.code !== 0) {
82
+ throw Error(
83
+ `error code ${response?.code} reading ${kind} of ${path}: ${response.log}`,
84
+ );
85
+ }
86
+ return data;
87
+ });
50
88
 
51
89
  return {
52
- // height=0 is the same as omitting height and implies the highest block
53
- url: (path = 'published', { kind = 'children', height = 0 } = {}) =>
54
- `/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=${height}`,
90
+ url,
55
91
  decode({ result: { response } }) {
56
92
  const { code } = response;
57
93
  if (code !== 0) {
@@ -66,11 +102,11 @@ export const makeVStorage = powers => {
66
102
  * @returns {Promise<string>} latest vstorage value at path
67
103
  */
68
104
  async readLatest(path = 'published') {
69
- const raw = await getJSON(this.url(path, { kind: 'data' }));
105
+ const raw = await readStorage(path, { kind: 'data' });
70
106
  return this.decode(raw);
71
107
  },
72
108
  async keys(path = 'published') {
73
- const raw = await getJSON(this.url(path, { kind: 'children' }));
109
+ const raw = await readStorage(path, { kind: 'children' });
74
110
  return JSON.parse(this.decode(raw)).children;
75
111
  },
76
112
  /**
@@ -79,7 +115,7 @@ export const makeVStorage = powers => {
79
115
  * @returns {Promise<{blockHeight: number, values: string[]}>}
80
116
  */
81
117
  async readAt(path, height = undefined) {
82
- const raw = await getJSON(this.url(path, { kind: 'data', height }));
118
+ const raw = await readStorage(path, { kind: 'data', height });
83
119
  const txt = this.decode(raw);
84
120
  /** @type {{ value: string }} */
85
121
  const { value } = JSON.parse(txt);
@@ -89,32 +125,34 @@ export const makeVStorage = powers => {
89
125
  * Read values going back as far as available
90
126
  *
91
127
  * @param {string} path
128
+ * @param {number | string} [minHeight]
92
129
  * @returns {Promise<string[]>}
93
130
  */
94
- async readFully(path) {
131
+ async readFully(path, minHeight = undefined) {
95
132
  const parts = [];
96
133
  // undefined the first iteration, to query at the highest
97
134
  let blockHeight;
98
135
  do {
99
- console.debug('READING', { blockHeight });
136
+ // console.debug('READING', { blockHeight });
100
137
  let values;
101
138
  try {
102
139
  // eslint-disable-next-line no-await-in-loop
103
140
  ({ blockHeight, values } = await this.readAt(
104
141
  path,
105
- blockHeight && blockHeight - 1,
142
+ blockHeight && Number(blockHeight) - 1,
106
143
  ));
107
- console.debug('readAt returned', { blockHeight });
144
+ // console.debug('readAt returned', { blockHeight });
108
145
  } catch (err) {
109
- if ('log' in err && err.log.match(/unknown request/)) {
110
- console.error(err);
146
+ if (err.message.match(/unknown request/)) {
147
+ // console.error(err);
111
148
  break;
112
149
  }
113
150
  throw err;
114
151
  }
115
152
  parts.push(values);
116
- console.debug('PUSHED', values);
117
- console.debug('NEW', { blockHeight });
153
+ // console.debug('PUSHED', values);
154
+ // console.debug('NEW', { blockHeight, minHeight });
155
+ if (minHeight && Number(blockHeight) <= Number(minHeight)) break;
118
156
  } while (blockHeight > 0);
119
157
  return parts.flat();
120
158
  },
@@ -122,76 +160,14 @@ export const makeVStorage = powers => {
122
160
  };
123
161
  /** @typedef {ReturnType<typeof makeVStorage>} VStorage */
124
162
 
125
- /**
126
- * Like makeMarshal but,
127
- * - slotToVal takes an iface arg
128
- * - if a part being serialized has a boardId property, it passes through as a slot value whereas the normal marshaller would treat it as a copyRecord
129
- *
130
- * @param {(slot: string, iface: string) => any} slotToVal
131
- * @returns {import('@endo/marshal').Marshal<string>}
132
- */
133
- export const boardSlottingMarshaller = (slotToVal = (s, _i) => s) => ({
134
- /** @param {{body: string, slots: string[]}} capData */
135
- unserialize: ({ body, slots }) => {
136
- const reviver = (_key, obj) => {
137
- const qclass = obj !== null && typeof obj === 'object' && obj['@qclass'];
138
- // NOTE: hilbert hotel not impl
139
- switch (qclass) {
140
- case 'slot': {
141
- const { index, iface } = obj;
142
- return slotToVal(slots[index], iface);
143
- }
144
- case 'bigint':
145
- return BigInt(obj.digits);
146
- case 'undefined':
147
- return undefined;
148
- default:
149
- return obj;
150
- }
151
- };
152
- return JSON.parse(body, reviver);
153
- },
154
- serialize: whole => {
155
- const seen = new Map();
156
- const slotIndex = v => {
157
- if (seen.has(v)) {
158
- return seen.get(v);
159
- }
160
- const index = seen.size;
161
- seen.set(v, index);
162
- return { index, iface: v.iface };
163
- };
164
- const recur = part => {
165
- if (part === null) return null;
166
- if (typeof part === 'bigint') {
167
- return { '@qclass': 'bigint', digits: `${part}` };
168
- }
169
- if (Array.isArray(part)) {
170
- return part.map(recur);
171
- }
172
- if (typeof part === 'object') {
173
- if ('boardId' in part) {
174
- return { '@qclass': 'slot', ...slotIndex(part.boardId) };
175
- }
176
- return Object.fromEntries(
177
- Object.entries(part).map(([k, v]) => [k, recur(v)]),
178
- );
179
- }
180
- return part;
181
- };
182
- const after = recur(whole);
183
- return { body: JSON.stringify(after), slots: [...seen.keys()] };
184
- },
185
- });
186
-
187
- export const makeFromBoard = (slotKey = 'boardId') => {
163
+ export const makeFromBoard = () => {
188
164
  const cache = new Map();
189
- const convertSlotToVal = (slot, iface) => {
190
- if (cache.has(slot)) {
191
- return cache.get(slot);
165
+ const convertSlotToVal = (boardId, iface) => {
166
+ if (cache.has(boardId)) {
167
+ return cache.get(boardId);
192
168
  }
193
- const val = harden({ [slotKey]: slot, iface });
194
- cache.set(slot, val);
169
+ const val = makeBoardRemote({ boardId, iface });
170
+ cache.set(boardId, val);
195
171
  return val;
196
172
  };
197
173
  return harden({ convertSlotToVal });
@@ -217,7 +193,7 @@ export const storageHelper = {
217
193
  unserializeTxt: (txt, ctx) => {
218
194
  const { capDatas } = storageHelper.parseCapData(txt);
219
195
  return capDatas.map(capData =>
220
- boardSlottingMarshaller(ctx.convertSlotToVal).unserialize(capData),
196
+ boardSlottingMarshaller(ctx.convertSlotToVal).fromCapData(capData),
221
197
  );
222
198
  },
223
199
  /** @param {string[]} capDataStrings array of stringified capData */
@@ -239,18 +215,21 @@ harden(storageHelper);
239
215
  /**
240
216
  * @param {IdMap} ctx
241
217
  * @param {VStorage} vstorage
242
- * @returns {Promise<{ brand: Record<string, RpcRemote>, instance: Record<string, RpcRemote>, reverse: Record<string, string> }>}
218
+ * @returns {Promise<import('@agoric/vats/tools/board-utils.js').AgoricNamesRemotes>}
243
219
  */
244
220
  export const makeAgoricNames = async (ctx, vstorage) => {
245
221
  const reverse = {};
246
222
  const entries = await Promise.all(
247
- ['brand', 'instance'].map(async kind => {
223
+ ['brand', 'instance', 'vbankAsset'].map(async kind => {
248
224
  const content = await vstorage.readLatest(
249
225
  `published.agoricNames.${kind}`,
250
226
  );
227
+ /** @type {Array<[string, import('@agoric/vats/tools/board-utils.js').BoardRemote]>} */
251
228
  const parts = storageHelper.unserializeTxt(content, ctx).at(-1);
252
229
  for (const [name, remote] of parts) {
253
- reverse[remote.boardId] = name;
230
+ if ('getBoardId' in remote) {
231
+ reverse[remote.getBoardId()] = name;
232
+ }
254
233
  }
255
234
  return [kind, Object.fromEntries(parts)];
256
235
  }),
@@ -258,10 +237,36 @@ export const makeAgoricNames = async (ctx, vstorage) => {
258
237
  return { ...Object.fromEntries(entries), reverse };
259
238
  };
260
239
 
261
- export const makeRpcUtils = async ({ fetch }) => {
262
- const vstorage = makeVStorage({ fetch });
263
- const fromBoard = makeFromBoard();
264
- const agoricNames = await makeAgoricNames(fromBoard, vstorage);
240
+ /**
241
+ * @param {{ fetch: typeof window.fetch }} io
242
+ * @param {MinimalNetworkConfig} config
243
+ */
244
+ export const makeRpcUtils = async ({ fetch }, config = networkConfig) => {
245
+ try {
246
+ const vstorage = makeVStorage({ fetch }, config);
247
+ const fromBoard = makeFromBoard();
248
+ const agoricNames = await makeAgoricNames(fromBoard, vstorage);
249
+
250
+ const unserializer = boardSlottingMarshaller(fromBoard.convertSlotToVal);
265
251
 
266
- return { vstorage, fromBoard, agoricNames };
252
+ /** @type {(txt: string) => unknown} */
253
+ const unserializeHead = txt =>
254
+ storageHelper.unserializeTxt(txt, fromBoard).at(-1);
255
+
256
+ /** @type {(path: string) => Promise<unknown>} */
257
+ const readLatestHead = path =>
258
+ vstorage.readLatest(path).then(unserializeHead);
259
+
260
+ return {
261
+ agoricNames,
262
+ fromBoard,
263
+ readLatestHead,
264
+ unserializeHead,
265
+ unserializer,
266
+ vstorage,
267
+ };
268
+ } catch (err) {
269
+ throw Error(`RPC failure (${config.rpcAddrs}): ${err.message}`);
270
+ }
267
271
  };
272
+ /** @typedef {Awaited<ReturnType<typeof makeRpcUtils>>} RpcUtils */
package/src/lib/wallet.js CHANGED
@@ -1,20 +1,315 @@
1
1
  // @ts-check
2
+ /* global process */
2
3
 
3
- import { storageHelper } from './rpc.js';
4
+ import { iterateReverse } from '@agoric/casting';
5
+ import { makeWalletStateCoalescer } from '@agoric/smart-wallet/src/utils.js';
6
+ import { execSwingsetTransaction, pollBlocks, pollTx } from './chain.js';
7
+ import { boardSlottingMarshaller, makeRpcUtils } from './rpc.js';
8
+
9
+ /** @typedef {import('@agoric/smart-wallet/src/smartWallet.js').CurrentWalletRecord} CurrentWalletRecord */
10
+ /** @typedef {import('@agoric/vats/tools/board-utils.js').AgoricNamesRemotes} AgoricNamesRemotes */
11
+
12
+ const { values } = Object;
13
+ const { Fail } = assert;
14
+ const marshaller = boardSlottingMarshaller();
15
+
16
+ /** @type {CurrentWalletRecord} */
17
+ const emptyCurrentRecord = {
18
+ purses: [],
19
+ offerToUsedInvitation: [],
20
+ offerToPublicSubscriberPaths: [],
21
+ liveOffers: [],
22
+ };
4
23
 
5
24
  /**
6
25
  * @param {string} addr
7
- * @param {import('./rpc').IdMap} ctx
8
- * @param {object} io
9
- * @param {import('./rpc.js').VStorage} io.vstorage
26
+ * @param {Pick<import('./rpc.js').RpcUtils, 'readLatestHead'>} io
10
27
  * @returns {Promise<import('@agoric/smart-wallet/src/smartWallet').CurrentWalletRecord>}
11
28
  */
12
- export const getCurrent = async (addr, ctx, { vstorage }) => {
13
- const capDataStr = await vstorage.readLatest(
14
- `published.wallet.${addr}.current`,
15
- );
29
+ export const getCurrent = async (addr, { readLatestHead }) => {
30
+ // Partial because older writes may not have had all properties
31
+ // NB: assumes changes are only additions
32
+ let current =
33
+ /** @type {Partial<import('@agoric/smart-wallet/src/smartWallet').CurrentWalletRecord> | undefined} */ (
34
+ await readLatestHead(`published.wallet.${addr}.current`)
35
+ );
36
+ if (current === undefined) {
37
+ throw new Error(`undefined current node for ${addr}`);
38
+ }
39
+
40
+ // Repair a type misunderstanding seen in the wild.
41
+ // See https://github.com/Agoric/agoric-sdk/pull/7139
42
+ let offerToUsedInvitation = current.offerToUsedInvitation;
43
+ if (
44
+ offerToUsedInvitation &&
45
+ typeof offerToUsedInvitation === 'object' &&
46
+ !Array.isArray(offerToUsedInvitation)
47
+ ) {
48
+ offerToUsedInvitation = Object.entries(offerToUsedInvitation);
49
+ current = harden({
50
+ ...current,
51
+ offerToUsedInvitation,
52
+ });
53
+ }
54
+
55
+ // override full empty record with defined values from published one
56
+ return { ...emptyCurrentRecord, ...current };
57
+ };
58
+
59
+ /**
60
+ * @param {string} addr
61
+ * @param {Pick<import('./rpc.js').RpcUtils, 'readLatestHead'>} io
62
+ * @returns {Promise<import('@agoric/smart-wallet/src/smartWallet').UpdateRecord>}
63
+ */
64
+ export const getLastUpdate = (addr, { readLatestHead }) => {
65
+ // @ts-expect-error cast
66
+ return readLatestHead(`published.wallet.${addr}`);
67
+ };
68
+
69
+ /**
70
+ * @param {import('@agoric/smart-wallet/src/smartWallet').BridgeAction} bridgeAction
71
+ * @param {Pick<import('stream').Writable,'write'>} [stdout]
72
+ */
73
+ export const outputAction = (bridgeAction, stdout = process.stdout) => {
74
+ const capData = marshaller.toCapData(harden(bridgeAction));
75
+ stdout.write(JSON.stringify(capData));
76
+ stdout.write('\n');
77
+ };
78
+
79
+ const sendHint =
80
+ 'Now use `agoric wallet send ...` to sign and broadcast the offer.\n';
81
+
82
+ /**
83
+ * @param {import('@agoric/smart-wallet/src/smartWallet').BridgeAction} bridgeAction
84
+ * @param {{
85
+ * stdout: Pick<import('stream').Writable,'write'>,
86
+ * stderr: Pick<import('stream').Writable,'write'>,
87
+ * }} io
88
+ */
89
+ export const outputActionAndHint = (bridgeAction, { stdout, stderr }) => {
90
+ outputAction(bridgeAction, stdout);
91
+ stderr.write(sendHint);
92
+ };
93
+
94
+ /**
95
+ * @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer
96
+ * @param {Pick<import('stream').Writable,'write'>} [stdout]
97
+ */
98
+ export const outputExecuteOfferAction = (offer, stdout = process.stdout) => {
99
+ /** @type {import('@agoric/smart-wallet/src/smartWallet').BridgeAction} */
100
+ const spendAction = {
101
+ method: 'executeOffer',
102
+ offer,
103
+ };
104
+ outputAction(spendAction, stdout);
105
+ };
106
+
107
+ /**
108
+ * @deprecated use `.current` node for current state
109
+ * @param {import('@agoric/casting').Follower<import('@agoric/casting').ValueFollowerElement<import('@agoric/smart-wallet/src/smartWallet').UpdateRecord>>} follower
110
+ * @param {Brand<'set'>} [invitationBrand]
111
+ */
112
+ export const coalesceWalletState = async (follower, invitationBrand) => {
113
+ // values with oldest last
114
+ const history = [];
115
+ for await (const followerElement of iterateReverse(follower)) {
116
+ history.push(followerElement.value);
117
+ }
118
+
119
+ const coalescer = makeWalletStateCoalescer(invitationBrand);
120
+ // update with oldest first
121
+ for (const record of history.reverse()) {
122
+ coalescer.update(record);
123
+ }
124
+
125
+ return coalescer.state;
126
+ };
127
+
128
+ /**
129
+ * Sign and broadcast a wallet-action.
130
+ *
131
+ * @throws { Error & { code: number } } if transaction fails
132
+ * @param {import('@agoric/smart-wallet/src/smartWallet').BridgeAction} bridgeAction
133
+ * @param {import('./rpc').MinimalNetworkConfig & {
134
+ * from: string,
135
+ * verbose?: boolean,
136
+ * keyring?: {home?: string, backend: string},
137
+ * stdout: Pick<import('stream').Writable, 'write'>,
138
+ * execFileSync: typeof import('child_process').execFileSync,
139
+ * delay: (ms: number) => Promise<void>,
140
+ * dryRun?: boolean,
141
+ * }} opts
142
+ */
143
+ export const sendAction = async (bridgeAction, opts) => {
144
+ const offerBody = JSON.stringify(marshaller.toCapData(harden(bridgeAction)));
145
+
146
+ // tryExit should not require --allow-spend
147
+ // https://github.com/Agoric/agoric-sdk/issues/7291
148
+ const spendMethods = ['executeOffer', 'tryExitOffer'];
149
+ const spendArg = spendMethods.includes(bridgeAction.method)
150
+ ? ['--allow-spend']
151
+ : [];
152
+
153
+ const act = ['wallet-action', ...spendArg, offerBody];
154
+ const out = execSwingsetTransaction([...act, '--output', 'json'], opts);
155
+ if (opts.dryRun) {
156
+ return;
157
+ }
158
+
159
+ assert(out); // not dry run
160
+ const tx = JSON.parse(out);
161
+ if (tx.code !== 0) {
162
+ const err = Error(`failed to send action. code: ${tx.code}`);
163
+ // @ts-expect-error XXX how to add properties to an error?
164
+ err.code = tx.code;
165
+ throw err;
166
+ }
167
+
168
+ return pollTx(tx.txhash, opts);
169
+ };
170
+
171
+ /**
172
+ * @param {CurrentWalletRecord} current
173
+ * @param {AgoricNamesRemotes} agoricNames
174
+ */
175
+ export const findContinuingIds = (current, agoricNames) => {
176
+ // XXX should runtime type-check
177
+ /** @type {{ offerToUsedInvitation: [string, Amount<'set'>][]}} */
178
+ const { offerToUsedInvitation: entries } = /** @type {any} */ (current);
179
+
180
+ Array.isArray(entries) || Fail`entries must be an array: ${entries}`;
16
181
 
17
- const capDatas = storageHelper.unserializeTxt(capDataStr, ctx);
182
+ const keyOf = (obj, val) => {
183
+ const found = Object.entries(obj).find(e => e[1] === val);
184
+ return found && found[0];
185
+ };
18
186
 
19
- return capDatas[0];
187
+ const found = [];
188
+ for (const [offerId, { value }] of entries) {
189
+ /** @type {{ description: string, instance: unknown }[]} */
190
+ const [{ description, instance }] = value;
191
+ if (
192
+ description === 'charter member invitation' ||
193
+ /Voter\d+/.test(description)
194
+ ) {
195
+ const instanceName = keyOf(agoricNames.instance, instance);
196
+ found.push({ instance, instanceName, description, offerId });
197
+ }
198
+ }
199
+ return found;
20
200
  };
201
+
202
+ export const makeWalletUtils = async (
203
+ { fetch, execFileSync, delay },
204
+ networkConfig,
205
+ ) => {
206
+ const { agoricNames, fromBoard, readLatestHead, vstorage } =
207
+ await makeRpcUtils({ fetch }, networkConfig);
208
+ /**
209
+ * @param {string} from
210
+ * @param {number|string} [minHeight]
211
+ */
212
+ const storedWalletState = async (from, minHeight = undefined) => {
213
+ const m = boardSlottingMarshaller(fromBoard.convertSlotToVal);
214
+
215
+ const history = await vstorage.readFully(
216
+ `published.wallet.${from}`,
217
+ minHeight,
218
+ );
219
+
220
+ /** @type {{ Invitation: Brand<'set'> }} */
221
+ // @ts-expect-error XXX how to narrow AssetKind to set?
222
+ const { Invitation } = agoricNames.brand;
223
+ const coalescer = makeWalletStateCoalescer(Invitation);
224
+ // update with oldest first
225
+ for (const txt of history.reverse()) {
226
+ const { body, slots } = JSON.parse(txt);
227
+ const record = m.fromCapData({ body, slots });
228
+ coalescer.update(record);
229
+ }
230
+ const coalesced = coalescer.state;
231
+ harden(coalesced);
232
+ return coalesced;
233
+ };
234
+
235
+ /**
236
+ * Get OfferStatus by id, polling until available.
237
+ *
238
+ * @param {string} from
239
+ * @param {string|number} id
240
+ * @param {number|string} minHeight
241
+ * @param {boolean} [untilNumWantsSatisfied]
242
+ */
243
+ const pollOffer = async (
244
+ from,
245
+ id,
246
+ minHeight,
247
+ untilNumWantsSatisfied = false,
248
+ ) => {
249
+ const lookup = async () => {
250
+ // eslint-disable-next-line @jessie.js/no-nested-await, no-await-in-loop
251
+ const { offerStatuses } = await storedWalletState(from, minHeight);
252
+ const offerStatus = [...offerStatuses.values()].find(s => s.id === id);
253
+ if (!offerStatus) throw Error('retry');
254
+ harden(offerStatus);
255
+ if (untilNumWantsSatisfied && !('numWantsSatisfied' in offerStatus)) {
256
+ throw Error('retry (no numWantsSatisfied yet)');
257
+ }
258
+ return offerStatus;
259
+ };
260
+ const retryMessage = 'offer not in wallet at block';
261
+ const opts = { ...networkConfig, execFileSync, delay, retryMessage };
262
+ return pollBlocks(opts)(lookup);
263
+ };
264
+
265
+ return {
266
+ networkConfig,
267
+ agoricNames,
268
+ fromBoard,
269
+ vstorage,
270
+ readLatestHead,
271
+ storedWalletState,
272
+ pollOffer,
273
+ };
274
+ };
275
+
276
+ /**
277
+ * @param {{
278
+ * brand: Record<string, Brand>,
279
+ * vbankAsset: Record<string, { brand: Brand, displayInfo: DisplayInfo }>,
280
+ * }} agoricNames
281
+ * @param {(msg: string) => Error} makeError error constructor
282
+ * @returns {(a: string) => Amount<'nat'>}
283
+ */
284
+ export const makeParseAmount =
285
+ (agoricNames, makeError = msg => RangeError(msg)) =>
286
+ opt => {
287
+ assert.typeof(opt, 'string', 'parseAmount expected string');
288
+ const m = opt.match(/^(?<value>[\d_]+(\.[\d_]+)?)(?<brand>[A-Z]\w*?)$/);
289
+ if (!m || !m.groups) {
290
+ throw makeError(`invalid amount: ${opt}`);
291
+ }
292
+ const anyBrand = agoricNames.brand[m.groups.brand];
293
+ if (!anyBrand) {
294
+ throw makeError(`unknown brand: ${m.groups.brand}`);
295
+ }
296
+ const assetDesc = values(agoricNames.vbankAsset).find(
297
+ d => d.brand === anyBrand,
298
+ );
299
+ if (!assetDesc) {
300
+ throw makeError(`unknown brand: ${m.groups.brand}`);
301
+ }
302
+ const { displayInfo } = assetDesc;
303
+ if (!displayInfo.decimalPlaces || displayInfo.assetKind !== 'nat') {
304
+ throw makeError(`bad brand: ${displayInfo}`);
305
+ }
306
+ const value = BigInt(
307
+ Number(m.groups.value.replace(/_/g, '')) *
308
+ 10 ** displayInfo.decimalPlaces,
309
+ );
310
+ /** @type {Brand<'nat'>} */
311
+ // @ts-expect-error dynamic cast
312
+ const natBrand = anyBrand;
313
+ const amt = { value, brand: natBrand };
314
+ return amt;
315
+ };
@@ -15,12 +15,10 @@ const publishMain = async (progname, rawArgs, powers, opts) => {
15
15
  const { node: rpcAddress, home: homeDirectory, chainID = 'agoric' } = opts;
16
16
 
17
17
  if (typeof rpcAddress !== 'string') {
18
- throw new Error(
19
- `Required flag for agoric publish: -n, --node <rpcAddress>`,
20
- );
18
+ throw Error(`Required flag for agoric publish: -n, --node <rpcAddress>`);
21
19
  }
22
20
  if (typeof homeDirectory !== 'string') {
23
- throw new Error(
21
+ throw Error(
24
22
  `Required flag for agoric publish: -h, --home <directory>, containing ag-solo-mnemonic`,
25
23
  );
26
24
  }
@@ -47,7 +45,7 @@ const publishMain = async (progname, rawArgs, powers, opts) => {
47
45
  });
48
46
  const publishBundle = makeBundlePublisher({
49
47
  getDefaultConnection() {
50
- throw new Error(
48
+ throw Error(
51
49
  'Invariant: publishBundle will never call getDefaultConnection because we provide an explicit connectionSpec',
52
50
  );
53
51
  },