@thetanuts-finance/odette-cli 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/NOTICE.md +65 -0
- package/README.md +128 -2
- package/dist/bookCheck.js +305 -0
- package/dist/bookEligibility.js +69 -0
- package/dist/bookMatch.js +131 -0
- package/dist/bookOrderIdentity.js +43 -0
- package/dist/bookPreview.js +35 -0
- package/dist/client.js +119 -0
- package/dist/commands/book.js +918 -0
- package/dist/commands/config.js +299 -0
- package/dist/commands/registry.js +14 -0
- package/dist/commands/setup.js +221 -0
- package/dist/commands/wallet.js +570 -0
- package/dist/config.js +146 -0
- package/dist/confirm.js +39 -0
- package/dist/defaults.js +17 -0
- package/dist/format.js +130 -0
- package/dist/index.js +84 -0
- package/dist/options.js +50 -0
- package/dist/output.js +417 -0
- package/package.json +52 -4
package/dist/output.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// Adapted from thetanuts-sdk cli/src/output.ts at 9e72aef1ab5c008ccc92352fa5048bd08c452ede.
|
|
2
|
+
// MIT License, Copyright (c) 2026 Thetanuts Finance.
|
|
3
|
+
// Changes: comments; fetchEthUsdSafe is bounded by a timeout (a stalled price
|
|
4
|
+
// lookup hid the receipt of a completed fill, inviting a duplicate buy).
|
|
5
|
+
// redactSecrets is exported for raw error text printed outside renderError.
|
|
6
|
+
import Table from 'cli-table3';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
/**
|
|
9
|
+
* JSON.stringify replacer that serializes bigints as decimal strings. Lifted
|
|
10
|
+
* verbatim from `mcp-server/src/index.ts` — many SDK reads return nested
|
|
11
|
+
* bigint values (vault state, RangerInfo, etc.); without this,
|
|
12
|
+
* `JSON.stringify` throws "Do not know how to serialize a BigInt".
|
|
13
|
+
*/
|
|
14
|
+
export function jsonReplacer(_key, value) {
|
|
15
|
+
if (typeof value === 'bigint')
|
|
16
|
+
return value.toString();
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function isPlainObject(value) {
|
|
20
|
+
return (typeof value === 'object' &&
|
|
21
|
+
value !== null &&
|
|
22
|
+
!Array.isArray(value) &&
|
|
23
|
+
Object.getPrototypeOf(value) === Object.prototype);
|
|
24
|
+
}
|
|
25
|
+
function toCellString(v) {
|
|
26
|
+
if (v === null || v === undefined)
|
|
27
|
+
return '';
|
|
28
|
+
if (typeof v === 'bigint')
|
|
29
|
+
return v.toString();
|
|
30
|
+
if (typeof v === 'object')
|
|
31
|
+
return JSON.stringify(v, jsonReplacer);
|
|
32
|
+
return String(v);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Recursively shorten long 0x-hex strings inside an object/array for table
|
|
36
|
+
* display. Keeps everything else intact. The original (untruncated) value is
|
|
37
|
+
* preserved for -o json since this helper is only invoked from the table
|
|
38
|
+
* renderer. Threshold matches roughly two terminal columns of useful preview.
|
|
39
|
+
*
|
|
40
|
+
* Encoded fill / approve / settle calldata routinely exceeds 4 KB on
|
|
41
|
+
* multi-leg structures and renders as 50+ wrapped rows otherwise.
|
|
42
|
+
*/
|
|
43
|
+
// Addresses (42), tx hashes / bytes32 (66), and compressed RFQ public keys
|
|
44
|
+
// (68) are the copy/paste primitives of a chain CLI — `chain contracts`,
|
|
45
|
+
// `keys show`, `position list` -> `position info --address`, and every
|
|
46
|
+
// receipt -> block-explorer flow depends on them rendering whole. Only
|
|
47
|
+
// genuinely large blobs collapse (EIP-712 signatures are 132, encoded
|
|
48
|
+
// calldata far more), and they keep the length suffix so a dry-run's size
|
|
49
|
+
// stays checkable. Raise this if a longer primitive is ever displayed.
|
|
50
|
+
const TRUNCATE_HEX_THRESHOLD = 68;
|
|
51
|
+
// Long enough to preserve a full copy/paste next-step command. The longest
|
|
52
|
+
// the CLI emits is `check`'s "Preview fill: odette preview …
|
|
53
|
+
// --collateral <amount>" at ~122 chars; keep headroom above that.
|
|
54
|
+
const TRUNCATE_STRING_THRESHOLD = 140;
|
|
55
|
+
function truncateForCell(s) {
|
|
56
|
+
if (/^0x[0-9a-fA-F]+$/.test(s) && s.length > TRUNCATE_HEX_THRESHOLD) {
|
|
57
|
+
return `${s.slice(0, 10)}…${s.slice(-6)} (${s.length} chars)`;
|
|
58
|
+
}
|
|
59
|
+
if (s.length <= TRUNCATE_STRING_THRESHOLD)
|
|
60
|
+
return s;
|
|
61
|
+
return `${s.slice(0, TRUNCATE_STRING_THRESHOLD - 1)}…`;
|
|
62
|
+
}
|
|
63
|
+
function truncateValue(v) {
|
|
64
|
+
if (typeof v === 'string')
|
|
65
|
+
return truncateForCell(v);
|
|
66
|
+
if (v === null || v === undefined)
|
|
67
|
+
return v;
|
|
68
|
+
if (typeof v === 'bigint' || typeof v === 'number' || typeof v === 'boolean') {
|
|
69
|
+
return v;
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(v))
|
|
72
|
+
return v.map(truncateValue);
|
|
73
|
+
if (isPlainObject(v)) {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [k, val] of Object.entries(v)) {
|
|
76
|
+
out[k] = truncateValue(val);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
return v;
|
|
81
|
+
}
|
|
82
|
+
function toCellStringTruncated(v) {
|
|
83
|
+
if (v === null || v === undefined)
|
|
84
|
+
return '';
|
|
85
|
+
if (typeof v === 'bigint')
|
|
86
|
+
return v.toString();
|
|
87
|
+
if (typeof v === 'string')
|
|
88
|
+
return truncateForCell(v);
|
|
89
|
+
if (typeof v === 'object') {
|
|
90
|
+
return JSON.stringify(truncateValue(v), jsonReplacer);
|
|
91
|
+
}
|
|
92
|
+
return String(v);
|
|
93
|
+
}
|
|
94
|
+
const EXPENDABLE_TABLE_COLUMNS = new Set([
|
|
95
|
+
'signingKey',
|
|
96
|
+
'offerSignature',
|
|
97
|
+
'transactionHash',
|
|
98
|
+
'signature',
|
|
99
|
+
'calldata',
|
|
100
|
+
'data',
|
|
101
|
+
'rawApiData',
|
|
102
|
+
]);
|
|
103
|
+
function estimatedTableWidth(headers, rows, cell) {
|
|
104
|
+
// cli-table3 needs one character of padding on each side plus a border per
|
|
105
|
+
// column. This is an intentionally conservative estimate.
|
|
106
|
+
return headers.reduce((total, header) => {
|
|
107
|
+
const widest = Math.max(header.length, ...rows.map((row) => cell(row[header]).length));
|
|
108
|
+
return total + widest + 3;
|
|
109
|
+
}, 1);
|
|
110
|
+
}
|
|
111
|
+
function renderArrayCards(headers, rows, cell) {
|
|
112
|
+
return rows
|
|
113
|
+
.map((row, index) => {
|
|
114
|
+
const table = new Table({ head: ['field', 'value'] });
|
|
115
|
+
for (const header of headers)
|
|
116
|
+
table.push([header, cell(row[header])]);
|
|
117
|
+
return `${rows.length > 1 ? `#${index + 1}\n` : ''}${table.toString()}`;
|
|
118
|
+
})
|
|
119
|
+
.join('\n\n');
|
|
120
|
+
}
|
|
121
|
+
function renderArrayTable(rows, truncate = true) {
|
|
122
|
+
if (rows.length === 0)
|
|
123
|
+
return '(empty)';
|
|
124
|
+
// Use first row's keys as the column ordering. Implementer commands that
|
|
125
|
+
// need stable columns should normalize their output before calling render.
|
|
126
|
+
const first = rows[0];
|
|
127
|
+
const cell = truncate ? toCellStringTruncated : toCellString;
|
|
128
|
+
if (!isPlainObject(first)) {
|
|
129
|
+
return rows.map((r) => cell(r)).join('\n');
|
|
130
|
+
}
|
|
131
|
+
const objectRows = rows.filter(isPlainObject);
|
|
132
|
+
let headers = Object.keys(first);
|
|
133
|
+
const terminalWidth = process.stdout.columns ?? 120;
|
|
134
|
+
const hidden = [];
|
|
135
|
+
// Metadata such as a 65-byte signature is useful in JSON but destroys a
|
|
136
|
+
// human table. Drop it only when needed to fit the current terminal.
|
|
137
|
+
while (estimatedTableWidth(headers, objectRows, cell) > terminalWidth) {
|
|
138
|
+
const index = headers.findIndex((header) => EXPENDABLE_TABLE_COLUMNS.has(header));
|
|
139
|
+
if (index === -1)
|
|
140
|
+
break;
|
|
141
|
+
hidden.push(headers[index]);
|
|
142
|
+
headers = headers.filter((_, i) => i !== index);
|
|
143
|
+
}
|
|
144
|
+
if (estimatedTableWidth(headers, objectRows, cell) > terminalWidth && objectRows.length <= 5) {
|
|
145
|
+
const cards = renderArrayCards(headers, objectRows, cell);
|
|
146
|
+
return hidden.length > 0
|
|
147
|
+
? `${cards}\n(table view omitted: ${hidden.join(', ')}; use --output json for full values)`
|
|
148
|
+
: cards;
|
|
149
|
+
}
|
|
150
|
+
const table = new Table({ head: headers });
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
if (isPlainObject(row)) {
|
|
153
|
+
table.push(headers.map((h) => cell(row[h])));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
table.push([cell(row)]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const note = hidden.length > 0
|
|
160
|
+
? `\n(table view omitted: ${hidden.join(', ')}; use --output json for full values)`
|
|
161
|
+
: '';
|
|
162
|
+
return table.toString() + note;
|
|
163
|
+
}
|
|
164
|
+
function renderObjectTable(obj, truncate = true) {
|
|
165
|
+
const cell = truncate ? toCellStringTruncated : toCellString;
|
|
166
|
+
const entries = Object.entries(obj);
|
|
167
|
+
// A key/value table has no column competition, so wrap the value column to
|
|
168
|
+
// the terminal instead of letting a single long cell push the table far past
|
|
169
|
+
// the viewport. Wrapping keeps the value intact and selectable; truncating
|
|
170
|
+
// would break copy/paste of addresses and next-step commands.
|
|
171
|
+
const terminalWidth = process.stdout.columns ?? 120;
|
|
172
|
+
const keyWidth = Math.min(32, entries.reduce((widest, [k]) => Math.max(widest, k.length), 3) + 2);
|
|
173
|
+
const valueWidth = Math.max(24, terminalWidth - keyWidth - 4);
|
|
174
|
+
const table = new Table({
|
|
175
|
+
head: ['key', 'value'],
|
|
176
|
+
colWidths: [keyWidth, valueWidth],
|
|
177
|
+
wordWrap: true,
|
|
178
|
+
wrapOnWordBoundary: false,
|
|
179
|
+
});
|
|
180
|
+
for (const [k, v] of entries) {
|
|
181
|
+
table.push([k, cell(v)]);
|
|
182
|
+
}
|
|
183
|
+
return table.toString();
|
|
184
|
+
}
|
|
185
|
+
function renderCsv(data) {
|
|
186
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
187
|
+
if (rows.length === 0)
|
|
188
|
+
return '';
|
|
189
|
+
const first = rows[0];
|
|
190
|
+
if (!isPlainObject(first)) {
|
|
191
|
+
return rows.map((r) => toCellString(r)).join('\n');
|
|
192
|
+
}
|
|
193
|
+
const headers = Object.keys(first);
|
|
194
|
+
const esc = (s) => /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
195
|
+
const lines = [headers.join(',')];
|
|
196
|
+
for (const row of rows) {
|
|
197
|
+
if (isPlainObject(row)) {
|
|
198
|
+
lines.push(headers.map((h) => esc(toCellString(row[h]))).join(','));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
lines.push(esc(toCellString(row)));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return lines.join('\n');
|
|
205
|
+
}
|
|
206
|
+
function renderYaml(data, indent = 0) {
|
|
207
|
+
// Minimal YAML emitter — sufficient for SDK return shapes (objects, arrays,
|
|
208
|
+
// primitives). Implementer agents needing richer YAML should add `yaml` as
|
|
209
|
+
// a dependency rather than expanding this helper.
|
|
210
|
+
const pad = ' '.repeat(indent);
|
|
211
|
+
if (data === null || data === undefined)
|
|
212
|
+
return `${pad}null`;
|
|
213
|
+
if (typeof data === 'bigint')
|
|
214
|
+
return `${pad}${data.toString()}`;
|
|
215
|
+
if (typeof data === 'string' ||
|
|
216
|
+
typeof data === 'number' ||
|
|
217
|
+
typeof data === 'boolean') {
|
|
218
|
+
return `${pad}${String(data)}`;
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(data)) {
|
|
221
|
+
if (data.length === 0)
|
|
222
|
+
return `${pad}[]`;
|
|
223
|
+
return data
|
|
224
|
+
.map((item) => {
|
|
225
|
+
if (isPlainObject(item) || Array.isArray(item)) {
|
|
226
|
+
const sub = renderYaml(item, indent + 1).replace(/^ {2}/, '');
|
|
227
|
+
return `${pad}- ${sub.trimStart()}`;
|
|
228
|
+
}
|
|
229
|
+
return `${pad}- ${toCellString(item)}`;
|
|
230
|
+
})
|
|
231
|
+
.join('\n');
|
|
232
|
+
}
|
|
233
|
+
if (isPlainObject(data)) {
|
|
234
|
+
const entries = Object.entries(data);
|
|
235
|
+
if (entries.length === 0)
|
|
236
|
+
return `${pad}{}`;
|
|
237
|
+
return entries
|
|
238
|
+
.map(([k, v]) => {
|
|
239
|
+
if (isPlainObject(v) || Array.isArray(v)) {
|
|
240
|
+
return `${pad}${k}:\n${renderYaml(v, indent + 1)}`;
|
|
241
|
+
}
|
|
242
|
+
return `${pad}${k}: ${toCellString(v)}`;
|
|
243
|
+
})
|
|
244
|
+
.join('\n');
|
|
245
|
+
}
|
|
246
|
+
return `${pad}${toCellString(data)}`;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Print a value to stdout in the user's selected format. `table` is the
|
|
250
|
+
* default; implementer commands should pass the raw SDK return as `data` and
|
|
251
|
+
* let the user pick `--output json` / `csv` / `yaml`.
|
|
252
|
+
*/
|
|
253
|
+
export function render(data, opts = {}) {
|
|
254
|
+
const fmt = opts.output ?? 'table';
|
|
255
|
+
if (fmt === 'json') {
|
|
256
|
+
process.stdout.write(JSON.stringify(data, jsonReplacer, 2) + '\n');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (fmt === 'csv') {
|
|
260
|
+
process.stdout.write(renderCsv(data) + '\n');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (fmt === 'yaml') {
|
|
264
|
+
process.stdout.write(renderYaml(data) + '\n');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// table (default)
|
|
268
|
+
if (data === null || data === undefined) {
|
|
269
|
+
process.stdout.write('\n');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const truncate = opts.truncate ?? true;
|
|
273
|
+
if (Array.isArray(data)) {
|
|
274
|
+
process.stdout.write(renderArrayTable(data, truncate) + '\n');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (isPlainObject(data)) {
|
|
278
|
+
process.stdout.write(renderObjectTable(data, truncate) + '\n');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// primitive
|
|
282
|
+
process.stdout.write(toCellString(data) + '\n');
|
|
283
|
+
}
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Tx receipt rendering helpers
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
/** Longest wait for the optional ETH/USD lookup after a transaction is mined. */
|
|
288
|
+
export const ETH_USD_TIMEOUT_MS = 3_000;
|
|
289
|
+
/**
|
|
290
|
+
* Best-effort ETH/USD spot fetch from the Odette market data API. Returns
|
|
291
|
+
* `null` (never throws) on any failure or after ETH_USD_TIMEOUT_MS — gas-cost
|
|
292
|
+
* USD is a nice-to-have. It runs after a fill or approval is already mined,
|
|
293
|
+
* so it must never delay the receipt: a hidden receipt invites a retry, and a
|
|
294
|
+
* retry buys a second position. The request is aborted on timeout.
|
|
295
|
+
*
|
|
296
|
+
* Structurally typed against the SDK's `client.api.getMarketData()` so we
|
|
297
|
+
* don't have to drag in the full OdetteClient type here.
|
|
298
|
+
*/
|
|
299
|
+
export async function fetchEthUsdSafe(api, timeoutMs = ETH_USD_TIMEOUT_MS) {
|
|
300
|
+
const controller = new AbortController();
|
|
301
|
+
let timer;
|
|
302
|
+
const timeout = new Promise((resolve) => {
|
|
303
|
+
timer = setTimeout(() => {
|
|
304
|
+
controller.abort();
|
|
305
|
+
resolve(null);
|
|
306
|
+
}, timeoutMs);
|
|
307
|
+
// Not unref'd: if nothing else holds the event loop, an unref'd timer lets
|
|
308
|
+
// Node exit before it fires, and the receipt would never print.
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
const md = await Promise.race([api.getMarketData({ signal: controller.signal }), timeout]);
|
|
312
|
+
const eth = md?.prices?.ETH;
|
|
313
|
+
if (typeof eth === 'number' && Number.isFinite(eth) && eth > 0)
|
|
314
|
+
return eth;
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Build the post-broadcast receipt payload with gas cost enrichments.
|
|
326
|
+
*
|
|
327
|
+
* `gasUsed` units are useless on their own — surface the effective gas price
|
|
328
|
+
* (gwei), the total tx fee in ETH, and the USD equivalent when an ETH/USD
|
|
329
|
+
* spot price is available. Any missing field is omitted rather than zeroed
|
|
330
|
+
* so the table doesn't show misleading "0.000000 ETH" rows on chains where
|
|
331
|
+
* the receipt doesn't expose gas pricing (some L2 RPCs).
|
|
332
|
+
*/
|
|
333
|
+
export function buildTxReceiptPayload(receipt, ethUsdPrice, extra) {
|
|
334
|
+
const payload = {
|
|
335
|
+
txHash: receipt.hash,
|
|
336
|
+
status: receipt.status === 1 ? 'success' : receipt.status === 0 ? 'failed' : receipt.status ?? 'unknown',
|
|
337
|
+
};
|
|
338
|
+
if (receipt.blockNumber !== undefined && receipt.blockNumber !== null) {
|
|
339
|
+
payload.blockNumber = receipt.blockNumber;
|
|
340
|
+
}
|
|
341
|
+
if (receipt.gasUsed !== undefined && receipt.gasUsed !== null) {
|
|
342
|
+
payload.gasUsed = receipt.gasUsed.toString();
|
|
343
|
+
}
|
|
344
|
+
// ethers v6 names the post-mining effective price `gasPrice`; tolerate
|
|
345
|
+
// either field name to stay forward/back-compat with SDK helper changes.
|
|
346
|
+
const gp = receipt.effectiveGasPrice ?? receipt.gasPrice ?? null;
|
|
347
|
+
if (gp && receipt.gasUsed) {
|
|
348
|
+
try {
|
|
349
|
+
const gwei = Number(gp) / 1e9;
|
|
350
|
+
const feeWei = receipt.gasUsed * gp;
|
|
351
|
+
const feeEth = Number(feeWei) / 1e18;
|
|
352
|
+
payload.gasPriceGwei = gwei.toFixed(3);
|
|
353
|
+
payload.feeEth = feeEth.toFixed(6);
|
|
354
|
+
if (typeof ethUsdPrice === 'number' && Number.isFinite(ethUsdPrice) && ethUsdPrice > 0) {
|
|
355
|
+
payload.feeUsd = `$${(feeEth * ethUsdPrice).toFixed(4)}`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Defensive — never let the receipt render throw. If the math fails
|
|
360
|
+
// (e.g. somebody passes a string instead of a bigint), just omit the
|
|
361
|
+
// enriched fields.
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (extra) {
|
|
365
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
366
|
+
payload[k] = v;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return payload;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Strip secrets that ethers and other transitively-thrown errors routinely
|
|
373
|
+
* embed in their `.message`. We redact:
|
|
374
|
+
* - `Bearer <token>` Authorization values (any caller hand-formatting an
|
|
375
|
+
* HTTP header into a thrown error).
|
|
376
|
+
* - Alchemy/Infura-style `/v<N>/<API_KEY>` path segments where the key is
|
|
377
|
+
* 20+ chars of safe URL chars — the standard shape for hosted RPCs.
|
|
378
|
+
* - `apiKey=<KEY>` query params (16+ chars) — generic key-in-querystring
|
|
379
|
+
* pattern used by several JSON-RPC providers.
|
|
380
|
+
* Intentionally narrow: we don't try to redact every possible secret, just
|
|
381
|
+
* the ones we know reliably leak into ethers' error surface.
|
|
382
|
+
*/
|
|
383
|
+
export function redactSecrets(s) {
|
|
384
|
+
return s
|
|
385
|
+
.replace(/Bearer\s+[A-Za-z0-9_\-\.=]+/g, 'Bearer <redacted>')
|
|
386
|
+
.replace(/\/v[0-9]+\/[A-Za-z0-9_\-]{20,}/g, (m) => {
|
|
387
|
+
const v = m.split('/')[1];
|
|
388
|
+
return `/${v}/<redacted>`;
|
|
389
|
+
})
|
|
390
|
+
.replace(/apiKey=[A-Za-z0-9_\-]{16,}/g, 'apiKey=<redacted>')
|
|
391
|
+
// Basic-auth credentials in URLs: https://user:pass@host (TNU-AUDIT-0083).
|
|
392
|
+
.replace(/(https?:\/\/)([^:/?#@\s]+):([^@\s]+)@/g, '$1<redacted>:<redacted>@')
|
|
393
|
+
// QuickNode-style host: `<random>.<chain>.quiknode.pro/<token>/`.
|
|
394
|
+
.replace(/([A-Za-z0-9_\-]+)\.([A-Za-z0-9_\-]+)\.quiknode\.pro\/[A-Za-z0-9_\-]+/g, '$1.$2.quiknode.pro/<redacted>')
|
|
395
|
+
// Etherscan-style 32-char uppercase API key on `apikey` query param.
|
|
396
|
+
.replace(/apikey=[A-Za-z0-9]{32,}/gi, 'apikey=<redacted>');
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Print an error to stderr. When `jsonErrors` is set, emits a single-line
|
|
400
|
+
* JSON object so scripts can parse failure modes.
|
|
401
|
+
*/
|
|
402
|
+
export function renderError(err, opts = {}) {
|
|
403
|
+
const e = err;
|
|
404
|
+
const rawMsg = e?.message ?? String(err);
|
|
405
|
+
const msg = redactSecrets(rawMsg);
|
|
406
|
+
if (opts.jsonErrors) {
|
|
407
|
+
const payload = {
|
|
408
|
+
error: msg,
|
|
409
|
+
code: e?.code,
|
|
410
|
+
};
|
|
411
|
+
process.stderr.write(JSON.stringify(payload, jsonReplacer) + '\n');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const useColor = !opts.noColor && process.stderr.isTTY;
|
|
415
|
+
const prefix = useColor ? pc.red('Error:') : 'Error:';
|
|
416
|
+
process.stderr.write(`${prefix} ${msg}\n`);
|
|
417
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thetanuts-finance/odette-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Odette command-line interface. Buy Odette options on Base from a terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"odette": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"NOTICE.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.build.json && chmod +x dist/index.js",
|
|
17
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
|
+
"odette": "node --conditions=odette-source --import tsx src/index.ts",
|
|
19
|
+
"prepack": "node ../../scripts/copy-package-docs.mjs",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"odette",
|
|
24
|
+
"thetanuts",
|
|
25
|
+
"options",
|
|
26
|
+
"cli",
|
|
27
|
+
"defi",
|
|
28
|
+
"base"
|
|
29
|
+
],
|
|
30
|
+
"author": "Thetanuts Finance",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Shawnchee/odette-sdk.git",
|
|
35
|
+
"directory": "packages/cli"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@thetanuts-finance/odette-sdk": "0.1.1",
|
|
45
|
+
"cli-table3": "0.6.5",
|
|
46
|
+
"commander": "12.1.0",
|
|
47
|
+
"ethers": "^6.16.0",
|
|
48
|
+
"picocolors": "1.1.1",
|
|
49
|
+
"prompts": "2.4.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/prompts": "2.4.9"
|
|
53
|
+
}
|
|
54
|
+
}
|