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.
@@ -0,0 +1,626 @@
1
+ /**
2
+ * @file Inter Protocol Liquidation Bidding CLI
3
+ * @see {makeInterCommand} for main function
4
+ */
5
+
6
+ // @ts-check
7
+ import { CommanderError, InvalidArgumentError } from 'commander';
8
+ // TODO: should get M from endo https://github.com/Agoric/agoric-sdk/issues/7090
9
+ import { makeOfferSpecShape } from '@agoric/inter-protocol/src/auction/auctionBook.js';
10
+ import { Offers } from '@agoric/inter-protocol/src/clientSupport.js';
11
+ import { objectMap } from '@agoric/internal';
12
+ import { M, matches } from '@agoric/store';
13
+
14
+ // XXX scare away ambient type zombies to fix ScheduleNotification.activeStartTime etc.
15
+ // https://github.com/Agoric/agoric-sdk/issues/6512
16
+ // https://github.com/Agoric/agoric-sdk/issues/6343
17
+ import '@agoric/inter-protocol/src/vaultFactory/types.js';
18
+
19
+ import { normalizeAddressWithOptions, pollBlocks } from '../lib/chain.js';
20
+ import {
21
+ asBoardRemote,
22
+ bigintReplacer,
23
+ makeAmountFormatter,
24
+ } from '../lib/format.js';
25
+ import { getNetworkConfig } from '../lib/rpc.js';
26
+ import {
27
+ getCurrent,
28
+ makeParseAmount,
29
+ makeWalletUtils,
30
+ outputActionAndHint,
31
+ sendAction,
32
+ } from '../lib/wallet.js';
33
+
34
+ const { values } = Object;
35
+
36
+ const bidInvitationShape = harden({
37
+ source: 'agoricContract',
38
+ instancePath: ['auctioneer'],
39
+ callPipe: [['makeBidInvitation', M.any()]],
40
+ });
41
+
42
+ /** @typedef {import('@agoric/vats/tools/board-utils.js').VBankAssetDetail } AssetDescriptor */
43
+ /** @typedef {import('@agoric/smart-wallet/src/smartWallet').TryExitOfferAction } TryExitOfferAction */
44
+ /** @typedef {import('@agoric/inter-protocol/src/auction/auctionBook.js').OfferSpec} BidSpec */
45
+ /** @typedef {import('@agoric/inter-protocol/src/auction/scheduler.js').ScheduleNotification} ScheduleNotification */
46
+ /** @typedef {import('@agoric/inter-protocol/src/auction/auctionBook.js').BookDataNotification} BookDataNotification */
47
+
48
+ /**
49
+ * Format amounts, prices etc. based on brand board Ids, displayInfo
50
+ *
51
+ * @param {AssetDescriptor[]} assets
52
+ */
53
+ const makeFormatters = assets => {
54
+ const r4 = x => Math.round(x * 10_000) / 10_000;
55
+
56
+ const br = asBoardRemote;
57
+ const fmtAmtTuple = makeAmountFormatter(assets);
58
+
59
+ /** @param {Amount} amt */
60
+ const amount = amt => (([l, m]) => `${m} ${l}`)(fmtAmtTuple(br(amt)));
61
+ /** @param {Record<string, Amount> | undefined} r */
62
+ const record = r => (r ? objectMap(r, amount) : undefined);
63
+ /** @param {Ratio} r */
64
+ const price = r => {
65
+ const [nl, nm] = fmtAmtTuple(br(r.numerator));
66
+ const [dl, dm] = fmtAmtTuple(br(r.denominator));
67
+ return `${r4(Number(nm) / Number(dm))} ${nl}/${dl}`;
68
+ };
69
+ /** @param {Ratio} r */
70
+ const discount = r =>
71
+ r4(100 - (Number(r.numerator.value) / Number(r.denominator.value)) * 100);
72
+
73
+ // XXX real TimeMath.absValue requires real Remotable timerBrand
74
+ /** @param {import('@agoric/time/src/types.js').Timestamp} ts */
75
+ const absValue = ts => (typeof ts === 'bigint' ? ts : ts.absValue);
76
+
77
+ /** @param {import('@agoric/time/src/types.js').Timestamp} tr */
78
+ const absTime = tr => new Date(Number(absValue(tr)) * 1000).toISOString();
79
+ /** @param {import('@agoric/time/src/types.js').RelativeTimeRecord} tr */
80
+ const relTime = tr =>
81
+ new Date(Number(tr.relValue) * 1000).toISOString().slice(11, 19);
82
+
83
+ /** @param {bigint} bp */
84
+ const basisPoints = bp => `${(Number(bp) / 100).toFixed(2)}%`;
85
+
86
+ /**
87
+ * @template T
88
+ * @param {(_: T) => string} f
89
+ * @returns { (x: T | null | undefined ) => string | undefined }
90
+ */
91
+ const maybe = f => x => x ? f(x) : undefined;
92
+
93
+ return {
94
+ amount,
95
+ amountOpt: maybe(amount),
96
+ record,
97
+ price,
98
+ priceOpt: maybe(price),
99
+ discount,
100
+ absTime,
101
+ absTimeOpt: maybe(absTime),
102
+ relTime,
103
+ basisPoints,
104
+ };
105
+ };
106
+
107
+ /**
108
+ * Dynamic check that an OfferStatus is also a BidSpec.
109
+ *
110
+ * @param {import('@agoric/smart-wallet/src/offers.js').OfferStatus} offerStatus
111
+ * @param {Awaited<ReturnType<import('../lib/rpc').makeAgoricNames>>} agoricNames
112
+ * @param {typeof console.warn} warn
113
+ * returns null if offerStatus is not a BidSpec
114
+ */
115
+ const coerceBid = (offerStatus, agoricNames, warn) => {
116
+ const { offerArgs } = offerStatus;
117
+ /** @type {unknown} */
118
+ const collateralBrand = /** @type {any} */ (offerArgs)?.maxBuy?.brand;
119
+ if (!collateralBrand) {
120
+ warn('mal-formed bid offerArgs', offerStatus.id, offerArgs);
121
+ return null;
122
+ }
123
+ const bidSpecShape = makeOfferSpecShape(
124
+ // @ts-expect-error XXX AssetKind narrowing?
125
+ agoricNames.brand.IST,
126
+ collateralBrand,
127
+ );
128
+ if (!matches(offerStatus.offerArgs, bidSpecShape)) {
129
+ warn('mal-formed bid offerArgs', offerArgs);
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * @type {import('@agoric/smart-wallet/src/offers.js').OfferStatus &
135
+ * { offerArgs: BidSpec}}
136
+ */
137
+ // @ts-expect-error dynamic cast
138
+ const bid = offerStatus;
139
+ return bid;
140
+ };
141
+
142
+ /**
143
+ * Format amounts etc. in a BidSpec OfferStatus
144
+ *
145
+ * @param {import('@agoric/smart-wallet/src/offers.js').OfferStatus &
146
+ * { offerArgs: BidSpec}} bid
147
+ * @param {import('agoric/src/lib/format.js').AssetDescriptor[]} assets
148
+ */
149
+ export const fmtBid = (bid, assets) => {
150
+ const fmt = makeFormatters(assets);
151
+
152
+ const { offerArgs } = bid;
153
+ /** @type {{ price: string } | { discount: number }} */
154
+ const spec =
155
+ 'offerPrice' in offerArgs
156
+ ? { price: fmt.price(offerArgs.offerPrice) }
157
+ : { discount: fmt.discount(offerArgs.offerBidScaling) };
158
+
159
+ const {
160
+ id,
161
+ proposal: { give, want },
162
+ offerArgs: { maxBuy },
163
+ payouts,
164
+ result,
165
+ error,
166
+ } = bid;
167
+ const resultProp =
168
+ !error && result && result !== 'UNPUBLISHED' ? { result } : {};
169
+ const props = {
170
+ ...(give ? { give: fmt.record(give) } : {}),
171
+ ...(want ? { give: fmt.record(want) } : {}),
172
+ ...(maxBuy ? { maxBuy: fmt.amount(maxBuy) } : {}),
173
+ ...(payouts ? { payouts: fmt.record(payouts) } : resultProp),
174
+ ...(error ? { error } : {}),
175
+ };
176
+ return harden({ id, ...spec, ...props });
177
+ };
178
+
179
+ /**
180
+ * Make Inter Protocol liquidation bidding commands.
181
+ *
182
+ * @param {{
183
+ * env: Partial<Record<string, string>>,
184
+ * stdout: Pick<import('stream').Writable,'write'>,
185
+ * stderr: Pick<import('stream').Writable,'write'>,
186
+ * now: () => number,
187
+ * createCommand: // Note: includes access to process.stdout, .stderr, .exit
188
+ * typeof import('commander').createCommand,
189
+ * execFileSync: typeof import('child_process').execFileSync,
190
+ * setTimeout: typeof setTimeout,
191
+ * }} process
192
+ * @param {{ fetch: typeof window.fetch }} net
193
+ */
194
+ export const makeInterCommand = (
195
+ {
196
+ env,
197
+ stdout,
198
+ stderr,
199
+ now,
200
+ setTimeout,
201
+ execFileSync: rawExec,
202
+ createCommand,
203
+ },
204
+ { fetch },
205
+ ) => {
206
+ const interCmd = createCommand('inter')
207
+ .description('Inter Protocol commands for liquidation bidding etc.')
208
+ .option('--home <dir>', 'agd CosmosSDK application home directory')
209
+ .option(
210
+ '--keyring-backend <os|file|test>',
211
+ `keyring's backend (os|file|test) (default "${
212
+ env.AGORIC_KEYRING_BACKEND || 'os'
213
+ }")`,
214
+ env.AGORIC_KEYRING_BACKEND,
215
+ );
216
+
217
+ /** @type {typeof import('child_process').execFileSync} */
218
+ // @ts-expect-error execFileSync is overloaded
219
+ const execFileSync = (file, args, ...opts) => {
220
+ try {
221
+ return rawExec(file, args, ...opts);
222
+ } catch (err) {
223
+ throw new InvalidArgumentError(
224
+ `${err.message}: is ${file} in your $PATH?`,
225
+ );
226
+ }
227
+ };
228
+
229
+ /** @param {number} ms */
230
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
231
+ const show = (info, indent = false) =>
232
+ stdout.write(
233
+ `${JSON.stringify(info, bigintReplacer, indent ? 2 : undefined)}\n`,
234
+ );
235
+
236
+ const tryMakeUtils = async () => {
237
+ await null;
238
+ try {
239
+ // XXX pass fetch to getNetworkConfig() explicitly
240
+ // await null above makes this await safe
241
+ // eslint-disable-next-line @jessie.js/no-nested-await
242
+ const networkConfig = await getNetworkConfig(env);
243
+ return makeWalletUtils({ fetch, execFileSync, delay }, networkConfig);
244
+ } catch (err) {
245
+ throw new CommanderError(1, 'RPC_FAIL', err.message);
246
+ }
247
+ };
248
+
249
+ const auctionCmd = interCmd
250
+ .command('auction')
251
+ .description('auction commands');
252
+ auctionCmd
253
+ .command('status')
254
+ .description(
255
+ `show auction status in JSON format
256
+
257
+ For example:
258
+
259
+ inter auction status
260
+ {
261
+ "schedule": {
262
+ "activeStartTime": "2023-04-19T22:50:02.000Z",
263
+ "nextStartTime": "2023-04-19T23:00:02.000Z",
264
+ "nextDescendingStepTime": "2023-04-19T22:51:02.000Z"
265
+ },
266
+ "book0": {
267
+ "startPrice": "12.34 IST/ATOM",
268
+ "currentPriceLevel": "11.723 IST/ATOM",
269
+ "startCollateral": "0 ATOM",
270
+ "collateralAvailable": "0 ATOM"
271
+ },
272
+ "params": {
273
+ "DiscountStep": "5.00%",
274
+ "ClockStep": "00:00:20",
275
+ "LowestRate": "45.00%"
276
+ }
277
+ }
278
+ `,
279
+ )
280
+ .option('--book <number>', 'Auction Book', Number, 0)
281
+ .action(
282
+ async (
283
+ /**
284
+ * @type {{
285
+ * book: number,
286
+ * }}
287
+ */ opts,
288
+ ) => {
289
+ const { agoricNames, readLatestHead } = await tryMakeUtils();
290
+
291
+ /** @type { [ScheduleNotification, BookDataNotification, *] } */
292
+ // @ts-expect-error dynamic cast
293
+ const [schedule, book, { current: params }] = await Promise.all([
294
+ readLatestHead(`published.auction.schedule`),
295
+ readLatestHead(`published.auction.book${opts.book}`),
296
+ readLatestHead(`published.auction.governance`),
297
+ ]);
298
+
299
+ const fmt = makeFormatters(Object.values(agoricNames.vbankAsset));
300
+ const info = {
301
+ schedule: {
302
+ activeStartTime: fmt.absTimeOpt(schedule.activeStartTime),
303
+ nextStartTime: fmt.absTimeOpt(schedule.nextStartTime),
304
+ nextDescendingStepTime: fmt.absTimeOpt(
305
+ schedule.nextDescendingStepTime,
306
+ ),
307
+ },
308
+ [`book${opts.book}`]: {
309
+ startPrice: fmt.priceOpt(book.startPrice),
310
+ currentPriceLevel: fmt.priceOpt(book.currentPriceLevel),
311
+ startProceedsGoal: fmt.amountOpt(book.startProceedsGoal),
312
+ remainingProceedsGoal: fmt.amountOpt(book.remainingProceedsGoal),
313
+ proceedsRaised: fmt.amountOpt(book.proceedsRaised),
314
+ startCollateral: fmt.amount(book.startCollateral),
315
+ collateralAvailable: fmt.amountOpt(book.collateralAvailable),
316
+ },
317
+ params: {
318
+ DiscountStep: fmt.basisPoints(params.DiscountStep.value),
319
+ ClockStep: fmt.relTime(params.ClockStep.value),
320
+ LowestRate: fmt.basisPoints(params.LowestRate.value),
321
+ },
322
+ };
323
+
324
+ show(info, true);
325
+ },
326
+ );
327
+
328
+ const bidCmd = interCmd
329
+ .command('bid')
330
+ .description('auction bidding commands');
331
+
332
+ /**
333
+ * @param {string} from
334
+ * @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer
335
+ * @param {Awaited<ReturnType<tryMakeUtils>>} tools
336
+ * @param {boolean?} dryRun
337
+ */
338
+ const placeBid = async (from, offer, tools, dryRun = false) => {
339
+ const { networkConfig, agoricNames, pollOffer } = tools;
340
+ const io = { ...networkConfig, execFileSync, delay, stdout };
341
+
342
+ const { home, keyringBackend: backend } = interCmd.opts();
343
+ const result = await sendAction(
344
+ { method: 'executeOffer', offer },
345
+ { keyring: { home, backend }, from, verbose: false, dryRun, ...io },
346
+ );
347
+ if (dryRun) {
348
+ return;
349
+ }
350
+
351
+ assert(result); // Not dry-run
352
+ const { timestamp, txhash, height } = result;
353
+ console.error('bid is broadcast:');
354
+ show({ timestamp, height, offerId: offer.id, txhash });
355
+ const found = await pollOffer(from, offer.id, height);
356
+ // TODO: command to wait 'till bid exits?
357
+ const bid = coerceBid(found, agoricNames, console.warn);
358
+ if (!bid) {
359
+ console.warn('malformed bid', found);
360
+ return;
361
+ }
362
+ const info = fmtBid(bid, values(agoricNames.vbankAsset));
363
+ show(info);
364
+ };
365
+
366
+ /** @param {string} literalOrName */
367
+ const normalizeAddress = literalOrName =>
368
+ normalizeAddressWithOptions(literalOrName, interCmd.opts(), {
369
+ execFileSync,
370
+ });
371
+
372
+ /**
373
+ * @typedef {{
374
+ * give: string,
375
+ * maxBuy: string,
376
+ * wantMinimum?: string,
377
+ * offerId: string,
378
+ * from: string,
379
+ * generateOnly?: boolean,
380
+ * dryRun?: boolean,
381
+ * }} SharedBidOpts
382
+ */
383
+
384
+ /** @param {ReturnType<createCommand>} cmd */
385
+ const withSharedBidOptions = cmd =>
386
+ cmd
387
+ .requiredOption(
388
+ '--from <address>',
389
+ 'wallet address literal or name',
390
+ normalizeAddress,
391
+ )
392
+ .requiredOption('--give <amount>', 'IST to bid')
393
+ .option(
394
+ '--maxBuy <amount>',
395
+ 'max Collateral wanted',
396
+ String,
397
+ '1_000_000ATOM',
398
+ )
399
+ .option(
400
+ '--wantMinimum <amount>',
401
+ 'only transact a bid that supplies this much collateral',
402
+ )
403
+ .option('--offer-id <string>', 'Offer id', String, `bid-${now()}`)
404
+ .option('--generate-only', 'print wallet action only')
405
+ .option('--dry-run', 'dry run only');
406
+
407
+ withSharedBidOptions(bidCmd.command('by-price'))
408
+ .description('Place a bid on collateral by price.')
409
+ .requiredOption('--price <number>', 'bid price (IST/Collateral)', Number)
410
+ .action(
411
+ /**
412
+ * @param {SharedBidOpts & {
413
+ * price: number,
414
+ * }} opts
415
+ */
416
+ async ({ generateOnly, dryRun, ...opts }) => {
417
+ const tools = await tryMakeUtils();
418
+
419
+ const parseAmount = makeParseAmount(
420
+ tools.agoricNames,
421
+ msg => new InvalidArgumentError(msg),
422
+ );
423
+ const offer = Offers.auction.Bid(tools.agoricNames.brand, {
424
+ ...opts,
425
+ parseAmount,
426
+ });
427
+
428
+ if (generateOnly) {
429
+ outputActionAndHint(
430
+ { method: 'executeOffer', offer },
431
+ { stdout, stderr },
432
+ );
433
+ return;
434
+ }
435
+
436
+ await placeBid(opts.from, offer, tools, dryRun);
437
+ },
438
+ );
439
+
440
+ /** @param {string} v */
441
+ const parsePercent = v => {
442
+ const p = Number(v);
443
+ if (!(p >= -100 && p <= 100)) {
444
+ throw new InvalidArgumentError('must be between -100 and 100');
445
+ }
446
+ return p / 100;
447
+ };
448
+
449
+ withSharedBidOptions(bidCmd.command('by-discount'))
450
+ .description(
451
+ `Place a bid on collateral based on discount from oracle price.`,
452
+ )
453
+ .requiredOption(
454
+ '--discount <percent>',
455
+ 'bid discount (0 to 100) or markup (0 to -100) %',
456
+ parsePercent,
457
+ )
458
+ .action(
459
+ /**
460
+ * @param {SharedBidOpts & {
461
+ * discount: number,
462
+ * }} opts
463
+ */
464
+ async ({ generateOnly, ...opts }) => {
465
+ const tools = await tryMakeUtils();
466
+
467
+ const parseAmount = makeParseAmount(
468
+ tools.agoricNames,
469
+ msg => new InvalidArgumentError(msg),
470
+ );
471
+ const offer = Offers.auction.Bid(tools.agoricNames.brand, {
472
+ ...opts,
473
+ parseAmount,
474
+ });
475
+ if (generateOnly) {
476
+ outputActionAndHint(
477
+ { method: 'executeOffer', offer },
478
+ { stdout, stderr },
479
+ );
480
+ return;
481
+ }
482
+ await placeBid(opts.from, offer, tools);
483
+ },
484
+ );
485
+
486
+ bidCmd
487
+ .command('cancel')
488
+ .description('Try to exit a bid offer')
489
+ .argument('id', 'offer id (as from bid list)')
490
+ .requiredOption(
491
+ '--from <address>',
492
+ 'wallet address literal or name',
493
+ normalizeAddress,
494
+ )
495
+ .option('--generate-only', 'print wallet action only')
496
+ .action(
497
+ /**
498
+ * @param {string} id
499
+ * @param {{
500
+ * from: string,
501
+ * generateOnly?: boolean,
502
+ * }} opts
503
+ */
504
+ async (id, { from, generateOnly }) => {
505
+ /** @type {TryExitOfferAction} */
506
+ const action = { method: 'tryExitOffer', offerId: id };
507
+
508
+ if (generateOnly) {
509
+ outputActionAndHint(action, { stdout, stderr });
510
+ return;
511
+ }
512
+
513
+ const { networkConfig, readLatestHead } = await tryMakeUtils();
514
+
515
+ const current = await getCurrent(from, { readLatestHead });
516
+ const liveIds = current.liveOffers.map(([i, _s]) => i);
517
+ if (!liveIds.includes(id)) {
518
+ throw new InvalidArgumentError(
519
+ `${id} not in live offer ids: ${liveIds}`,
520
+ );
521
+ }
522
+
523
+ const io = { ...networkConfig, execFileSync, delay, stdout };
524
+
525
+ const { home, keyringBackend: backend } = interCmd.opts();
526
+ const result = await sendAction(action, {
527
+ keyring: { home, backend },
528
+ from,
529
+ verbose: false,
530
+ ...io,
531
+ });
532
+ assert(result); // not dry-run
533
+ const { timestamp, txhash, height } = result;
534
+ console.error('cancel action is broadcast:');
535
+ show({ timestamp, height, offerId: id, txhash });
536
+
537
+ const checkGone = async blockInfo => {
538
+ const pollResult = await getCurrent(from, { readLatestHead });
539
+ const found = pollResult.liveOffers.find(([i, _]) => i === id);
540
+ if (found) throw Error('retry');
541
+ return blockInfo;
542
+ };
543
+ const blockInfo = await pollBlocks({
544
+ retryMessage: 'offer still live in block',
545
+ ...networkConfig,
546
+ execFileSync,
547
+ delay,
548
+ })(checkGone);
549
+ console.error('bid', id, 'is no longer live');
550
+ show(blockInfo);
551
+ },
552
+ );
553
+
554
+ bidCmd
555
+ .command('list')
556
+ .description(
557
+ `Show status of bid offers.
558
+
559
+ For example:
560
+
561
+ $ inter bid list --from my-acct
562
+ {"id":"bid-1679677228803","price":"9 IST/ATOM","give":{"Bid":"50IST"},"want":"5ATOM"}
563
+ {"id":"bid-1679677312341","discount":10,"give":{"Bid":"200IST"},"want":"1ATOM"}
564
+ `,
565
+ )
566
+ .requiredOption(
567
+ '--from <address>',
568
+ 'wallet address literal or name',
569
+ normalizeAddress,
570
+ )
571
+ .option('--all', 'show exited bids as well')
572
+ .action(
573
+ /**
574
+ * @param {{
575
+ * from: string,
576
+ * all?: boolean,
577
+ * }} opts
578
+ */
579
+ async opts => {
580
+ const { agoricNames, readLatestHead, storedWalletState } =
581
+ await tryMakeUtils();
582
+
583
+ const [current, state] = await Promise.all([
584
+ getCurrent(opts.from, { readLatestHead }),
585
+ storedWalletState(opts.from),
586
+ ]);
587
+ const entries = opts.all
588
+ ? state.offerStatuses.entries()
589
+ : current.liveOffers;
590
+ for (const [id, spec] of entries) {
591
+ const offerStatus = state.offerStatuses.get(id) || spec;
592
+ harden(offerStatus); // coalesceWalletState should do this
593
+ // console.debug(offerStatus.invitationSpec);
594
+ if (!matches(offerStatus.invitationSpec, bidInvitationShape))
595
+ continue;
596
+
597
+ const bid = coerceBid(offerStatus, agoricNames, console.warn);
598
+ if (!bid) continue;
599
+
600
+ const info = fmtBid(bid, values(agoricNames.vbankAsset));
601
+ show(info);
602
+ }
603
+ },
604
+ );
605
+
606
+ const assetCmd = interCmd
607
+ .command('vbank')
608
+ .description('vbank asset commands');
609
+ assetCmd
610
+ .command('list')
611
+ .description('list registered assets with decimalPlaces, boardId, etc.')
612
+ .action(async () => {
613
+ const { agoricNames } = await tryMakeUtils();
614
+ const assets = Object.values(agoricNames.vbankAsset).map(a => {
615
+ return {
616
+ issuerName: a.issuerName,
617
+ denom: a.denom,
618
+ brand: { boardId: a.brand.getBoardId() },
619
+ displayInfo: { decimalPlaces: a.displayInfo.decimalPlaces },
620
+ };
621
+ });
622
+ show(assets, true);
623
+ });
624
+
625
+ return interCmd;
626
+ };