@zoralabs/cli 0.2.2 → 0.2.4
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/dist/index.js +4106 -0
- package/package.json +8 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,4106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
|
+
import { ExitPromptError } from "@inquirer/core";
|
|
6
|
+
import "fs";
|
|
7
|
+
import { setApiBaseUrl } from "@zoralabs/coins-sdk";
|
|
8
|
+
|
|
9
|
+
// src/commands/auth.ts
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
|
|
12
|
+
// src/lib/config.ts
|
|
13
|
+
import {
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
chmodSync
|
|
19
|
+
} from "fs";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import { homedir, platform } from "os";
|
|
22
|
+
function getConfigDir() {
|
|
23
|
+
if (platform() === "win32") {
|
|
24
|
+
return join(
|
|
25
|
+
process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
|
|
26
|
+
"zora"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return join(homedir(), ".config", "zora");
|
|
30
|
+
}
|
|
31
|
+
var CONFIG_DIR = getConfigDir();
|
|
32
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
33
|
+
var WALLET_FILE = join(CONFIG_DIR, "wallet.json");
|
|
34
|
+
var CONFIG_VERSION = 1;
|
|
35
|
+
var WALLET_VERSION = 1;
|
|
36
|
+
function assertVersion(parsed, expectedVersion, filePath) {
|
|
37
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
38
|
+
throw new Error(`${filePath}: expected an object`);
|
|
39
|
+
}
|
|
40
|
+
const obj = parsed;
|
|
41
|
+
if (!("version" in obj)) {
|
|
42
|
+
throw new Error(`${filePath}: missing required field "version"`);
|
|
43
|
+
}
|
|
44
|
+
if (obj.version !== expectedVersion) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${filePath}: unsupported version ${obj.version} (expected ${expectedVersion})`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
var configReadOnly = false;
|
|
51
|
+
function readConfig() {
|
|
52
|
+
if (!existsSync(CONFIG_FILE)) return { version: CONFIG_VERSION };
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(
|
|
58
|
+
`Warning: could not parse ${CONFIG_FILE}: ${err.message}. Run 'zora auth configure' to fix.`
|
|
59
|
+
);
|
|
60
|
+
configReadOnly = true;
|
|
61
|
+
return { version: CONFIG_VERSION };
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
assertVersion(parsed, CONFIG_VERSION, CONFIG_FILE);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(
|
|
67
|
+
`Warning: ${err.message}. Delete ${CONFIG_FILE} or run 'zora auth configure' to reset.`
|
|
68
|
+
);
|
|
69
|
+
configReadOnly = true;
|
|
70
|
+
return { version: CONFIG_VERSION };
|
|
71
|
+
}
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
var IS_WINDOWS = platform() === "win32";
|
|
75
|
+
function writeSecure(filePath, data) {
|
|
76
|
+
writeFileSync(filePath, data, IS_WINDOWS ? {} : { mode: 384 });
|
|
77
|
+
if (!IS_WINDOWS) {
|
|
78
|
+
chmodSync(filePath, 384);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function writeConfig(config) {
|
|
82
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
83
|
+
writeSecure(
|
|
84
|
+
CONFIG_FILE,
|
|
85
|
+
JSON.stringify({ ...config, version: CONFIG_VERSION }, null, 2) + "\n"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
function readWallet() {
|
|
89
|
+
if (!existsSync(WALLET_FILE)) return void 0;
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(readFileSync(WALLET_FILE, "utf-8"));
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw new Error(`${WALLET_FILE}: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
assertVersion(parsed, WALLET_VERSION, WALLET_FILE);
|
|
97
|
+
const obj = parsed;
|
|
98
|
+
if (typeof obj.privateKey !== "string" || !obj.privateKey) {
|
|
99
|
+
throw new Error(`${WALLET_FILE}: missing or invalid "privateKey" field`);
|
|
100
|
+
}
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
function writeWallet(wallet) {
|
|
104
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
105
|
+
writeSecure(
|
|
106
|
+
WALLET_FILE,
|
|
107
|
+
JSON.stringify({ ...wallet, version: WALLET_VERSION }, null, 2) + "\n"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function getEnvApiKey() {
|
|
111
|
+
const envKey = process.env.ZORA_API_KEY;
|
|
112
|
+
if (envKey === void 0) return void 0;
|
|
113
|
+
if (!envKey) {
|
|
114
|
+
console.error(
|
|
115
|
+
"ZORA_API_KEY is set but empty. Provide a valid key or unset the variable."
|
|
116
|
+
);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
return envKey;
|
|
120
|
+
}
|
|
121
|
+
function getApiKey() {
|
|
122
|
+
return getEnvApiKey() ?? readConfig().apiKey;
|
|
123
|
+
}
|
|
124
|
+
function saveApiKey(apiKey) {
|
|
125
|
+
const config = readConfig();
|
|
126
|
+
config.apiKey = apiKey;
|
|
127
|
+
writeConfig(config);
|
|
128
|
+
}
|
|
129
|
+
function getPrivateKey() {
|
|
130
|
+
return readWallet()?.privateKey;
|
|
131
|
+
}
|
|
132
|
+
function savePrivateKey(privateKey) {
|
|
133
|
+
writeWallet({ privateKey });
|
|
134
|
+
}
|
|
135
|
+
function getWalletPath() {
|
|
136
|
+
return WALLET_FILE;
|
|
137
|
+
}
|
|
138
|
+
function getAnalyticsId() {
|
|
139
|
+
return readConfig().analyticsId;
|
|
140
|
+
}
|
|
141
|
+
function saveAnalyticsId(id) {
|
|
142
|
+
if (configReadOnly) return;
|
|
143
|
+
const config = readConfig();
|
|
144
|
+
config.analyticsId = id;
|
|
145
|
+
writeConfig(config);
|
|
146
|
+
}
|
|
147
|
+
function getConfigPath() {
|
|
148
|
+
return CONFIG_FILE;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/lib/mask-key.ts
|
|
152
|
+
function maskKey(key) {
|
|
153
|
+
if (key.length <= 12) return "***";
|
|
154
|
+
return key.slice(0, 8) + "..." + key.slice(-4);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/lib/output.ts
|
|
158
|
+
var VALID_OUTPUT_MODES = ["table", "json", "live"];
|
|
159
|
+
var getOutputMode = (cmd, defaultMode) => {
|
|
160
|
+
const raw = cmd.optsWithGlobals().output;
|
|
161
|
+
if (!raw) return defaultMode;
|
|
162
|
+
if (VALID_OUTPUT_MODES.includes(raw)) return raw;
|
|
163
|
+
return outputErrorAndExit(
|
|
164
|
+
false,
|
|
165
|
+
`Invalid --output value: ${raw}.`,
|
|
166
|
+
`Supported: ${VALID_OUTPUT_MODES.join(", ")}`
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
var getJson = (cmd) => getOutputMode(cmd, "table") === "json";
|
|
170
|
+
var getYes = (cmd) => cmd.optsWithGlobals().yes ?? false;
|
|
171
|
+
var outputJson = (data) => {
|
|
172
|
+
console.log(JSON.stringify(data, null, 2));
|
|
173
|
+
};
|
|
174
|
+
var outputErrorAndExit = (json, message, suggestion) => {
|
|
175
|
+
if (json) {
|
|
176
|
+
const payload = { error: message };
|
|
177
|
+
if (suggestion) payload.suggestion = suggestion;
|
|
178
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
179
|
+
} else {
|
|
180
|
+
console.error(`\x1B[31mError:\x1B[0m ${message}`);
|
|
181
|
+
if (suggestion) {
|
|
182
|
+
console.error(`\x1B[2m${suggestion}\x1B[0m`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
process.exit(1);
|
|
186
|
+
};
|
|
187
|
+
var outputData = (json, opts) => {
|
|
188
|
+
if (json) {
|
|
189
|
+
outputJson(opts.json);
|
|
190
|
+
} else {
|
|
191
|
+
opts.table();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var getLiveConfig = (cmd, defaultMode) => {
|
|
195
|
+
const mode = getOutputMode(cmd, defaultMode);
|
|
196
|
+
const live = mode === "live";
|
|
197
|
+
const intervalRaw = parseInt(cmd.optsWithGlobals().interval, 10);
|
|
198
|
+
const intervalSeconds = isNaN(intervalRaw) || intervalRaw < 5 ? 30 : intervalRaw;
|
|
199
|
+
return { live, intervalSeconds };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// src/lib/prompt.ts
|
|
203
|
+
import confirm from "@inquirer/confirm";
|
|
204
|
+
import select from "@inquirer/select";
|
|
205
|
+
import password from "@inquirer/password";
|
|
206
|
+
var confirmOrDefault = async (opts, nonInteractive) => {
|
|
207
|
+
if (nonInteractive) return true;
|
|
208
|
+
return confirm(opts);
|
|
209
|
+
};
|
|
210
|
+
var selectOrDefault = async (opts, nonInteractive) => {
|
|
211
|
+
if (nonInteractive) return opts.default;
|
|
212
|
+
return select(opts);
|
|
213
|
+
};
|
|
214
|
+
var passwordOrFail = async (json, opts, nonInteractive) => {
|
|
215
|
+
if (nonInteractive) {
|
|
216
|
+
outputErrorAndExit(
|
|
217
|
+
json,
|
|
218
|
+
"This command requires interactive input. Remove --yes to proceed."
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return password(opts);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/lib/analytics.ts
|
|
225
|
+
import { PostHog } from "posthog-node";
|
|
226
|
+
import { createHash, randomUUID } from "crypto";
|
|
227
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
228
|
+
|
|
229
|
+
// src/lib/constants.ts
|
|
230
|
+
var BASE_CHAIN_ID = 8453;
|
|
231
|
+
var WETH_ADDRESS = "0x4200000000000000000000000000000000000006";
|
|
232
|
+
var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
233
|
+
var ZORA_ADDRESS = "0x1111111111166b7FE7bd91427724B487980aFc69";
|
|
234
|
+
var USDC_DECIMALS = 6;
|
|
235
|
+
var BASE_TRADE_TOKENS = {
|
|
236
|
+
eth: {
|
|
237
|
+
symbol: "ETH",
|
|
238
|
+
decimals: 18,
|
|
239
|
+
trade: { type: "eth" },
|
|
240
|
+
priceAddress: WETH_ADDRESS,
|
|
241
|
+
fixedPriceUsd: void 0
|
|
242
|
+
},
|
|
243
|
+
usdc: {
|
|
244
|
+
symbol: "USDC",
|
|
245
|
+
decimals: USDC_DECIMALS,
|
|
246
|
+
trade: {
|
|
247
|
+
type: "erc20",
|
|
248
|
+
address: USDC_ADDRESS
|
|
249
|
+
},
|
|
250
|
+
priceAddress: USDC_ADDRESS,
|
|
251
|
+
fixedPriceUsd: 1
|
|
252
|
+
},
|
|
253
|
+
zora: {
|
|
254
|
+
symbol: "ZORA",
|
|
255
|
+
decimals: 18,
|
|
256
|
+
trade: {
|
|
257
|
+
type: "erc20",
|
|
258
|
+
address: ZORA_ADDRESS
|
|
259
|
+
},
|
|
260
|
+
priceAddress: ZORA_ADDRESS,
|
|
261
|
+
fixedPriceUsd: void 0
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var POSTHOG_TOKEN = "phc_F3nLidy5mjn4xWQ6PYujO96MVig7UoszINhUUY0usOx";
|
|
265
|
+
var POSTHOG_HOST = "https://us.i.posthog.com";
|
|
266
|
+
|
|
267
|
+
// src/lib/wallet.ts
|
|
268
|
+
import { apiPost } from "@zoralabs/coins-sdk";
|
|
269
|
+
import { createPublicClient, createWalletClient, custom } from "viem";
|
|
270
|
+
import { base } from "viem/chains";
|
|
271
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
272
|
+
var normalizeKey = (key) => key.startsWith("0x") ? key : `0x${key}`;
|
|
273
|
+
var resolveAccount = (json = false) => {
|
|
274
|
+
const envKey = process.env.ZORA_PRIVATE_KEY;
|
|
275
|
+
const key = envKey || getPrivateKey();
|
|
276
|
+
if (!key) {
|
|
277
|
+
console.error(
|
|
278
|
+
"No wallet configured. Run 'zora setup' to create or import one."
|
|
279
|
+
);
|
|
280
|
+
return process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
return privateKeyToAccount(normalizeKey(key));
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error(
|
|
286
|
+
`\u2717 Invalid private key: ${err instanceof Error ? err.message : String(err)}`
|
|
287
|
+
);
|
|
288
|
+
console.error(" Run 'zora setup --force' to replace it.");
|
|
289
|
+
return process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
function formatRpcError(error) {
|
|
293
|
+
if (typeof error === "string") return error;
|
|
294
|
+
if (error instanceof Error) return error.message;
|
|
295
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
296
|
+
const message = error.message;
|
|
297
|
+
if (typeof message === "string") return message;
|
|
298
|
+
}
|
|
299
|
+
return JSON.stringify(error);
|
|
300
|
+
}
|
|
301
|
+
function createCliRpcTransport(chainId = base.id) {
|
|
302
|
+
return custom({
|
|
303
|
+
async request({
|
|
304
|
+
method,
|
|
305
|
+
params
|
|
306
|
+
}) {
|
|
307
|
+
let response;
|
|
308
|
+
try {
|
|
309
|
+
response = await apiPost("/cli-rpc", {
|
|
310
|
+
chainId,
|
|
311
|
+
method,
|
|
312
|
+
params: params ?? []
|
|
313
|
+
});
|
|
314
|
+
} catch (err) {
|
|
315
|
+
throw new Error(`CLI RPC request failed: ${formatRpcError(err)}`);
|
|
316
|
+
}
|
|
317
|
+
if (response.error) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`CLI RPC request failed: ${formatRpcError(response.error)}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const payload = response.data;
|
|
323
|
+
if (payload && typeof payload === "object" && "error" in payload && payload.error) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`CLI RPC request failed: ${formatRpcError(payload.error)}`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (payload && typeof payload === "object" && "result" in payload) {
|
|
329
|
+
return payload.result;
|
|
330
|
+
}
|
|
331
|
+
return payload;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function createClients(account) {
|
|
336
|
+
const transport = createCliRpcTransport();
|
|
337
|
+
const publicClient = createPublicClient({
|
|
338
|
+
chain: base,
|
|
339
|
+
transport
|
|
340
|
+
});
|
|
341
|
+
const walletClient = createWalletClient({
|
|
342
|
+
chain: base,
|
|
343
|
+
transport,
|
|
344
|
+
account
|
|
345
|
+
});
|
|
346
|
+
return { publicClient, walletClient };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/lib/analytics.ts
|
|
350
|
+
var SHUTDOWN_TIMEOUT_MS = 2e3;
|
|
351
|
+
var client = null;
|
|
352
|
+
var distinctId = null;
|
|
353
|
+
var isDisabled = () => process.env.ZORA_NO_ANALYTICS === "1" || process.env.DO_NOT_TRACK === "1" || process.env.CI !== void 0 || process.env.NODE_ENV === "test";
|
|
354
|
+
var getOrCreateDistinctId = () => {
|
|
355
|
+
if (distinctId) return distinctId;
|
|
356
|
+
try {
|
|
357
|
+
const stored = getAnalyticsId();
|
|
358
|
+
if (stored) {
|
|
359
|
+
distinctId = stored;
|
|
360
|
+
return distinctId;
|
|
361
|
+
}
|
|
362
|
+
distinctId = randomUUID();
|
|
363
|
+
saveAnalyticsId(distinctId);
|
|
364
|
+
return distinctId;
|
|
365
|
+
} catch {
|
|
366
|
+
distinctId = randomUUID();
|
|
367
|
+
return distinctId;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var getClient = () => {
|
|
371
|
+
if (!client) {
|
|
372
|
+
client = new PostHog(POSTHOG_TOKEN, { host: POSTHOG_HOST });
|
|
373
|
+
}
|
|
374
|
+
return client;
|
|
375
|
+
};
|
|
376
|
+
var commonProperties = () => ({
|
|
377
|
+
cli_version: true ? "0.2.4" : "development",
|
|
378
|
+
os: process.platform,
|
|
379
|
+
arch: process.arch,
|
|
380
|
+
node_version: process.version
|
|
381
|
+
});
|
|
382
|
+
var hashApiKey = (key) => createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
383
|
+
var getWalletAddress = () => {
|
|
384
|
+
try {
|
|
385
|
+
const key = process.env.ZORA_PRIVATE_KEY || getPrivateKey();
|
|
386
|
+
if (!key) return void 0;
|
|
387
|
+
return privateKeyToAccount2(normalizeKey(key)).address;
|
|
388
|
+
} catch {
|
|
389
|
+
return void 0;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
var identified = false;
|
|
393
|
+
var identify = () => {
|
|
394
|
+
try {
|
|
395
|
+
if (isDisabled() || identified) return;
|
|
396
|
+
identified = true;
|
|
397
|
+
const id = getOrCreateDistinctId();
|
|
398
|
+
const apiKey = getApiKey();
|
|
399
|
+
const walletAddress = getWalletAddress();
|
|
400
|
+
if (!apiKey && !walletAddress) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
getClient().identify({
|
|
404
|
+
distinctId: id,
|
|
405
|
+
properties: {
|
|
406
|
+
api_key_hash: apiKey ? hashApiKey(apiKey) : void 0,
|
|
407
|
+
wallet_address: walletAddress ?? void 0
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
var track = (event, properties) => {
|
|
414
|
+
try {
|
|
415
|
+
if (isDisabled()) return;
|
|
416
|
+
getClient().capture({
|
|
417
|
+
distinctId: getOrCreateDistinctId(),
|
|
418
|
+
event,
|
|
419
|
+
properties: { ...commonProperties(), ...properties }
|
|
420
|
+
});
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
var shutdownAnalytics = async () => {
|
|
425
|
+
if (!client) return;
|
|
426
|
+
const flushing = client;
|
|
427
|
+
client = null;
|
|
428
|
+
try {
|
|
429
|
+
await Promise.race([
|
|
430
|
+
flushing.shutdown(),
|
|
431
|
+
new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
|
|
432
|
+
]);
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/commands/auth.ts
|
|
438
|
+
var authCommand = new Command("auth").description(
|
|
439
|
+
"Manage API key authentication.\nAPI key is optional \u2014 without one, requests are rate-limited.\nGet a key at https://zora.co/settings/developer"
|
|
440
|
+
);
|
|
441
|
+
authCommand.command("configure").description("Set your Zora API key").option("--yes", "Skip interactive prompt and execute directly").action(async function() {
|
|
442
|
+
const json = getJson(this);
|
|
443
|
+
const nonInteractive = getYes(this);
|
|
444
|
+
if (getEnvApiKey()) {
|
|
445
|
+
outputData(json, {
|
|
446
|
+
json: {
|
|
447
|
+
status: "env_override",
|
|
448
|
+
message: "API key is set via ZORA_API_KEY environment variable."
|
|
449
|
+
},
|
|
450
|
+
table: () => console.log(
|
|
451
|
+
"API key is set via ZORA_API_KEY environment variable. Unset it to configure manually."
|
|
452
|
+
)
|
|
453
|
+
});
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const existing = getApiKey();
|
|
457
|
+
if (existing) {
|
|
458
|
+
console.log(`Current key: ${maskKey(existing)}`);
|
|
459
|
+
}
|
|
460
|
+
console.log("Get your API key from: https://zora.co/settings/developer\n");
|
|
461
|
+
const apiKey = await passwordOrFail(
|
|
462
|
+
json,
|
|
463
|
+
{ message: "Paste your API key:" },
|
|
464
|
+
nonInteractive
|
|
465
|
+
);
|
|
466
|
+
const trimmed = apiKey.trim();
|
|
467
|
+
if (!trimmed) {
|
|
468
|
+
outputErrorAndExit(
|
|
469
|
+
json,
|
|
470
|
+
"No API key provided.",
|
|
471
|
+
"Usage: zora auth configure"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
saveApiKey(trimmed);
|
|
476
|
+
outputData(json, {
|
|
477
|
+
json: { saved: true, path: getConfigPath() },
|
|
478
|
+
table: () => console.log(`API key saved to ${getConfigPath()}`)
|
|
479
|
+
});
|
|
480
|
+
track("cli_auth_configure", {
|
|
481
|
+
output_format: json ? "json" : "text"
|
|
482
|
+
});
|
|
483
|
+
} catch (err) {
|
|
484
|
+
outputErrorAndExit(
|
|
485
|
+
json,
|
|
486
|
+
`Failed to save API key: ${err.message}`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
authCommand.command("status").description("Check authentication status").action(function() {
|
|
491
|
+
const json = getJson(this);
|
|
492
|
+
const apiKey = getApiKey();
|
|
493
|
+
if (!apiKey) {
|
|
494
|
+
outputData(json, {
|
|
495
|
+
json: { authenticated: false },
|
|
496
|
+
table: () => {
|
|
497
|
+
console.log(
|
|
498
|
+
"No API key configured. The CLI works without one, but requests are rate-limited."
|
|
499
|
+
);
|
|
500
|
+
console.log(
|
|
501
|
+
"Run 'zora auth configure' to set an API key for higher rate limits."
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
track("cli_auth_status", {
|
|
506
|
+
authenticated: false,
|
|
507
|
+
source: null,
|
|
508
|
+
output_format: json ? "json" : "text"
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const source = getEnvApiKey() ? "env (ZORA_API_KEY)" : getConfigPath();
|
|
513
|
+
outputData(json, {
|
|
514
|
+
json: { authenticated: true, key: maskKey(apiKey), source },
|
|
515
|
+
table: () => {
|
|
516
|
+
console.log(`Authenticated: ${maskKey(apiKey)}`);
|
|
517
|
+
console.log(`Source: ${source}`);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
track("cli_auth_status", {
|
|
521
|
+
authenticated: true,
|
|
522
|
+
source: getEnvApiKey() ? "env" : "file",
|
|
523
|
+
output_format: json ? "json" : "text"
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// src/commands/balance.tsx
|
|
528
|
+
import { Command as Command2 } from "commander";
|
|
529
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
530
|
+
import { getProfileBalances, setApiKey } from "@zoralabs/coins-sdk";
|
|
531
|
+
|
|
532
|
+
// src/lib/render.tsx
|
|
533
|
+
import { render, renderToString } from "ink";
|
|
534
|
+
var renderOnce = (element) => {
|
|
535
|
+
const columns = process.stdout.columns || 80;
|
|
536
|
+
const output = renderToString(element, { columns });
|
|
537
|
+
console.log(output);
|
|
538
|
+
};
|
|
539
|
+
var renderLive = async (element) => {
|
|
540
|
+
const instance = render(element);
|
|
541
|
+
await instance.waitUntilExit();
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/components/BalanceView.tsx
|
|
545
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef } from "react";
|
|
546
|
+
import { Box as Box2, Text as Text2, useInput, useApp } from "ink";
|
|
547
|
+
import Spinner from "ink-spinner";
|
|
548
|
+
|
|
549
|
+
// src/components/table.tsx
|
|
550
|
+
import { Box, Text } from "ink";
|
|
551
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
552
|
+
var PADDING_LEFT = 1;
|
|
553
|
+
var truncate = (str, max) => {
|
|
554
|
+
if (str.length <= max) return str;
|
|
555
|
+
return str.slice(0, max - 1) + "\u2026";
|
|
556
|
+
};
|
|
557
|
+
var computeColumnWidths = (columns, fullWidth) => {
|
|
558
|
+
const baseWidths = columns.map((col) => col.width);
|
|
559
|
+
const totalBase = baseWidths.reduce((sum, w) => sum + w, 0);
|
|
560
|
+
if (!fullWidth) return baseWidths;
|
|
561
|
+
const width = process.stdout.columns ?? 80;
|
|
562
|
+
const available = width - PADDING_LEFT;
|
|
563
|
+
if (available <= totalBase) return baseWidths;
|
|
564
|
+
const computed = baseWidths.map(
|
|
565
|
+
(w) => Math.max(1, Math.floor(w / totalBase * available))
|
|
566
|
+
);
|
|
567
|
+
let remainder = available - computed.reduce((sum, w) => sum + w, 0);
|
|
568
|
+
for (let i = 0; i < computed.length && remainder > 0; i++) {
|
|
569
|
+
computed[i]++;
|
|
570
|
+
remainder--;
|
|
571
|
+
}
|
|
572
|
+
return computed;
|
|
573
|
+
};
|
|
574
|
+
var Table = ({
|
|
575
|
+
columns,
|
|
576
|
+
data,
|
|
577
|
+
title,
|
|
578
|
+
subtitle,
|
|
579
|
+
fullWidth = true,
|
|
580
|
+
footer
|
|
581
|
+
}) => {
|
|
582
|
+
const widths = computeColumnWidths(columns, fullWidth);
|
|
583
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1, children: [
|
|
584
|
+
title && /* @__PURE__ */ jsxs(Box, { paddingLeft: PADDING_LEFT, marginBottom: 1, children: [
|
|
585
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: title }),
|
|
586
|
+
subtitle && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
587
|
+
" ",
|
|
588
|
+
subtitle
|
|
589
|
+
] })
|
|
590
|
+
] }),
|
|
591
|
+
/* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, children: columns.map((col, i) => /* @__PURE__ */ jsx(Box, { width: widths[i], children: /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, wrap: "truncate", children: col.header }) }, col.header)) }),
|
|
592
|
+
data.map((row, i) => /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, children: columns.map((col, colIdx) => {
|
|
593
|
+
const colWidth = widths[colIdx];
|
|
594
|
+
const value = col.noTruncate ? col.accessor(row) : truncate(col.accessor(row), colWidth - 2);
|
|
595
|
+
const colorName = col.color?.(row);
|
|
596
|
+
return /* @__PURE__ */ jsx(Box, { width: colWidth, children: /* @__PURE__ */ jsx(
|
|
597
|
+
Text,
|
|
598
|
+
{
|
|
599
|
+
color: colorName,
|
|
600
|
+
wrap: col.noTruncate ? "wrap" : "truncate",
|
|
601
|
+
children: value
|
|
602
|
+
}
|
|
603
|
+
) }, col.header);
|
|
604
|
+
}) }, i)),
|
|
605
|
+
footer && /* @__PURE__ */ jsx(Box, { paddingLeft: PADDING_LEFT, marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "wrap", children: footer }) })
|
|
606
|
+
] });
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/lib/format.ts
|
|
610
|
+
import { format, formatDistanceStrict } from "date-fns";
|
|
611
|
+
import { formatEther } from "viem";
|
|
612
|
+
function formatCompactUsd(value) {
|
|
613
|
+
if (!value || Number(value) === 0) return "$0";
|
|
614
|
+
return new Intl.NumberFormat("en-US", {
|
|
615
|
+
style: "currency",
|
|
616
|
+
currency: "USD",
|
|
617
|
+
notation: "compact",
|
|
618
|
+
minimumFractionDigits: 1,
|
|
619
|
+
maximumFractionDigits: 1
|
|
620
|
+
}).format(Number(value));
|
|
621
|
+
}
|
|
622
|
+
var NO_CHANGE = { text: "-", color: void 0 };
|
|
623
|
+
function formatMcapChange(marketCap, delta) {
|
|
624
|
+
if (!delta || !marketCap) return NO_CHANGE;
|
|
625
|
+
const currentMCap = Number(marketCap);
|
|
626
|
+
const absoluteDelta = Number(delta);
|
|
627
|
+
const previousMCap = currentMCap - absoluteDelta;
|
|
628
|
+
if (currentMCap === 0 || previousMCap === 0) return NO_CHANGE;
|
|
629
|
+
const percentChange = absoluteDelta / previousMCap * 100;
|
|
630
|
+
const plusPrefix = percentChange >= 0 ? "+" : "";
|
|
631
|
+
const text = `${plusPrefix}${percentChange.toFixed(1)}%`;
|
|
632
|
+
const color = percentChange > 0 ? "green" : percentChange < 0 ? "red" : void 0;
|
|
633
|
+
return { text, color };
|
|
634
|
+
}
|
|
635
|
+
function formatUsd(value) {
|
|
636
|
+
return new Intl.NumberFormat("en-US", {
|
|
637
|
+
style: "currency",
|
|
638
|
+
currency: "USD",
|
|
639
|
+
minimumFractionDigits: 2,
|
|
640
|
+
maximumFractionDigits: 2
|
|
641
|
+
}).format(value);
|
|
642
|
+
}
|
|
643
|
+
function formatHolders(count) {
|
|
644
|
+
return new Intl.NumberFormat("en-US").format(count);
|
|
645
|
+
}
|
|
646
|
+
function formatRelativeTime(date, now = /* @__PURE__ */ new Date()) {
|
|
647
|
+
const diffMs = now.getTime() - date.getTime();
|
|
648
|
+
if (diffMs < 6e4) return "just now";
|
|
649
|
+
return formatDistanceStrict(date, now, { addSuffix: true });
|
|
650
|
+
}
|
|
651
|
+
function formatAbsoluteTime(date) {
|
|
652
|
+
return format(date, "yyyy-MM-dd h:mm a");
|
|
653
|
+
}
|
|
654
|
+
function formatCreatedAt(isoDate, now) {
|
|
655
|
+
if (!isoDate) return "-";
|
|
656
|
+
const date = new Date(isoDate);
|
|
657
|
+
if (isNaN(date.getTime())) return "-";
|
|
658
|
+
return `${formatRelativeTime(date, now)} (${formatAbsoluteTime(date)})`;
|
|
659
|
+
}
|
|
660
|
+
var formatEthDisplay = (wei) => {
|
|
661
|
+
const eth = formatEther(wei);
|
|
662
|
+
const parts = eth.split(".");
|
|
663
|
+
if (!parts[1]) return eth;
|
|
664
|
+
const trimmed = parts[1].replace(/0+$/, "") || "0";
|
|
665
|
+
return `${parts[0]}.${trimmed}`;
|
|
666
|
+
};
|
|
667
|
+
var formatCoinsDisplay = (coinsOut) => new Intl.NumberFormat("en-US", {
|
|
668
|
+
maximumFractionDigits: 2
|
|
669
|
+
}).format(Number(coinsOut));
|
|
670
|
+
|
|
671
|
+
// src/lib/balance-format.ts
|
|
672
|
+
var COIN_DECIMALS = 18;
|
|
673
|
+
var parseRawBalance = (rawBalance) => Number(normalizeTokenAmount(rawBalance));
|
|
674
|
+
var normalizeTokenAmount = (rawBalance, decimals = COIN_DECIMALS) => {
|
|
675
|
+
try {
|
|
676
|
+
const value = BigInt(rawBalance);
|
|
677
|
+
const divisor = 10n ** BigInt(decimals);
|
|
678
|
+
const whole = value / divisor;
|
|
679
|
+
const fraction = value % divisor;
|
|
680
|
+
if (fraction === 0n) return whole.toString();
|
|
681
|
+
const fractionText = fraction.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
682
|
+
return `${whole}.${fractionText}`;
|
|
683
|
+
} catch {
|
|
684
|
+
console.warn(`Warning: could not parse token amount "${rawBalance}"`);
|
|
685
|
+
return rawBalance;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
var formatBalanceAsUsd = (balance, priceInUsdc) => {
|
|
689
|
+
if (!priceInUsdc) return "-";
|
|
690
|
+
const value = parseRawBalance(balance) * Number(priceInUsdc);
|
|
691
|
+
if (value < 0.01) return "<$0.01";
|
|
692
|
+
return formatUsd(value);
|
|
693
|
+
};
|
|
694
|
+
var formatBalance = (balance) => {
|
|
695
|
+
const n = parseRawBalance(balance);
|
|
696
|
+
if (n === 0) return "0";
|
|
697
|
+
if (n < 1e-3) return "<0.001";
|
|
698
|
+
if (n < 1) return n.toFixed(4);
|
|
699
|
+
return new Intl.NumberFormat("en-US", {
|
|
700
|
+
notation: "compact",
|
|
701
|
+
compactDisplay: "short",
|
|
702
|
+
maximumFractionDigits: 1
|
|
703
|
+
}).format(n);
|
|
704
|
+
};
|
|
705
|
+
var trimTrailingZeros = (value) => {
|
|
706
|
+
if (!value.includes(".")) return value;
|
|
707
|
+
const trimmed = value.replace(/0+$/, "").replace(/\.$/, "");
|
|
708
|
+
return trimmed || "0";
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// src/lib/balance-columns.tsx
|
|
712
|
+
var SORT_LABELS = {
|
|
713
|
+
"usd-value": "USD Value",
|
|
714
|
+
balance: "Balance",
|
|
715
|
+
"market-cap": "Market Cap",
|
|
716
|
+
"price-change": "Price Change"
|
|
717
|
+
};
|
|
718
|
+
var walletColumns = [
|
|
719
|
+
{ header: "Name", width: 14, accessor: (row) => row.name },
|
|
720
|
+
{
|
|
721
|
+
header: "Symbol",
|
|
722
|
+
width: 10,
|
|
723
|
+
noTruncate: true,
|
|
724
|
+
accessor: (row) => row.symbol
|
|
725
|
+
},
|
|
726
|
+
{ header: "Balance", width: 20, accessor: (row) => row.balance },
|
|
727
|
+
{ header: "USD Value", width: 16, accessor: (row) => row.usdValue }
|
|
728
|
+
];
|
|
729
|
+
var balanceColumns = [
|
|
730
|
+
{ header: "#", width: 5, accessor: (row) => String(row.rank) },
|
|
731
|
+
{
|
|
732
|
+
header: "Name",
|
|
733
|
+
width: 24,
|
|
734
|
+
accessor: (row) => row.coin?.name ?? "Unknown"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
header: "Symbol",
|
|
738
|
+
width: 12,
|
|
739
|
+
noTruncate: true,
|
|
740
|
+
accessor: (row) => row.coin?.symbol ?? ""
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
header: "Balance",
|
|
744
|
+
width: 14,
|
|
745
|
+
accessor: (row) => formatBalance(row.balance)
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
header: "USD Value",
|
|
749
|
+
width: 14,
|
|
750
|
+
accessor: (row) => formatBalanceAsUsd(row.balance, row.coin?.tokenPrice?.priceInUsdc)
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
header: "Market Cap",
|
|
754
|
+
width: 14,
|
|
755
|
+
accessor: (row) => formatCompactUsd(row.coin?.marketCap)
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
header: "24h Change",
|
|
759
|
+
width: 12,
|
|
760
|
+
accessor: (row) => formatMcapChange(row.coin?.marketCap, row.coin?.marketCapDelta24h).text,
|
|
761
|
+
color: (row) => formatMcapChange(row.coin?.marketCap, row.coin?.marketCapDelta24h).color
|
|
762
|
+
}
|
|
763
|
+
];
|
|
764
|
+
|
|
765
|
+
// src/hooks/use-auto-refresh.ts
|
|
766
|
+
import { useState, useEffect, useCallback } from "react";
|
|
767
|
+
var useAutoRefresh = (intervalSeconds, enabled) => {
|
|
768
|
+
const [refreshCount, setRefreshCount] = useState(0);
|
|
769
|
+
const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(intervalSeconds);
|
|
770
|
+
const [resetCount, setResetCount] = useState(0);
|
|
771
|
+
const triggerManualRefresh = useCallback(() => {
|
|
772
|
+
if (!enabled) return;
|
|
773
|
+
setRefreshCount((c) => c + 1);
|
|
774
|
+
setSecondsUntilRefresh(intervalSeconds);
|
|
775
|
+
setResetCount((c) => c + 1);
|
|
776
|
+
}, [enabled, intervalSeconds]);
|
|
777
|
+
useEffect(() => {
|
|
778
|
+
if (!enabled) return;
|
|
779
|
+
setSecondsUntilRefresh(intervalSeconds);
|
|
780
|
+
const ticker = setInterval(() => {
|
|
781
|
+
setSecondsUntilRefresh((prev) => {
|
|
782
|
+
if (prev <= 1) {
|
|
783
|
+
setRefreshCount((c) => c + 1);
|
|
784
|
+
return intervalSeconds;
|
|
785
|
+
}
|
|
786
|
+
return prev - 1;
|
|
787
|
+
});
|
|
788
|
+
}, 1e3);
|
|
789
|
+
return () => clearInterval(ticker);
|
|
790
|
+
}, [enabled, intervalSeconds, resetCount]);
|
|
791
|
+
if (!enabled) {
|
|
792
|
+
return { refreshCount: 0, secondsUntilRefresh: 0, triggerManualRefresh };
|
|
793
|
+
}
|
|
794
|
+
return { refreshCount, secondsUntilRefresh, triggerManualRefresh };
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
// src/components/BalanceView.tsx
|
|
798
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
799
|
+
var BalanceView = ({
|
|
800
|
+
fetchData,
|
|
801
|
+
sort,
|
|
802
|
+
mode = "full",
|
|
803
|
+
autoRefresh = false,
|
|
804
|
+
intervalSeconds = 30
|
|
805
|
+
}) => {
|
|
806
|
+
const { exit } = useApp();
|
|
807
|
+
const [loading, setLoading] = useState2(true);
|
|
808
|
+
const [isRefreshing, setIsRefreshing] = useState2(false);
|
|
809
|
+
const [error, setError] = useState2(null);
|
|
810
|
+
const [data, setData] = useState2(null);
|
|
811
|
+
const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
|
|
812
|
+
const [manualRefreshCount, setManualRefreshCount] = useState2(0);
|
|
813
|
+
const hasLoadedOnce = useRef(false);
|
|
814
|
+
const load = useCallback2(async () => {
|
|
815
|
+
if (hasLoadedOnce.current) {
|
|
816
|
+
setIsRefreshing(true);
|
|
817
|
+
} else {
|
|
818
|
+
setLoading(true);
|
|
819
|
+
}
|
|
820
|
+
setError(null);
|
|
821
|
+
try {
|
|
822
|
+
const result = await fetchData();
|
|
823
|
+
setData(result);
|
|
824
|
+
hasLoadedOnce.current = true;
|
|
825
|
+
} catch (err) {
|
|
826
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
827
|
+
}
|
|
828
|
+
setLoading(false);
|
|
829
|
+
setIsRefreshing(false);
|
|
830
|
+
}, [fetchData]);
|
|
831
|
+
useEffect2(() => {
|
|
832
|
+
load();
|
|
833
|
+
}, [refreshCount, manualRefreshCount]);
|
|
834
|
+
useInput((input, key) => {
|
|
835
|
+
if (input === "q" || key.escape) {
|
|
836
|
+
exit();
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (input === "r" && !loading) {
|
|
840
|
+
triggerManualRefresh();
|
|
841
|
+
setManualRefreshCount((c) => c + 1);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
if (error && !data) {
|
|
845
|
+
return /* @__PURE__ */ jsxs2(
|
|
846
|
+
Box2,
|
|
847
|
+
{
|
|
848
|
+
flexDirection: "column",
|
|
849
|
+
paddingLeft: 1,
|
|
850
|
+
paddingTop: 1,
|
|
851
|
+
paddingBottom: 1,
|
|
852
|
+
children: [
|
|
853
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
|
|
854
|
+
"Error: ",
|
|
855
|
+
error
|
|
856
|
+
] }),
|
|
857
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Press q to exit" }) })
|
|
858
|
+
]
|
|
859
|
+
}
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
if (loading && !data) {
|
|
863
|
+
return /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
864
|
+
/* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
|
|
865
|
+
" Loading\u2026"
|
|
866
|
+
] }) });
|
|
867
|
+
}
|
|
868
|
+
if (!data) return null;
|
|
869
|
+
const hints = ["r refresh"];
|
|
870
|
+
if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
|
|
871
|
+
hints.push("q quit");
|
|
872
|
+
const footer = hints.join(" \xB7 ");
|
|
873
|
+
const showWallet = mode === "full" || mode === "wallet";
|
|
874
|
+
const showCoins = mode === "full" || mode === "coins";
|
|
875
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
876
|
+
isRefreshing && /* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
877
|
+
/* @__PURE__ */ jsx2(Spinner, { type: "dots" }),
|
|
878
|
+
" Refreshing\u2026"
|
|
879
|
+
] }) }),
|
|
880
|
+
showWallet && /* @__PURE__ */ jsx2(
|
|
881
|
+
Table,
|
|
882
|
+
{
|
|
883
|
+
columns: walletColumns,
|
|
884
|
+
data: data.walletBalances,
|
|
885
|
+
title: "Wallet"
|
|
886
|
+
}
|
|
887
|
+
),
|
|
888
|
+
showCoins && data.rankedBalances.length === 0 ? /* @__PURE__ */ jsxs2(
|
|
889
|
+
Box2,
|
|
890
|
+
{
|
|
891
|
+
flexDirection: "column",
|
|
892
|
+
paddingLeft: 1,
|
|
893
|
+
paddingTop: 1,
|
|
894
|
+
paddingBottom: 1,
|
|
895
|
+
children: [
|
|
896
|
+
/* @__PURE__ */ jsx2(Text2, { children: "No coin balances found." }),
|
|
897
|
+
/* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
|
|
898
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Buy coins to see them here:" }),
|
|
899
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
900
|
+
" zora buy ",
|
|
901
|
+
"<address>",
|
|
902
|
+
" --eth 0.001"
|
|
903
|
+
] })
|
|
904
|
+
] })
|
|
905
|
+
]
|
|
906
|
+
}
|
|
907
|
+
) : showCoins ? /* @__PURE__ */ jsx2(
|
|
908
|
+
Table,
|
|
909
|
+
{
|
|
910
|
+
columns: balanceColumns,
|
|
911
|
+
data: data.rankedBalances,
|
|
912
|
+
title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
|
|
913
|
+
subtitle: `${data.rankedBalances.length} of ${data.total}`
|
|
914
|
+
}
|
|
915
|
+
) : null,
|
|
916
|
+
/* @__PURE__ */ jsx2(Box2, { paddingLeft: 1, paddingBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: footer }) })
|
|
917
|
+
] });
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// src/lib/wallet-balances.ts
|
|
921
|
+
import { getTokenInfo } from "@zoralabs/coins-sdk";
|
|
922
|
+
import {
|
|
923
|
+
createPublicClient as createPublicClient2,
|
|
924
|
+
erc20Abi,
|
|
925
|
+
formatUnits,
|
|
926
|
+
http
|
|
927
|
+
} from "viem";
|
|
928
|
+
import { base as base2 } from "viem/chains";
|
|
929
|
+
var TRACKED_TOKENS = [
|
|
930
|
+
{
|
|
931
|
+
name: "Ether",
|
|
932
|
+
symbol: "ETH",
|
|
933
|
+
address: WETH_ADDRESS,
|
|
934
|
+
decimals: 18,
|
|
935
|
+
priceAddress: WETH_ADDRESS,
|
|
936
|
+
isNative: true
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "USD Coin",
|
|
940
|
+
symbol: "USDC",
|
|
941
|
+
address: USDC_ADDRESS,
|
|
942
|
+
decimals: USDC_DECIMALS,
|
|
943
|
+
priceAddress: USDC_ADDRESS,
|
|
944
|
+
fixedPriceUsd: 1
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: "ZORA",
|
|
948
|
+
symbol: "ZORA",
|
|
949
|
+
address: ZORA_ADDRESS,
|
|
950
|
+
decimals: 18,
|
|
951
|
+
priceAddress: ZORA_ADDRESS
|
|
952
|
+
}
|
|
953
|
+
];
|
|
954
|
+
var fetchTokenPriceUsd = async (address, chainId = BASE_CHAIN_ID) => {
|
|
955
|
+
try {
|
|
956
|
+
const res = await getTokenInfo({ address, chainId });
|
|
957
|
+
return res.data?.erc20Token?.currency?.priceUsd ? Number(res.data.erc20Token.currency.priceUsd) : null;
|
|
958
|
+
} catch (err) {
|
|
959
|
+
console.warn(
|
|
960
|
+
`Warning: failed to fetch price for ${address}: ${err instanceof Error ? err.message : String(err)}`
|
|
961
|
+
);
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
var fetchWalletBalances = async (walletAddress) => {
|
|
966
|
+
const publicClient = createPublicClient2({ chain: base2, transport: http() });
|
|
967
|
+
const nativeToken = TRACKED_TOKENS.find((t) => t.isNative);
|
|
968
|
+
const erc20Tokens = TRACKED_TOKENS.filter((t) => !t.isNative);
|
|
969
|
+
const [ethBalance, multicallResults] = await Promise.all([
|
|
970
|
+
publicClient.getBalance({ address: walletAddress }),
|
|
971
|
+
publicClient.multicall({
|
|
972
|
+
contracts: erc20Tokens.map((t) => ({
|
|
973
|
+
address: t.address,
|
|
974
|
+
abi: erc20Abi,
|
|
975
|
+
functionName: "balanceOf",
|
|
976
|
+
args: [walletAddress]
|
|
977
|
+
}))
|
|
978
|
+
})
|
|
979
|
+
]);
|
|
980
|
+
const rawBalances = /* @__PURE__ */ new Map();
|
|
981
|
+
if (nativeToken) rawBalances.set(nativeToken, ethBalance);
|
|
982
|
+
erc20Tokens.forEach((token, i) => {
|
|
983
|
+
if (multicallResults[i].status === "success") {
|
|
984
|
+
rawBalances.set(token, multicallResults[i].result);
|
|
985
|
+
} else {
|
|
986
|
+
console.warn(`Warning: failed to fetch balance for ${token.symbol}`);
|
|
987
|
+
rawBalances.set(token, 0n);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
const priceResults = await Promise.allSettled(
|
|
991
|
+
TRACKED_TOKENS.map(async (token) => {
|
|
992
|
+
const balance = rawBalances.get(token) ?? 0n;
|
|
993
|
+
let priceUsd = null;
|
|
994
|
+
if (token.fixedPriceUsd != null) {
|
|
995
|
+
priceUsd = token.fixedPriceUsd;
|
|
996
|
+
} else if (balance > 0n || token.isNative) {
|
|
997
|
+
priceUsd = await fetchTokenPriceUsd(token.priceAddress);
|
|
998
|
+
}
|
|
999
|
+
return { token, balance, priceUsd };
|
|
1000
|
+
})
|
|
1001
|
+
);
|
|
1002
|
+
const resolved = priceResults.map((result, i) => {
|
|
1003
|
+
if (result.status === "fulfilled") return result.value;
|
|
1004
|
+
const token = TRACKED_TOKENS[i];
|
|
1005
|
+
console.warn(`Warning: failed to resolve token ${token.symbol}`);
|
|
1006
|
+
return { token, balance: rawBalances.get(token) ?? 0n, priceUsd: null };
|
|
1007
|
+
});
|
|
1008
|
+
const visible = resolved.filter((r) => r.balance > 0n || r.token.isNative);
|
|
1009
|
+
const intermediate = visible.map(({ token, balance, priceUsd }) => {
|
|
1010
|
+
const human = formatUnits(balance, token.decimals);
|
|
1011
|
+
const usdValue = priceUsd !== null ? Number(human) * priceUsd : null;
|
|
1012
|
+
return { token, human, priceUsd, usdValue };
|
|
1013
|
+
});
|
|
1014
|
+
const walletBalances = intermediate.map(
|
|
1015
|
+
({ token, human, usdValue }) => ({
|
|
1016
|
+
name: token.name,
|
|
1017
|
+
symbol: token.symbol,
|
|
1018
|
+
balance: trimTrailingZeros(human),
|
|
1019
|
+
usdValue: usdValue !== null ? formatUsd(usdValue) : "-"
|
|
1020
|
+
})
|
|
1021
|
+
);
|
|
1022
|
+
const walletBalancesJson = intermediate.map(
|
|
1023
|
+
({ token, human, priceUsd, usdValue }) => ({
|
|
1024
|
+
name: token.name,
|
|
1025
|
+
symbol: token.symbol,
|
|
1026
|
+
address: token.isNative ? null : token.address,
|
|
1027
|
+
balance: trimTrailingZeros(human),
|
|
1028
|
+
priceUsd,
|
|
1029
|
+
usdValue: usdValue !== null ? Number(usdValue.toFixed(6)) : null
|
|
1030
|
+
})
|
|
1031
|
+
);
|
|
1032
|
+
return { walletBalances, walletBalancesJson };
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// src/commands/balance.tsx
|
|
1036
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1037
|
+
var SORT_MAP = {
|
|
1038
|
+
"usd-value": "USD_VALUE",
|
|
1039
|
+
balance: "BALANCE",
|
|
1040
|
+
"market-cap": "MARKET_CAP",
|
|
1041
|
+
"price-change": "PRICE_CHANGE"
|
|
1042
|
+
};
|
|
1043
|
+
var SORT_OPTIONS = Object.keys(SORT_LABELS).join(", ");
|
|
1044
|
+
var extractErrorMessage = (error) => {
|
|
1045
|
+
if (typeof error === "object" && error !== null && "error" in error) {
|
|
1046
|
+
return String(error.error);
|
|
1047
|
+
}
|
|
1048
|
+
return JSON.stringify(error);
|
|
1049
|
+
};
|
|
1050
|
+
var formatBalanceJson = (balance, rank) => {
|
|
1051
|
+
const priceUsd = balance.coin?.tokenPrice?.priceInUsdc;
|
|
1052
|
+
const marketCap = balance.coin?.marketCap ? Number(balance.coin.marketCap) : null;
|
|
1053
|
+
const marketCapDelta24h = balance.coin?.marketCapDelta24h ? Number(balance.coin.marketCapDelta24h) : null;
|
|
1054
|
+
const volume24h = balance.coin?.volume24h ? Number(balance.coin.volume24h) : null;
|
|
1055
|
+
const totalVolume = balance.coin?.totalVolume ? Number(balance.coin.totalVolume) : null;
|
|
1056
|
+
const priceUsdValue = priceUsd ? Number(priceUsd) : null;
|
|
1057
|
+
const usdValue = priceUsdValue !== null ? Number((parseRawBalance(balance.balance) * priceUsdValue).toFixed(6)) : null;
|
|
1058
|
+
const marketCapChange24h = marketCap !== null && marketCapDelta24h !== null && marketCap - marketCapDelta24h !== 0 ? Number(
|
|
1059
|
+
(marketCapDelta24h / (marketCap - marketCapDelta24h) * 100).toFixed(
|
|
1060
|
+
4
|
|
1061
|
+
)
|
|
1062
|
+
) : null;
|
|
1063
|
+
return {
|
|
1064
|
+
rank,
|
|
1065
|
+
name: balance.coin?.name ?? null,
|
|
1066
|
+
symbol: balance.coin?.symbol ?? null,
|
|
1067
|
+
coinType: balance.coin?.coinType ?? null,
|
|
1068
|
+
chainId: balance.coin?.chainId ?? null,
|
|
1069
|
+
address: balance.coin?.address ?? null,
|
|
1070
|
+
creatorHandle: balance.coin?.creatorProfile?.handle ?? null,
|
|
1071
|
+
previewImage: balance.coin?.mediaContent?.previewImage?.medium ?? null,
|
|
1072
|
+
balance: normalizeTokenAmount(balance.balance),
|
|
1073
|
+
usdValue,
|
|
1074
|
+
priceUsd: priceUsdValue,
|
|
1075
|
+
marketCap,
|
|
1076
|
+
marketCapDelta24h,
|
|
1077
|
+
marketCapChange24h,
|
|
1078
|
+
volume24h,
|
|
1079
|
+
totalVolume
|
|
1080
|
+
};
|
|
1081
|
+
};
|
|
1082
|
+
function resolveContext(json) {
|
|
1083
|
+
const account = resolveAccount(json);
|
|
1084
|
+
const apiKey = getApiKey();
|
|
1085
|
+
if (!apiKey) {
|
|
1086
|
+
outputErrorAndExit(
|
|
1087
|
+
json,
|
|
1088
|
+
"Not authenticated. Run 'zora auth configure' to set your API key."
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
setApiKey(apiKey);
|
|
1092
|
+
return account;
|
|
1093
|
+
}
|
|
1094
|
+
function renderWallet(json, walletResult) {
|
|
1095
|
+
outputData(json, {
|
|
1096
|
+
json: { wallet: walletResult.walletBalancesJson },
|
|
1097
|
+
table: () => {
|
|
1098
|
+
renderOnce(
|
|
1099
|
+
/* @__PURE__ */ jsx3(
|
|
1100
|
+
Table,
|
|
1101
|
+
{
|
|
1102
|
+
columns: walletColumns,
|
|
1103
|
+
data: walletResult.walletBalances,
|
|
1104
|
+
title: "Wallet"
|
|
1105
|
+
}
|
|
1106
|
+
)
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
function renderCoins(json, balances, total, sort) {
|
|
1112
|
+
const rankedBalances = balances.map((balance, index) => ({
|
|
1113
|
+
...balance,
|
|
1114
|
+
rank: index + 1
|
|
1115
|
+
}));
|
|
1116
|
+
outputData(json, {
|
|
1117
|
+
json: {
|
|
1118
|
+
coins: rankedBalances.map(
|
|
1119
|
+
(balance) => formatBalanceJson(balance, balance.rank)
|
|
1120
|
+
)
|
|
1121
|
+
},
|
|
1122
|
+
table: () => {
|
|
1123
|
+
if (balances.length === 0) {
|
|
1124
|
+
console.log("\n No coin balances found.\n");
|
|
1125
|
+
console.log(" Buy coins to see them here:");
|
|
1126
|
+
console.log(" zora buy <address> --eth 0.001\n");
|
|
1127
|
+
} else {
|
|
1128
|
+
renderOnce(
|
|
1129
|
+
/* @__PURE__ */ jsx3(
|
|
1130
|
+
Table,
|
|
1131
|
+
{
|
|
1132
|
+
columns: balanceColumns,
|
|
1133
|
+
data: rankedBalances,
|
|
1134
|
+
title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
|
|
1135
|
+
subtitle: `${balances.length} of ${total}`
|
|
1136
|
+
}
|
|
1137
|
+
)
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
async function fetchCoins(json, address, sort, limit) {
|
|
1144
|
+
let response;
|
|
1145
|
+
try {
|
|
1146
|
+
response = await getProfileBalances({
|
|
1147
|
+
identifier: address,
|
|
1148
|
+
count: limit,
|
|
1149
|
+
sortOption: SORT_MAP[sort]
|
|
1150
|
+
});
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
outputErrorAndExit(
|
|
1153
|
+
json,
|
|
1154
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
if (response.error) {
|
|
1158
|
+
outputErrorAndExit(
|
|
1159
|
+
json,
|
|
1160
|
+
`API error: ${extractErrorMessage(response.error)}`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
const edges = response.data?.profile?.coinBalances?.edges ?? [];
|
|
1164
|
+
const balances = edges.map(
|
|
1165
|
+
(e) => e.node
|
|
1166
|
+
);
|
|
1167
|
+
const total = response.data?.profile?.coinBalances?.count ?? balances.length;
|
|
1168
|
+
return { balances, total };
|
|
1169
|
+
}
|
|
1170
|
+
function validateCoinOpts(json, sort, limitStr) {
|
|
1171
|
+
if (!SORT_MAP[sort]) {
|
|
1172
|
+
outputErrorAndExit(
|
|
1173
|
+
json,
|
|
1174
|
+
`Invalid --sort value: ${sort}.`,
|
|
1175
|
+
`Supported: ${SORT_OPTIONS}`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
const limit = parseInt(limitStr, 10);
|
|
1179
|
+
if (isNaN(limit) || limit <= 0 || limit > 20) {
|
|
1180
|
+
outputErrorAndExit(
|
|
1181
|
+
json,
|
|
1182
|
+
`Invalid --limit value: ${limitStr}. Must be an integer between 1 and 20.`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
return { sort, limit };
|
|
1186
|
+
}
|
|
1187
|
+
var balanceCommand = new Command2("balance").description("Show balances in your wallet").action(async function() {
|
|
1188
|
+
const output = getOutputMode(this, "live");
|
|
1189
|
+
const json = output === "json";
|
|
1190
|
+
const account = resolveContext(json);
|
|
1191
|
+
const { live, intervalSeconds } = getLiveConfig(this, "live");
|
|
1192
|
+
const sort = "usd-value";
|
|
1193
|
+
const limit = 10;
|
|
1194
|
+
const fetchBalanceData = async () => {
|
|
1195
|
+
const [walletResult, coinsResult] = await Promise.allSettled([
|
|
1196
|
+
fetchWalletBalances(account.address),
|
|
1197
|
+
fetchCoins(json, account.address, sort, limit)
|
|
1198
|
+
]);
|
|
1199
|
+
if (walletResult.status === "rejected" || coinsResult.status === "rejected") {
|
|
1200
|
+
const err = walletResult.status === "rejected" ? walletResult.reason : coinsResult.reason;
|
|
1201
|
+
throw new Error(err instanceof Error ? err.message : String(err));
|
|
1202
|
+
}
|
|
1203
|
+
const rankedBalances = coinsResult.value.balances.map(
|
|
1204
|
+
(balance, index) => ({
|
|
1205
|
+
...balance,
|
|
1206
|
+
rank: index + 1
|
|
1207
|
+
})
|
|
1208
|
+
);
|
|
1209
|
+
return {
|
|
1210
|
+
walletBalances: walletResult.value.walletBalances,
|
|
1211
|
+
walletBalancesJson: walletResult.value.walletBalancesJson,
|
|
1212
|
+
rankedBalances,
|
|
1213
|
+
total: coinsResult.value.total
|
|
1214
|
+
};
|
|
1215
|
+
};
|
|
1216
|
+
if (json) {
|
|
1217
|
+
const data = await fetchBalanceData().catch(
|
|
1218
|
+
(err) => outputErrorAndExit(
|
|
1219
|
+
json,
|
|
1220
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1221
|
+
)
|
|
1222
|
+
);
|
|
1223
|
+
outputData(json, {
|
|
1224
|
+
json: {
|
|
1225
|
+
wallet: data.walletBalancesJson,
|
|
1226
|
+
coins: data.rankedBalances.map(
|
|
1227
|
+
(balance) => formatBalanceJson(balance, balance.rank)
|
|
1228
|
+
)
|
|
1229
|
+
},
|
|
1230
|
+
table: () => {
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
track("cli_balances", {
|
|
1234
|
+
sort,
|
|
1235
|
+
limit,
|
|
1236
|
+
live: false,
|
|
1237
|
+
result_count: data.rankedBalances.length,
|
|
1238
|
+
total_count: data.total,
|
|
1239
|
+
output_format: "json"
|
|
1240
|
+
});
|
|
1241
|
+
} else if (live) {
|
|
1242
|
+
await renderLive(
|
|
1243
|
+
/* @__PURE__ */ jsx3(
|
|
1244
|
+
BalanceView,
|
|
1245
|
+
{
|
|
1246
|
+
fetchData: fetchBalanceData,
|
|
1247
|
+
sort,
|
|
1248
|
+
mode: "full",
|
|
1249
|
+
autoRefresh: live,
|
|
1250
|
+
intervalSeconds
|
|
1251
|
+
}
|
|
1252
|
+
)
|
|
1253
|
+
);
|
|
1254
|
+
track("cli_balances", {
|
|
1255
|
+
sort,
|
|
1256
|
+
limit,
|
|
1257
|
+
live,
|
|
1258
|
+
interval: intervalSeconds,
|
|
1259
|
+
output_format: "live"
|
|
1260
|
+
});
|
|
1261
|
+
} else {
|
|
1262
|
+
const data = await fetchBalanceData().catch(
|
|
1263
|
+
(err) => outputErrorAndExit(
|
|
1264
|
+
json,
|
|
1265
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1266
|
+
)
|
|
1267
|
+
);
|
|
1268
|
+
renderOnce(
|
|
1269
|
+
/* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
1270
|
+
/* @__PURE__ */ jsx3(
|
|
1271
|
+
Table,
|
|
1272
|
+
{
|
|
1273
|
+
columns: walletColumns,
|
|
1274
|
+
data: data.walletBalances,
|
|
1275
|
+
title: "Wallet"
|
|
1276
|
+
}
|
|
1277
|
+
),
|
|
1278
|
+
data.rankedBalances.length === 0 ? /* @__PURE__ */ jsxs3(
|
|
1279
|
+
Box3,
|
|
1280
|
+
{
|
|
1281
|
+
flexDirection: "column",
|
|
1282
|
+
paddingLeft: 1,
|
|
1283
|
+
paddingTop: 1,
|
|
1284
|
+
paddingBottom: 1,
|
|
1285
|
+
children: [
|
|
1286
|
+
/* @__PURE__ */ jsx3(Text3, { children: "No coin balances found." }),
|
|
1287
|
+
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
1288
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Buy coins to see them here:" }),
|
|
1289
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1290
|
+
" zora buy ",
|
|
1291
|
+
"<address>",
|
|
1292
|
+
" --eth 0.001"
|
|
1293
|
+
] })
|
|
1294
|
+
] })
|
|
1295
|
+
]
|
|
1296
|
+
}
|
|
1297
|
+
) : /* @__PURE__ */ jsx3(
|
|
1298
|
+
Table,
|
|
1299
|
+
{
|
|
1300
|
+
columns: balanceColumns,
|
|
1301
|
+
data: data.rankedBalances,
|
|
1302
|
+
title: `Coins \xB7 sorted by ${SORT_LABELS[sort]}`,
|
|
1303
|
+
subtitle: `${data.rankedBalances.length} of ${data.total}`
|
|
1304
|
+
}
|
|
1305
|
+
)
|
|
1306
|
+
] })
|
|
1307
|
+
);
|
|
1308
|
+
track("cli_balances", {
|
|
1309
|
+
sort,
|
|
1310
|
+
limit,
|
|
1311
|
+
live: false,
|
|
1312
|
+
result_count: data.rankedBalances.length,
|
|
1313
|
+
total_count: data.total,
|
|
1314
|
+
output_format: "text"
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").action(async function() {
|
|
1319
|
+
const output = getOutputMode(this, "live");
|
|
1320
|
+
const json = output === "json";
|
|
1321
|
+
const account = resolveContext(json);
|
|
1322
|
+
const { live, intervalSeconds } = getLiveConfig(this, "live");
|
|
1323
|
+
const fetchSpendableData = async () => {
|
|
1324
|
+
const walletResult = await fetchWalletBalances(account.address);
|
|
1325
|
+
return {
|
|
1326
|
+
walletBalances: walletResult.walletBalances,
|
|
1327
|
+
walletBalancesJson: walletResult.walletBalancesJson,
|
|
1328
|
+
rankedBalances: [],
|
|
1329
|
+
total: 0
|
|
1330
|
+
};
|
|
1331
|
+
};
|
|
1332
|
+
if (json) {
|
|
1333
|
+
const data = await fetchSpendableData().catch(
|
|
1334
|
+
(err) => outputErrorAndExit(
|
|
1335
|
+
json,
|
|
1336
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1337
|
+
)
|
|
1338
|
+
);
|
|
1339
|
+
outputData(json, {
|
|
1340
|
+
json: { wallet: data.walletBalancesJson },
|
|
1341
|
+
table: () => {
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
} else if (live) {
|
|
1345
|
+
await renderLive(
|
|
1346
|
+
/* @__PURE__ */ jsx3(
|
|
1347
|
+
BalanceView,
|
|
1348
|
+
{
|
|
1349
|
+
fetchData: fetchSpendableData,
|
|
1350
|
+
sort: "usd-value",
|
|
1351
|
+
mode: "wallet",
|
|
1352
|
+
autoRefresh: live,
|
|
1353
|
+
intervalSeconds
|
|
1354
|
+
}
|
|
1355
|
+
)
|
|
1356
|
+
);
|
|
1357
|
+
} else {
|
|
1358
|
+
const walletResult = await fetchWalletBalances(account.address).catch(
|
|
1359
|
+
(err) => outputErrorAndExit(
|
|
1360
|
+
json,
|
|
1361
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1362
|
+
)
|
|
1363
|
+
);
|
|
1364
|
+
renderWallet(json, walletResult);
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
balanceCommand.command("coins").description("Show coin positions").option("--sort <sort>", `Sort by: ${SORT_OPTIONS}`, "usd-value").option("--limit <n>", "Number of results (max 20)", "10").action(async function(opts) {
|
|
1368
|
+
const output = getOutputMode(this, "live");
|
|
1369
|
+
const json = output === "json";
|
|
1370
|
+
const { sort, limit } = validateCoinOpts(json, opts.sort, opts.limit);
|
|
1371
|
+
const account = resolveContext(json);
|
|
1372
|
+
const { live, intervalSeconds } = getLiveConfig(this, "live");
|
|
1373
|
+
const fetchCoinsData = async () => {
|
|
1374
|
+
const { balances, total } = await fetchCoins(
|
|
1375
|
+
json,
|
|
1376
|
+
account.address,
|
|
1377
|
+
sort,
|
|
1378
|
+
limit
|
|
1379
|
+
);
|
|
1380
|
+
const rankedBalances = balances.map((balance, index) => ({
|
|
1381
|
+
...balance,
|
|
1382
|
+
rank: index + 1
|
|
1383
|
+
}));
|
|
1384
|
+
return {
|
|
1385
|
+
walletBalances: [],
|
|
1386
|
+
walletBalancesJson: [],
|
|
1387
|
+
rankedBalances,
|
|
1388
|
+
total
|
|
1389
|
+
};
|
|
1390
|
+
};
|
|
1391
|
+
if (json) {
|
|
1392
|
+
const data = await fetchCoinsData();
|
|
1393
|
+
renderCoins(json, data.rankedBalances, data.total, sort);
|
|
1394
|
+
} else if (live) {
|
|
1395
|
+
await renderLive(
|
|
1396
|
+
/* @__PURE__ */ jsx3(
|
|
1397
|
+
BalanceView,
|
|
1398
|
+
{
|
|
1399
|
+
fetchData: fetchCoinsData,
|
|
1400
|
+
sort,
|
|
1401
|
+
mode: "coins",
|
|
1402
|
+
autoRefresh: live,
|
|
1403
|
+
intervalSeconds
|
|
1404
|
+
}
|
|
1405
|
+
)
|
|
1406
|
+
);
|
|
1407
|
+
} else {
|
|
1408
|
+
const { balances, total } = await fetchCoins(
|
|
1409
|
+
json,
|
|
1410
|
+
account.address,
|
|
1411
|
+
sort,
|
|
1412
|
+
limit
|
|
1413
|
+
);
|
|
1414
|
+
renderCoins(json, balances, total, sort);
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// src/commands/buy.ts
|
|
1419
|
+
import { Command as Command3 } from "commander";
|
|
1420
|
+
import confirm2 from "@inquirer/confirm";
|
|
1421
|
+
import { parseUnits, formatUnits as formatUnits3, isAddress } from "viem";
|
|
1422
|
+
import {
|
|
1423
|
+
setApiKey as setApiKey2,
|
|
1424
|
+
getCoin,
|
|
1425
|
+
tradeCoin,
|
|
1426
|
+
createTradeCall
|
|
1427
|
+
} from "@zoralabs/coins-sdk";
|
|
1428
|
+
|
|
1429
|
+
// src/lib/trade-helpers.ts
|
|
1430
|
+
import {
|
|
1431
|
+
parseEther,
|
|
1432
|
+
formatUnits as formatUnits2,
|
|
1433
|
+
isAddressEqual,
|
|
1434
|
+
parseEventLogs,
|
|
1435
|
+
erc20Abi as erc20Abi2
|
|
1436
|
+
} from "viem";
|
|
1437
|
+
var GAS_RESERVE = parseEther("0.00001");
|
|
1438
|
+
var BUY_AMOUNT_CHECKS = {
|
|
1439
|
+
eth: (opts) => opts.eth !== void 0,
|
|
1440
|
+
usd: (opts) => opts.usd !== void 0,
|
|
1441
|
+
percent: (opts) => opts.percent !== void 0,
|
|
1442
|
+
all: (opts) => opts.all === true
|
|
1443
|
+
};
|
|
1444
|
+
var SELL_AMOUNT_CHECKS = {
|
|
1445
|
+
amount: (opts) => opts.amount !== void 0,
|
|
1446
|
+
usd: (opts) => opts.usd !== void 0,
|
|
1447
|
+
percent: (opts) => opts.percent !== void 0,
|
|
1448
|
+
all: (opts) => opts.all === true
|
|
1449
|
+
};
|
|
1450
|
+
var getAmountMode = (json, opts, checks, flagNames) => {
|
|
1451
|
+
const provided = Object.entries(checks).filter(([, isProvided]) => isProvided(opts)).map(([mode]) => mode);
|
|
1452
|
+
if (provided.length === 0) {
|
|
1453
|
+
outputErrorAndExit(json, `Specify one amount flag: ${flagNames}`);
|
|
1454
|
+
}
|
|
1455
|
+
if (provided.length > 1) {
|
|
1456
|
+
outputErrorAndExit(json, `Only one amount flag allowed: ${flagNames}`);
|
|
1457
|
+
}
|
|
1458
|
+
return provided[0];
|
|
1459
|
+
};
|
|
1460
|
+
var parsePercentageLikeValue = (value) => {
|
|
1461
|
+
if (!/^\d+(\.\d+)?$/.test(value)) return void 0;
|
|
1462
|
+
const parsed = Number(value);
|
|
1463
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1464
|
+
};
|
|
1465
|
+
var formatAmountDisplay = (amount, decimals) => {
|
|
1466
|
+
const formatted = formatUnits2(amount, decimals);
|
|
1467
|
+
const parts = formatted.split(".");
|
|
1468
|
+
if (!parts[1]) {
|
|
1469
|
+
return new Intl.NumberFormat("en-US", {
|
|
1470
|
+
maximumFractionDigits: 2
|
|
1471
|
+
}).format(Number(formatted));
|
|
1472
|
+
}
|
|
1473
|
+
const twoDecimal = `${parts[0]}.${parts[1].slice(0, 2)}`;
|
|
1474
|
+
let maxDecimals = 2;
|
|
1475
|
+
if (Number(twoDecimal) === 0 && amount > 0n) {
|
|
1476
|
+
const sigIndex = parts[1].search(/[1-9]/);
|
|
1477
|
+
maxDecimals = sigIndex === -1 ? 6 : Math.min(sigIndex + 4, parts[1].length);
|
|
1478
|
+
}
|
|
1479
|
+
const truncated = `${parts[0]}.${parts[1].slice(0, maxDecimals)}`;
|
|
1480
|
+
return new Intl.NumberFormat("en-US", {
|
|
1481
|
+
maximumFractionDigits: maxDecimals
|
|
1482
|
+
}).format(Number(truncated));
|
|
1483
|
+
};
|
|
1484
|
+
var getReceivedAmountFromReceipt = ({
|
|
1485
|
+
receipt,
|
|
1486
|
+
tokenAddress,
|
|
1487
|
+
recipient
|
|
1488
|
+
}) => {
|
|
1489
|
+
const transfers = parseEventLogs({
|
|
1490
|
+
abi: erc20Abi2,
|
|
1491
|
+
eventName: "Transfer",
|
|
1492
|
+
logs: receipt.logs,
|
|
1493
|
+
strict: false
|
|
1494
|
+
});
|
|
1495
|
+
const matchingTransfers = transfers.filter((transfer) => {
|
|
1496
|
+
const to = transfer.args?.to;
|
|
1497
|
+
if (!to) return false;
|
|
1498
|
+
return isAddressEqual(transfer.address, tokenAddress) && isAddressEqual(to, recipient);
|
|
1499
|
+
});
|
|
1500
|
+
if (matchingTransfers.length === 0) {
|
|
1501
|
+
throw new Error("No matching Transfer event found in receipt.");
|
|
1502
|
+
}
|
|
1503
|
+
const amount = matchingTransfers.reduce((total, transfer) => {
|
|
1504
|
+
const value = transfer.args?.value;
|
|
1505
|
+
if (value === void 0) {
|
|
1506
|
+
throw new Error("Transfer event missing amount.");
|
|
1507
|
+
}
|
|
1508
|
+
return total + value;
|
|
1509
|
+
}, 0n);
|
|
1510
|
+
const lastTransfer = matchingTransfers[matchingTransfers.length - 1];
|
|
1511
|
+
const logIndex = lastTransfer?.logIndex ?? null;
|
|
1512
|
+
return { amount, logIndex };
|
|
1513
|
+
};
|
|
1514
|
+
var printDebugRequest = (label, tradeParameters) => {
|
|
1515
|
+
if (process.env.ZORA_API_TARGET) {
|
|
1516
|
+
console.error(`[debug] API target: ${process.env.ZORA_API_TARGET}`);
|
|
1517
|
+
}
|
|
1518
|
+
console.error(`
|
|
1519
|
+
[debug] ${label} \u2014 Quote Request:`);
|
|
1520
|
+
console.error(
|
|
1521
|
+
JSON.stringify(
|
|
1522
|
+
{
|
|
1523
|
+
tokenIn: tradeParameters.sell,
|
|
1524
|
+
tokenOut: tradeParameters.buy,
|
|
1525
|
+
amountIn: tradeParameters.amountIn.toString(),
|
|
1526
|
+
slippage: tradeParameters.slippage,
|
|
1527
|
+
chainId: 8453,
|
|
1528
|
+
sender: tradeParameters.sender,
|
|
1529
|
+
recipient: tradeParameters.recipient || tradeParameters.sender
|
|
1530
|
+
},
|
|
1531
|
+
null,
|
|
1532
|
+
2
|
|
1533
|
+
)
|
|
1534
|
+
);
|
|
1535
|
+
};
|
|
1536
|
+
var printDebugResponse = (label, quoteResponse) => {
|
|
1537
|
+
console.error(`
|
|
1538
|
+
[debug] ${label} \u2014 Quote Response:`);
|
|
1539
|
+
console.error(JSON.stringify(quoteResponse, null, 2));
|
|
1540
|
+
console.error("");
|
|
1541
|
+
};
|
|
1542
|
+
var printQuote = (json, info) => {
|
|
1543
|
+
if (json) {
|
|
1544
|
+
outputJson({
|
|
1545
|
+
action: "quote",
|
|
1546
|
+
coin: info.coinSymbol,
|
|
1547
|
+
address: info.address,
|
|
1548
|
+
spend: {
|
|
1549
|
+
amount: formatUnits2(info.amountIn, info.inputTokenDecimals),
|
|
1550
|
+
raw: info.amountIn.toString(),
|
|
1551
|
+
symbol: info.inputTokenSymbol
|
|
1552
|
+
},
|
|
1553
|
+
estimated: {
|
|
1554
|
+
amount: formatUnits2(BigInt(info.amountOut), 18),
|
|
1555
|
+
raw: info.amountOut,
|
|
1556
|
+
symbol: info.coinSymbol
|
|
1557
|
+
},
|
|
1558
|
+
slippage: info.slippagePct
|
|
1559
|
+
});
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
console.log(`
|
|
1563
|
+
Buy ${info.coinName} (${info.coinSymbol})
|
|
1564
|
+
`);
|
|
1565
|
+
console.log(` Amount ${info.spendAmount}`);
|
|
1566
|
+
console.log(` You get ~${info.coinsFormatted} ${info.coinSymbol}`);
|
|
1567
|
+
console.log(` Slippage ${info.slippagePct}%
|
|
1568
|
+
`);
|
|
1569
|
+
};
|
|
1570
|
+
var printTradeResult = (json, info) => {
|
|
1571
|
+
const receivedAmount = formatUnits2(info.receivedAmountOut, 18);
|
|
1572
|
+
const receivedFormatted = formatCoinsDisplay(receivedAmount);
|
|
1573
|
+
if (json) {
|
|
1574
|
+
outputJson({
|
|
1575
|
+
action: "buy",
|
|
1576
|
+
coin: info.coinSymbol,
|
|
1577
|
+
address: info.address,
|
|
1578
|
+
spent: {
|
|
1579
|
+
amount: formatUnits2(info.amountIn, info.inputTokenDecimals),
|
|
1580
|
+
raw: info.amountIn.toString(),
|
|
1581
|
+
symbol: info.inputTokenSymbol
|
|
1582
|
+
},
|
|
1583
|
+
received: {
|
|
1584
|
+
amount: receivedAmount,
|
|
1585
|
+
raw: info.receivedAmountOut.toString(),
|
|
1586
|
+
symbol: info.coinSymbol
|
|
1587
|
+
},
|
|
1588
|
+
tx: info.txHash
|
|
1589
|
+
});
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
console.log(`
|
|
1593
|
+
Bought ${info.coinName}
|
|
1594
|
+
`);
|
|
1595
|
+
console.log(` Spent ${info.spendAmount} ${info.inputTokenSymbol}`);
|
|
1596
|
+
console.log(` Received ${receivedFormatted} ${info.coinSymbol}`);
|
|
1597
|
+
console.log(` Tx ${info.txHash}
|
|
1598
|
+
`);
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
// src/commands/buy.ts
|
|
1602
|
+
var buyCommand = new Command3("buy").description("Buy a coin").argument("<address>", "Coin contract address (0x\u2026)").option("--eth <value>", "Buy with ETH amount").option("--usd <value>", "Buy with USD equivalent (use with --token)").option("--token <asset>", "Token to spend: eth, usdc, zora", "eth").option("--percent <value>", "Buy with percentage of ETH balance").option("--all", "Swap all ETH for coin").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").action(async function(coinAddress, opts) {
|
|
1603
|
+
const json = getJson(this);
|
|
1604
|
+
const debug = opts.debug === true;
|
|
1605
|
+
if (!isAddress(coinAddress)) {
|
|
1606
|
+
outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
|
|
1607
|
+
}
|
|
1608
|
+
const tokenKey = opts.token.toLowerCase();
|
|
1609
|
+
if (!(tokenKey in BASE_TRADE_TOKENS)) {
|
|
1610
|
+
outputErrorAndExit(
|
|
1611
|
+
json,
|
|
1612
|
+
`Invalid --token value: ${opts.token}. Use: eth, usdc, zora`
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
const inputToken = BASE_TRADE_TOKENS[tokenKey];
|
|
1616
|
+
const amountMode = getAmountMode(
|
|
1617
|
+
json,
|
|
1618
|
+
opts,
|
|
1619
|
+
BUY_AMOUNT_CHECKS,
|
|
1620
|
+
"--eth, --usd, --percent, or --all"
|
|
1621
|
+
);
|
|
1622
|
+
const slippagePct = parsePercentageLikeValue(opts.slippage);
|
|
1623
|
+
if (slippagePct === void 0 || slippagePct < 0 || slippagePct > 99) {
|
|
1624
|
+
outputErrorAndExit(
|
|
1625
|
+
json,
|
|
1626
|
+
"Invalid --slippage value. Must be between 0 and 99."
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
const slippage = slippagePct / 100;
|
|
1630
|
+
const apiKey = getApiKey();
|
|
1631
|
+
if (apiKey) {
|
|
1632
|
+
setApiKey2(apiKey);
|
|
1633
|
+
}
|
|
1634
|
+
const account = resolveAccount(json);
|
|
1635
|
+
const { publicClient, walletClient } = createClients(account);
|
|
1636
|
+
let token;
|
|
1637
|
+
try {
|
|
1638
|
+
const response = await getCoin({ address: coinAddress });
|
|
1639
|
+
token = response.data?.zora20Token;
|
|
1640
|
+
} catch (err) {
|
|
1641
|
+
outputErrorAndExit(
|
|
1642
|
+
json,
|
|
1643
|
+
`Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
if (!token) {
|
|
1647
|
+
outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
|
|
1648
|
+
}
|
|
1649
|
+
const coinName = token.name;
|
|
1650
|
+
const coinSymbol = token.symbol;
|
|
1651
|
+
let amountIn;
|
|
1652
|
+
if (amountMode === "usd") {
|
|
1653
|
+
const usdVal = parsePercentageLikeValue(opts.usd);
|
|
1654
|
+
if (usdVal === void 0 || usdVal <= 0) {
|
|
1655
|
+
outputErrorAndExit(
|
|
1656
|
+
json,
|
|
1657
|
+
"Invalid --usd value. Must be a positive number."
|
|
1658
|
+
);
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
let priceUsd;
|
|
1662
|
+
if (inputToken.fixedPriceUsd != null) {
|
|
1663
|
+
priceUsd = inputToken.fixedPriceUsd;
|
|
1664
|
+
} else {
|
|
1665
|
+
const fetched = await fetchTokenPriceUsd(inputToken.priceAddress);
|
|
1666
|
+
if (fetched === null) {
|
|
1667
|
+
outputErrorAndExit(
|
|
1668
|
+
json,
|
|
1669
|
+
`Failed to fetch ${inputToken.symbol} price.`
|
|
1670
|
+
);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
priceUsd = fetched;
|
|
1674
|
+
}
|
|
1675
|
+
const tokenAmount = usdVal / priceUsd;
|
|
1676
|
+
amountIn = parseUnits(
|
|
1677
|
+
tokenAmount.toFixed(inputToken.decimals),
|
|
1678
|
+
inputToken.decimals
|
|
1679
|
+
);
|
|
1680
|
+
if (amountIn === 0n) {
|
|
1681
|
+
outputErrorAndExit(json, "Calculated amount is zero. USD too small.");
|
|
1682
|
+
}
|
|
1683
|
+
if (debug) {
|
|
1684
|
+
console.error(
|
|
1685
|
+
`[debug] $${usdVal} USD = ${formatUnits3(amountIn, inputToken.decimals)} ${inputToken.symbol} (price: $${priceUsd})`
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
} else if (amountMode === "eth") {
|
|
1689
|
+
const val = parsePercentageLikeValue(opts.eth);
|
|
1690
|
+
if (val === void 0 || val <= 0) {
|
|
1691
|
+
outputErrorAndExit(
|
|
1692
|
+
json,
|
|
1693
|
+
"Invalid --eth value. Must be a positive number."
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
try {
|
|
1697
|
+
amountIn = parseUnits(opts.eth, inputToken.decimals);
|
|
1698
|
+
} catch {
|
|
1699
|
+
outputErrorAndExit(
|
|
1700
|
+
json,
|
|
1701
|
+
"Invalid --eth value. Must be a positive number."
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
} else {
|
|
1705
|
+
const isEth = tokenKey === "eth";
|
|
1706
|
+
let balance;
|
|
1707
|
+
if (isEth) {
|
|
1708
|
+
balance = await publicClient.getBalance({
|
|
1709
|
+
address: account.address
|
|
1710
|
+
});
|
|
1711
|
+
} else {
|
|
1712
|
+
const tokenAddress = inputToken.trade.address;
|
|
1713
|
+
balance = await publicClient.readContract({
|
|
1714
|
+
address: tokenAddress,
|
|
1715
|
+
abi: [
|
|
1716
|
+
{
|
|
1717
|
+
name: "balanceOf",
|
|
1718
|
+
type: "function",
|
|
1719
|
+
stateMutability: "view",
|
|
1720
|
+
inputs: [{ name: "account", type: "address" }],
|
|
1721
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
1722
|
+
}
|
|
1723
|
+
],
|
|
1724
|
+
functionName: "balanceOf",
|
|
1725
|
+
args: [account.address]
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
if (balance === 0n) {
|
|
1729
|
+
outputErrorAndExit(
|
|
1730
|
+
json,
|
|
1731
|
+
`No ${inputToken.symbol} balance. Deposit ${inputToken.symbol} to ${account.address} on Base.`
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
const gasReserve = isEth ? GAS_RESERVE : 0n;
|
|
1735
|
+
if (isEth && balance <= gasReserve) {
|
|
1736
|
+
outputErrorAndExit(
|
|
1737
|
+
json,
|
|
1738
|
+
`Balance too low (${formatEthDisplay(balance)} ETH). Need >${formatEthDisplay(GAS_RESERVE)} ETH for gas.`
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
const spendableBalance = balance - gasReserve;
|
|
1742
|
+
if (amountMode === "all") {
|
|
1743
|
+
amountIn = spendableBalance;
|
|
1744
|
+
} else {
|
|
1745
|
+
const pct = parsePercentageLikeValue(opts.percent);
|
|
1746
|
+
if (pct === void 0 || pct <= 0 || pct > 100) {
|
|
1747
|
+
outputErrorAndExit(
|
|
1748
|
+
json,
|
|
1749
|
+
"Invalid --percent value. Must be between 0 and 100."
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
amountIn = pct === 100 ? spendableBalance : spendableBalance * BigInt(Math.round(pct * 100)) / 10000n;
|
|
1753
|
+
if (amountIn === 0n) {
|
|
1754
|
+
outputErrorAndExit(
|
|
1755
|
+
json,
|
|
1756
|
+
"Calculated amount is zero. Balance too low."
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
let swapAmountUsd;
|
|
1762
|
+
if (amountMode === "usd") {
|
|
1763
|
+
swapAmountUsd = parsePercentageLikeValue(opts.usd);
|
|
1764
|
+
} else {
|
|
1765
|
+
const priceUsd = inputToken.fixedPriceUsd ?? await fetchTokenPriceUsd(inputToken.priceAddress);
|
|
1766
|
+
if (priceUsd != null) {
|
|
1767
|
+
swapAmountUsd = Number(
|
|
1768
|
+
(Number(formatUnits3(amountIn, inputToken.decimals)) * priceUsd).toFixed(2)
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const tradeParameters = {
|
|
1773
|
+
sell: inputToken.trade,
|
|
1774
|
+
buy: { type: "erc20", address: coinAddress },
|
|
1775
|
+
amountIn,
|
|
1776
|
+
slippage,
|
|
1777
|
+
sender: account.address
|
|
1778
|
+
};
|
|
1779
|
+
if (debug) {
|
|
1780
|
+
printDebugRequest("buy", tradeParameters);
|
|
1781
|
+
}
|
|
1782
|
+
let amountOut;
|
|
1783
|
+
try {
|
|
1784
|
+
const quote = await createTradeCall(tradeParameters);
|
|
1785
|
+
if (debug) {
|
|
1786
|
+
printDebugResponse("buy", quote);
|
|
1787
|
+
}
|
|
1788
|
+
if (!quote.quote?.amountOut || quote.quote.amountOut === "0") {
|
|
1789
|
+
outputErrorAndExit(
|
|
1790
|
+
json,
|
|
1791
|
+
"Quote returned zero output. Amount may be too small."
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
amountOut = quote.quote.amountOut;
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
if (debug) {
|
|
1797
|
+
console.error(
|
|
1798
|
+
`
|
|
1799
|
+
[debug] buy \u2014 Quote Error:
|
|
1800
|
+
${err instanceof Error ? err.stack || err.message : String(err)}
|
|
1801
|
+
`
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1805
|
+
const errorType = err?.errorType;
|
|
1806
|
+
const errorBody = err?.errorBody;
|
|
1807
|
+
if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
|
|
1808
|
+
if (json) {
|
|
1809
|
+
outputJson({ error: errorBody ?? msg });
|
|
1810
|
+
process.exit(1);
|
|
1811
|
+
}
|
|
1812
|
+
outputErrorAndExit(
|
|
1813
|
+
json,
|
|
1814
|
+
"Not enough available liquidity for your swap. Please try swapping fewer tokens."
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
outputErrorAndExit(
|
|
1818
|
+
json,
|
|
1819
|
+
`Quote failed: ${msg}`,
|
|
1820
|
+
"Check the coin address is valid and try again. Use --debug for full error details."
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
const spendAmount = formatUnits3(amountIn, inputToken.decimals);
|
|
1824
|
+
const spendFormatted = new Intl.NumberFormat("en-US", {
|
|
1825
|
+
maximumFractionDigits: 6
|
|
1826
|
+
}).format(Number(spendAmount));
|
|
1827
|
+
const coinsOut = formatUnits3(BigInt(amountOut), 18);
|
|
1828
|
+
const coinsFormatted = formatCoinsDisplay(coinsOut);
|
|
1829
|
+
if (opts.quote) {
|
|
1830
|
+
printQuote(json, {
|
|
1831
|
+
coinName,
|
|
1832
|
+
coinSymbol,
|
|
1833
|
+
address: coinAddress,
|
|
1834
|
+
spendAmount: `${spendFormatted} ${inputToken.symbol}`,
|
|
1835
|
+
amountIn,
|
|
1836
|
+
inputTokenSymbol: inputToken.symbol,
|
|
1837
|
+
inputTokenDecimals: inputToken.decimals,
|
|
1838
|
+
coinsFormatted,
|
|
1839
|
+
amountOut,
|
|
1840
|
+
slippagePct
|
|
1841
|
+
});
|
|
1842
|
+
track("cli_buy", {
|
|
1843
|
+
action: "quote",
|
|
1844
|
+
coin_address: coinAddress,
|
|
1845
|
+
coin_name: coinName,
|
|
1846
|
+
coin_symbol: coinSymbol,
|
|
1847
|
+
amount_mode: amountMode,
|
|
1848
|
+
swap_amount_usd: swapAmountUsd,
|
|
1849
|
+
valueUsd: swapAmountUsd,
|
|
1850
|
+
swapCoinType: token.coinType ?? null,
|
|
1851
|
+
slippage: slippagePct,
|
|
1852
|
+
output_format: json ? "json" : "table"
|
|
1853
|
+
});
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (!opts.yes) {
|
|
1857
|
+
printQuote(false, {
|
|
1858
|
+
coinName,
|
|
1859
|
+
coinSymbol,
|
|
1860
|
+
address: coinAddress,
|
|
1861
|
+
spendAmount: `${spendFormatted} ${inputToken.symbol}`,
|
|
1862
|
+
amountIn,
|
|
1863
|
+
inputTokenSymbol: inputToken.symbol,
|
|
1864
|
+
inputTokenDecimals: inputToken.decimals,
|
|
1865
|
+
coinsFormatted,
|
|
1866
|
+
amountOut,
|
|
1867
|
+
slippagePct
|
|
1868
|
+
});
|
|
1869
|
+
const ok = await confirm2({
|
|
1870
|
+
message: "Confirm?",
|
|
1871
|
+
default: false
|
|
1872
|
+
});
|
|
1873
|
+
if (!ok) {
|
|
1874
|
+
process.exit(0);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
let receipt;
|
|
1878
|
+
let txHash;
|
|
1879
|
+
let receivedAmountOut = BigInt(amountOut);
|
|
1880
|
+
let swapLogIndex = null;
|
|
1881
|
+
const swapCoinType = token.coinType ?? null;
|
|
1882
|
+
try {
|
|
1883
|
+
receipt = await tradeCoin({
|
|
1884
|
+
tradeParameters,
|
|
1885
|
+
walletClient,
|
|
1886
|
+
publicClient,
|
|
1887
|
+
account
|
|
1888
|
+
});
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
track("cli_buy", {
|
|
1891
|
+
action: "trade",
|
|
1892
|
+
coin_address: coinAddress,
|
|
1893
|
+
coin_name: coinName,
|
|
1894
|
+
coin_symbol: coinSymbol,
|
|
1895
|
+
amount_mode: amountMode,
|
|
1896
|
+
swap_amount_usd: swapAmountUsd,
|
|
1897
|
+
valueUsd: swapAmountUsd,
|
|
1898
|
+
swapCoinType,
|
|
1899
|
+
slippage: slippagePct,
|
|
1900
|
+
output_format: json ? "json" : "table",
|
|
1901
|
+
success: false,
|
|
1902
|
+
error_type: err instanceof Error ? err.constructor.name : "unknown"
|
|
1903
|
+
});
|
|
1904
|
+
await shutdownAnalytics();
|
|
1905
|
+
outputErrorAndExit(
|
|
1906
|
+
json,
|
|
1907
|
+
`Transaction failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
txHash = receipt.transactionHash;
|
|
1911
|
+
try {
|
|
1912
|
+
const result = getReceivedAmountFromReceipt({
|
|
1913
|
+
receipt,
|
|
1914
|
+
tokenAddress: coinAddress,
|
|
1915
|
+
recipient: account.address
|
|
1916
|
+
});
|
|
1917
|
+
receivedAmountOut = result.amount;
|
|
1918
|
+
swapLogIndex = result.logIndex;
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
console.warn(
|
|
1921
|
+
`Warning: transaction succeeded but could not determine received amount: ${err instanceof Error ? err.message : String(err)}`
|
|
1922
|
+
);
|
|
1923
|
+
console.warn(`Tx: ${txHash}`);
|
|
1924
|
+
}
|
|
1925
|
+
printTradeResult(json, {
|
|
1926
|
+
coinName,
|
|
1927
|
+
coinSymbol,
|
|
1928
|
+
address: coinAddress,
|
|
1929
|
+
spendAmount,
|
|
1930
|
+
amountIn,
|
|
1931
|
+
inputTokenSymbol: inputToken.symbol,
|
|
1932
|
+
inputTokenDecimals: inputToken.decimals,
|
|
1933
|
+
receivedAmountOut,
|
|
1934
|
+
txHash
|
|
1935
|
+
});
|
|
1936
|
+
track("cli_buy", {
|
|
1937
|
+
action: "trade",
|
|
1938
|
+
coin_address: coinAddress,
|
|
1939
|
+
coin_name: coinName,
|
|
1940
|
+
coin_symbol: coinSymbol,
|
|
1941
|
+
amount_mode: amountMode,
|
|
1942
|
+
input_amount: amountIn.toString(),
|
|
1943
|
+
input_token_symbol: inputToken.symbol,
|
|
1944
|
+
swap_amount_usd: swapAmountUsd,
|
|
1945
|
+
valueUsd: swapAmountUsd,
|
|
1946
|
+
swapCoinType,
|
|
1947
|
+
transactionHash: txHash,
|
|
1948
|
+
logIndex: swapLogIndex,
|
|
1949
|
+
slippage: slippagePct,
|
|
1950
|
+
output_format: json ? "json" : "table",
|
|
1951
|
+
success: true,
|
|
1952
|
+
tx_hash: txHash
|
|
1953
|
+
});
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
// src/commands/explore.tsx
|
|
1957
|
+
import { Command as Command4 } from "commander";
|
|
1958
|
+
import {
|
|
1959
|
+
setApiKey as setApiKey3,
|
|
1960
|
+
getCoinsTopVolume24h,
|
|
1961
|
+
getCoinsMostValuable,
|
|
1962
|
+
getCoinsNew,
|
|
1963
|
+
getCoinsTopGainers,
|
|
1964
|
+
getCoinsLastTraded,
|
|
1965
|
+
getCoinsLastTradedUnique,
|
|
1966
|
+
getExploreTopVolumeAll24h,
|
|
1967
|
+
getExploreTopVolumeCreators24h,
|
|
1968
|
+
getExploreNewAll,
|
|
1969
|
+
getExploreFeaturedCreators,
|
|
1970
|
+
getExploreFeaturedVideos,
|
|
1971
|
+
getCreatorCoins,
|
|
1972
|
+
getMostValuableCreatorCoins,
|
|
1973
|
+
getMostValuableAll,
|
|
1974
|
+
getMostValuableTrends,
|
|
1975
|
+
getNewTrends,
|
|
1976
|
+
getTopVolumeTrends24h,
|
|
1977
|
+
getTrendingAll,
|
|
1978
|
+
getTrendingCreators,
|
|
1979
|
+
getTrendingPosts,
|
|
1980
|
+
getTrendingTrends
|
|
1981
|
+
} from "@zoralabs/coins-sdk";
|
|
1982
|
+
|
|
1983
|
+
// src/lib/types.ts
|
|
1984
|
+
var SORT_LABELS2 = {
|
|
1985
|
+
mcap: "Top by Market Cap",
|
|
1986
|
+
volume: "Top by 24h Volume",
|
|
1987
|
+
new: "New",
|
|
1988
|
+
gainers: "Top Gainers (24h)",
|
|
1989
|
+
"last-traded": "Last Traded",
|
|
1990
|
+
"last-traded-unique": "Last Traded (Unique)",
|
|
1991
|
+
trending: "Trending",
|
|
1992
|
+
featured: "Featured"
|
|
1993
|
+
};
|
|
1994
|
+
var TYPE_LABELS = {
|
|
1995
|
+
all: "all",
|
|
1996
|
+
trend: "trends",
|
|
1997
|
+
"creator-coin": "creator coins",
|
|
1998
|
+
post: "posts"
|
|
1999
|
+
};
|
|
2000
|
+
var COIN_TYPE_DISPLAY = {
|
|
2001
|
+
CONTENT: "post",
|
|
2002
|
+
CREATOR: "creator-coin",
|
|
2003
|
+
TREND: "trend"
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
// src/components/ExploreView.tsx
|
|
2007
|
+
import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef2 } from "react";
|
|
2008
|
+
import { Box as Box4, Text as Text4, useInput as useInput2, useApp as useApp2 } from "ink";
|
|
2009
|
+
import Spinner2 from "ink-spinner";
|
|
2010
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2011
|
+
var COLUMNS = [
|
|
2012
|
+
{ header: "#", width: 4, accessor: (c) => String(c.rank) },
|
|
2013
|
+
{ header: "Name", width: 20, accessor: (c) => c.name ?? "Unknown" },
|
|
2014
|
+
{ header: "Address", width: 44, accessor: (c) => c.address ?? "" },
|
|
2015
|
+
{
|
|
2016
|
+
header: "Type",
|
|
2017
|
+
width: 14,
|
|
2018
|
+
accessor: (c) => COIN_TYPE_DISPLAY[c.coinType ?? ""] ?? c.coinType ?? ""
|
|
2019
|
+
},
|
|
2020
|
+
{
|
|
2021
|
+
header: "Market Cap",
|
|
2022
|
+
width: 12,
|
|
2023
|
+
accessor: (c) => formatCompactUsd(c.marketCap)
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
header: "24h Vol",
|
|
2027
|
+
width: 12,
|
|
2028
|
+
accessor: (c) => formatCompactUsd(c.volume24h)
|
|
2029
|
+
},
|
|
2030
|
+
{
|
|
2031
|
+
header: "24h Change",
|
|
2032
|
+
width: 11,
|
|
2033
|
+
accessor: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).text,
|
|
2034
|
+
color: (c) => formatMcapChange(c.marketCap, c.marketCapDelta24h).color
|
|
2035
|
+
}
|
|
2036
|
+
];
|
|
2037
|
+
var CACHE_TTL_MS = 6e4;
|
|
2038
|
+
var CACHE_KEY_FIRST = "__first__";
|
|
2039
|
+
var ExploreView = ({
|
|
2040
|
+
fetchPage,
|
|
2041
|
+
sort,
|
|
2042
|
+
type,
|
|
2043
|
+
limit,
|
|
2044
|
+
initialCursor,
|
|
2045
|
+
cacheTtlMs = CACHE_TTL_MS,
|
|
2046
|
+
autoRefresh = false,
|
|
2047
|
+
intervalSeconds = 30
|
|
2048
|
+
}) => {
|
|
2049
|
+
const { exit } = useApp2();
|
|
2050
|
+
const [loading, setLoading] = useState3(true);
|
|
2051
|
+
const [error, setError] = useState3(null);
|
|
2052
|
+
const [coins, setCoins] = useState3([]);
|
|
2053
|
+
const [pageInfo, setPageInfo] = useState3(null);
|
|
2054
|
+
const [page, setPage] = useState3(1);
|
|
2055
|
+
const [cursorHistory, setCursorHistory] = useState3(
|
|
2056
|
+
[]
|
|
2057
|
+
);
|
|
2058
|
+
const [currentCursor, setCurrentCursor] = useState3(
|
|
2059
|
+
initialCursor
|
|
2060
|
+
);
|
|
2061
|
+
const cache = useRef2(/* @__PURE__ */ new Map());
|
|
2062
|
+
const { refreshCount, secondsUntilRefresh, triggerManualRefresh } = useAutoRefresh(intervalSeconds, autoRefresh);
|
|
2063
|
+
const [manualRefreshCount, setManualRefreshCount] = useState3(0);
|
|
2064
|
+
const loadPage = useCallback3(
|
|
2065
|
+
async (cursor) => {
|
|
2066
|
+
const cacheKey = cursor ?? CACHE_KEY_FIRST;
|
|
2067
|
+
const cached = cache.current.get(cacheKey);
|
|
2068
|
+
const isFresh = cached && Date.now() - cached.fetchedAt < cacheTtlMs;
|
|
2069
|
+
if (isFresh) {
|
|
2070
|
+
setCoins(cached.result.coins);
|
|
2071
|
+
setPageInfo(cached.result.pageInfo ?? null);
|
|
2072
|
+
setError(null);
|
|
2073
|
+
setLoading(false);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
setLoading(true);
|
|
2077
|
+
setError(null);
|
|
2078
|
+
try {
|
|
2079
|
+
const result = await fetchPage(cursor);
|
|
2080
|
+
cache.current.set(cacheKey, { result, fetchedAt: Date.now() });
|
|
2081
|
+
setCoins(result.coins);
|
|
2082
|
+
setPageInfo(result.pageInfo ?? null);
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2085
|
+
}
|
|
2086
|
+
setLoading(false);
|
|
2087
|
+
},
|
|
2088
|
+
[fetchPage, cacheTtlMs]
|
|
2089
|
+
);
|
|
2090
|
+
useEffect3(() => {
|
|
2091
|
+
if (refreshCount === 0) return;
|
|
2092
|
+
const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
|
|
2093
|
+
cache.current.delete(cacheKey);
|
|
2094
|
+
}, [refreshCount, currentCursor]);
|
|
2095
|
+
useEffect3(() => {
|
|
2096
|
+
loadPage(currentCursor);
|
|
2097
|
+
}, [currentCursor, loadPage, refreshCount, manualRefreshCount]);
|
|
2098
|
+
useInput2((input, key) => {
|
|
2099
|
+
if (input === "q" || key.escape) {
|
|
2100
|
+
exit();
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
if (loading) return;
|
|
2104
|
+
const canGoNext = pageInfo?.hasNextPage && pageInfo.endCursor;
|
|
2105
|
+
const canGoPrev = cursorHistory.length > 0;
|
|
2106
|
+
if ((input === "n" || key.rightArrow) && canGoNext) {
|
|
2107
|
+
setCursorHistory((prev) => [...prev, currentCursor]);
|
|
2108
|
+
setCurrentCursor(pageInfo.endCursor);
|
|
2109
|
+
setPage((p) => p + 1);
|
|
2110
|
+
}
|
|
2111
|
+
if ((input === "p" || key.leftArrow) && canGoPrev) {
|
|
2112
|
+
const prev = cursorHistory[cursorHistory.length - 1];
|
|
2113
|
+
setCursorHistory((h) => h.slice(0, -1));
|
|
2114
|
+
setCurrentCursor(prev);
|
|
2115
|
+
setPage((p) => p - 1);
|
|
2116
|
+
}
|
|
2117
|
+
if (input === "r") {
|
|
2118
|
+
const cacheKey = currentCursor ?? CACHE_KEY_FIRST;
|
|
2119
|
+
cache.current.delete(cacheKey);
|
|
2120
|
+
triggerManualRefresh();
|
|
2121
|
+
setManualRefreshCount((c) => c + 1);
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
if (error) {
|
|
2125
|
+
return /* @__PURE__ */ jsxs4(
|
|
2126
|
+
Box4,
|
|
2127
|
+
{
|
|
2128
|
+
flexDirection: "column",
|
|
2129
|
+
paddingLeft: 1,
|
|
2130
|
+
paddingTop: 1,
|
|
2131
|
+
paddingBottom: 1,
|
|
2132
|
+
children: [
|
|
2133
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
|
|
2134
|
+
"Error: ",
|
|
2135
|
+
error
|
|
2136
|
+
] }),
|
|
2137
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press q to exit" }) })
|
|
2138
|
+
]
|
|
2139
|
+
}
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
if (loading) {
|
|
2143
|
+
return /* @__PURE__ */ jsx4(Box4, { paddingLeft: 1, paddingTop: 1, children: /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
2144
|
+
/* @__PURE__ */ jsx4(Spinner2, { type: "dots" }),
|
|
2145
|
+
" Loading\u2026"
|
|
2146
|
+
] }) });
|
|
2147
|
+
}
|
|
2148
|
+
if (coins.length === 0) {
|
|
2149
|
+
return /* @__PURE__ */ jsxs4(
|
|
2150
|
+
Box4,
|
|
2151
|
+
{
|
|
2152
|
+
flexDirection: "column",
|
|
2153
|
+
paddingLeft: 1,
|
|
2154
|
+
paddingTop: 1,
|
|
2155
|
+
paddingBottom: 1,
|
|
2156
|
+
children: [
|
|
2157
|
+
/* @__PURE__ */ jsx4(Text4, { children: "No coins found." }),
|
|
2158
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
|
|
2159
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Try a different sort or type:" }),
|
|
2160
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " zora explore --sort volume --type all" }),
|
|
2161
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " zora explore --sort new --type all" })
|
|
2162
|
+
] }),
|
|
2163
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press q to exit" }) })
|
|
2164
|
+
]
|
|
2165
|
+
}
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
const title = type !== "all" ? `${SORT_LABELS2[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS2[sort];
|
|
2169
|
+
const subtitle = `Page ${page} \xB7 ${coins.length} result${coins.length !== 1 ? "s" : ""}`;
|
|
2170
|
+
const rankedCoins = coins.map((c, i) => ({
|
|
2171
|
+
...c,
|
|
2172
|
+
rank: (page - 1) * limit + i + 1
|
|
2173
|
+
}));
|
|
2174
|
+
const hints = [];
|
|
2175
|
+
if (cursorHistory.length > 0) hints.push("\u2190 prev");
|
|
2176
|
+
if (pageInfo?.hasNextPage) hints.push("\u2192 next");
|
|
2177
|
+
hints.push("r refresh");
|
|
2178
|
+
if (autoRefresh) hints.push(`auto: ${secondsUntilRefresh}s`);
|
|
2179
|
+
hints.push("q quit");
|
|
2180
|
+
const footer = hints.join(" \xB7 ");
|
|
2181
|
+
return /* @__PURE__ */ jsx4(
|
|
2182
|
+
Table,
|
|
2183
|
+
{
|
|
2184
|
+
data: rankedCoins,
|
|
2185
|
+
columns: COLUMNS,
|
|
2186
|
+
title,
|
|
2187
|
+
subtitle,
|
|
2188
|
+
footer
|
|
2189
|
+
}
|
|
2190
|
+
);
|
|
2191
|
+
};
|
|
2192
|
+
|
|
2193
|
+
// src/commands/explore.tsx
|
|
2194
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
2195
|
+
var QUERY_MAP = {
|
|
2196
|
+
mcap: {
|
|
2197
|
+
all: getMostValuableAll,
|
|
2198
|
+
trend: getMostValuableTrends,
|
|
2199
|
+
"creator-coin": getMostValuableCreatorCoins,
|
|
2200
|
+
post: getCoinsMostValuable
|
|
2201
|
+
},
|
|
2202
|
+
volume: {
|
|
2203
|
+
all: getExploreTopVolumeAll24h,
|
|
2204
|
+
trend: getTopVolumeTrends24h,
|
|
2205
|
+
"creator-coin": getExploreTopVolumeCreators24h,
|
|
2206
|
+
post: getCoinsTopVolume24h
|
|
2207
|
+
},
|
|
2208
|
+
new: {
|
|
2209
|
+
all: getExploreNewAll,
|
|
2210
|
+
trend: getNewTrends,
|
|
2211
|
+
"creator-coin": getCreatorCoins,
|
|
2212
|
+
post: getCoinsNew
|
|
2213
|
+
},
|
|
2214
|
+
gainers: {
|
|
2215
|
+
post: getCoinsTopGainers
|
|
2216
|
+
},
|
|
2217
|
+
"last-traded": {
|
|
2218
|
+
post: getCoinsLastTraded
|
|
2219
|
+
},
|
|
2220
|
+
"last-traded-unique": {
|
|
2221
|
+
post: getCoinsLastTradedUnique
|
|
2222
|
+
},
|
|
2223
|
+
trending: {
|
|
2224
|
+
all: getTrendingAll,
|
|
2225
|
+
trend: getTrendingTrends,
|
|
2226
|
+
"creator-coin": getTrendingCreators,
|
|
2227
|
+
post: getTrendingPosts
|
|
2228
|
+
},
|
|
2229
|
+
featured: {
|
|
2230
|
+
"creator-coin": getExploreFeaturedCreators,
|
|
2231
|
+
post: getExploreFeaturedVideos
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
var SORT_OPTIONS2 = Object.keys(SORT_LABELS2).join(", ");
|
|
2235
|
+
var exploreCommand = new Command4("explore").description("Browse top, new, and highest volume coins").option("--sort <sort>", `Sort by: ${SORT_OPTIONS2}`, "mcap").option(
|
|
2236
|
+
"--type <type>",
|
|
2237
|
+
"Filter by type: all, trend, creator-coin, post (availability varies by sort)",
|
|
2238
|
+
"post"
|
|
2239
|
+
).option("--limit <n>", "Number of results (max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").action(async function(opts) {
|
|
2240
|
+
const output = getOutputMode(this, "live");
|
|
2241
|
+
const json = output === "json";
|
|
2242
|
+
const sort = opts.sort;
|
|
2243
|
+
const type = opts.type;
|
|
2244
|
+
const limit = parseInt(opts.limit, 10);
|
|
2245
|
+
const after = opts.after;
|
|
2246
|
+
if (isNaN(limit) || limit <= 0 || limit > 20) {
|
|
2247
|
+
outputErrorAndExit(
|
|
2248
|
+
json,
|
|
2249
|
+
`Invalid --limit value: ${opts.limit}. Must be an integer between 1 and 20.`,
|
|
2250
|
+
"Usage: zora explore --limit 10"
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
if (!QUERY_MAP[sort]) {
|
|
2254
|
+
outputErrorAndExit(
|
|
2255
|
+
json,
|
|
2256
|
+
`Invalid --sort value: ${sort}.`,
|
|
2257
|
+
`Supported: ${SORT_OPTIONS2}`
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
if (!QUERY_MAP[sort][type]) {
|
|
2261
|
+
const supported = Object.keys(QUERY_MAP[sort]);
|
|
2262
|
+
outputErrorAndExit(
|
|
2263
|
+
json,
|
|
2264
|
+
`Invalid --type for --sort ${sort}.`,
|
|
2265
|
+
`Supported: ${supported.join(", ")}`
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
const apiKey = getApiKey();
|
|
2269
|
+
if (apiKey) {
|
|
2270
|
+
setApiKey3(apiKey);
|
|
2271
|
+
}
|
|
2272
|
+
const queryFn = QUERY_MAP[sort][type];
|
|
2273
|
+
if (json) {
|
|
2274
|
+
let response;
|
|
2275
|
+
try {
|
|
2276
|
+
response = await queryFn({ count: limit, after });
|
|
2277
|
+
} catch (err) {
|
|
2278
|
+
outputErrorAndExit(
|
|
2279
|
+
json,
|
|
2280
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
if (response.error) {
|
|
2284
|
+
const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
|
|
2285
|
+
outputErrorAndExit(json, `API error: ${msg}`);
|
|
2286
|
+
}
|
|
2287
|
+
const edges = response.data?.exploreList?.edges ?? [];
|
|
2288
|
+
const coins = edges.map((e) => e.node);
|
|
2289
|
+
const pageInfo = response.data?.exploreList?.pageInfo;
|
|
2290
|
+
outputJson({ coins, pageInfo: pageInfo ?? null });
|
|
2291
|
+
track("cli_explore", {
|
|
2292
|
+
sort,
|
|
2293
|
+
type,
|
|
2294
|
+
limit,
|
|
2295
|
+
paginated: after !== void 0,
|
|
2296
|
+
result_count: coins.length,
|
|
2297
|
+
has_next_page: pageInfo?.hasNextPage ?? false,
|
|
2298
|
+
output_format: "json"
|
|
2299
|
+
});
|
|
2300
|
+
} else {
|
|
2301
|
+
const { live, intervalSeconds } = getLiveConfig(this, "live");
|
|
2302
|
+
const fetchPage = async (cursor) => {
|
|
2303
|
+
const response = await queryFn({ count: limit, after: cursor });
|
|
2304
|
+
if (response.error) {
|
|
2305
|
+
const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
|
|
2306
|
+
throw new Error(msg);
|
|
2307
|
+
}
|
|
2308
|
+
const edges = response.data?.exploreList?.edges ?? [];
|
|
2309
|
+
const coins = edges.map((e) => e.node);
|
|
2310
|
+
const pageInfo = response.data?.exploreList?.pageInfo;
|
|
2311
|
+
return { coins, pageInfo };
|
|
2312
|
+
};
|
|
2313
|
+
await renderLive(
|
|
2314
|
+
/* @__PURE__ */ jsx5(
|
|
2315
|
+
ExploreView,
|
|
2316
|
+
{
|
|
2317
|
+
fetchPage,
|
|
2318
|
+
sort,
|
|
2319
|
+
type,
|
|
2320
|
+
limit,
|
|
2321
|
+
initialCursor: after,
|
|
2322
|
+
autoRefresh: live,
|
|
2323
|
+
intervalSeconds
|
|
2324
|
+
}
|
|
2325
|
+
)
|
|
2326
|
+
);
|
|
2327
|
+
track("cli_explore", {
|
|
2328
|
+
sort,
|
|
2329
|
+
type,
|
|
2330
|
+
limit,
|
|
2331
|
+
live,
|
|
2332
|
+
interval: intervalSeconds,
|
|
2333
|
+
paginated: after !== void 0,
|
|
2334
|
+
output_format: "text"
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
// src/commands/get.tsx
|
|
2340
|
+
import { Command as Command5 } from "commander";
|
|
2341
|
+
import { setApiKey as setApiKey4 } from "@zoralabs/coins-sdk";
|
|
2342
|
+
|
|
2343
|
+
// src/lib/coin-ref.ts
|
|
2344
|
+
import { getCoin as getCoin2, getProfile, getTrend } from "@zoralabs/coins-sdk";
|
|
2345
|
+
var COIN_TYPE_MAP = {
|
|
2346
|
+
CONTENT: "post",
|
|
2347
|
+
CREATOR: "creator-coin",
|
|
2348
|
+
TREND: "trend"
|
|
2349
|
+
};
|
|
2350
|
+
function mapCoinType(raw) {
|
|
2351
|
+
if (!raw) return "unknown";
|
|
2352
|
+
return COIN_TYPE_MAP[raw] ?? "unknown";
|
|
2353
|
+
}
|
|
2354
|
+
function coinFromToken(token) {
|
|
2355
|
+
return {
|
|
2356
|
+
name: token.name ?? "Unknown",
|
|
2357
|
+
address: token.address ?? "",
|
|
2358
|
+
coinType: mapCoinType(token.coinType),
|
|
2359
|
+
marketCap: token.marketCap ?? "0",
|
|
2360
|
+
marketCapDelta24h: token.marketCapDelta24h ?? "0",
|
|
2361
|
+
volume24h: token.volume24h ?? "0",
|
|
2362
|
+
uniqueHolders: token.uniqueHolders ?? 0,
|
|
2363
|
+
createdAt: token.createdAt,
|
|
2364
|
+
creatorAddress: token.creatorAddress,
|
|
2365
|
+
creatorHandle: token.creatorProfile?.handle
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
function parseCoinRef(identifier, type) {
|
|
2369
|
+
if (identifier.startsWith("0x")) {
|
|
2370
|
+
return { kind: "address", address: identifier };
|
|
2371
|
+
}
|
|
2372
|
+
if (type === "creator-coin") {
|
|
2373
|
+
return { kind: "prefixed", type: "creator-coin", name: identifier };
|
|
2374
|
+
}
|
|
2375
|
+
if (type === "trend") {
|
|
2376
|
+
return { kind: "prefixed", type: "trend", name: identifier };
|
|
2377
|
+
}
|
|
2378
|
+
return { kind: "ambiguous", name: identifier };
|
|
2379
|
+
}
|
|
2380
|
+
async function resolveByAddress(address) {
|
|
2381
|
+
const response = await getCoin2({ address });
|
|
2382
|
+
if (response.error || !response.data?.zora20Token) {
|
|
2383
|
+
return {
|
|
2384
|
+
kind: "not-found",
|
|
2385
|
+
message: `No coin found at address ${address}`
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
return { kind: "found", coin: coinFromToken(response.data.zora20Token) };
|
|
2389
|
+
}
|
|
2390
|
+
async function resolveByTrendTicker(ticker) {
|
|
2391
|
+
const response = await getTrend({ ticker });
|
|
2392
|
+
if (response.error || !response.data?.trendCoin) {
|
|
2393
|
+
return {
|
|
2394
|
+
kind: "not-found",
|
|
2395
|
+
message: `No trend coin found with ticker "${ticker}"`
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
return { kind: "found", coin: coinFromToken(response.data.trendCoin) };
|
|
2399
|
+
}
|
|
2400
|
+
async function resolveByCreatorName(name) {
|
|
2401
|
+
const response = await getProfile({ identifier: name });
|
|
2402
|
+
if (response.error || !response.data?.profile) {
|
|
2403
|
+
return {
|
|
2404
|
+
kind: "not-found",
|
|
2405
|
+
message: `No creator found with name "${name}"`
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
const profile = response.data.profile;
|
|
2409
|
+
if (!profile.creatorCoin) {
|
|
2410
|
+
return {
|
|
2411
|
+
kind: "not-found",
|
|
2412
|
+
message: `"${name}" does not have a creator coin`
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
return resolveByAddress(profile.creatorCoin.address);
|
|
2416
|
+
}
|
|
2417
|
+
async function resolveCoin(ref) {
|
|
2418
|
+
switch (ref.kind) {
|
|
2419
|
+
case "address":
|
|
2420
|
+
return resolveByAddress(ref.address);
|
|
2421
|
+
case "prefixed":
|
|
2422
|
+
if (ref.type === "trend") {
|
|
2423
|
+
return resolveByTrendTicker(ref.name);
|
|
2424
|
+
}
|
|
2425
|
+
return resolveByCreatorName(ref.name);
|
|
2426
|
+
case "ambiguous":
|
|
2427
|
+
return resolveByCreatorName(ref.name);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// src/components/CoinDetail.tsx
|
|
2432
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
2433
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2434
|
+
var LABEL_WIDTH = 18;
|
|
2435
|
+
function Row({
|
|
2436
|
+
label,
|
|
2437
|
+
children
|
|
2438
|
+
}) {
|
|
2439
|
+
return /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
2440
|
+
/* @__PURE__ */ jsx6(Box5, { width: LABEL_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: label }) }),
|
|
2441
|
+
/* @__PURE__ */ jsx6(Text5, { children })
|
|
2442
|
+
] });
|
|
2443
|
+
}
|
|
2444
|
+
function CoinDetail({ coin }) {
|
|
2445
|
+
const change = formatMcapChange(coin.marketCap, coin.marketCapDelta24h);
|
|
2446
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingLeft: 1, children: [
|
|
2447
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
2448
|
+
/* @__PURE__ */ jsx6(Text5, { bold: true, children: coin.name }),
|
|
2449
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
2450
|
+
coin.coinType,
|
|
2451
|
+
" ",
|
|
2452
|
+
"\xB7",
|
|
2453
|
+
" ",
|
|
2454
|
+
coin.address
|
|
2455
|
+
] })
|
|
2456
|
+
] }),
|
|
2457
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
2458
|
+
/* @__PURE__ */ jsx6(Row, { label: "Market Cap", children: formatCompactUsd(coin.marketCap) }),
|
|
2459
|
+
/* @__PURE__ */ jsx6(Row, { label: "24h Volume", children: formatCompactUsd(coin.volume24h) }),
|
|
2460
|
+
/* @__PURE__ */ jsx6(Row, { label: "24h Change", children: /* @__PURE__ */ jsx6(Text5, { color: change.color, children: change.text }) }),
|
|
2461
|
+
/* @__PURE__ */ jsx6(Row, { label: "Holders", children: formatHolders(coin.uniqueHolders) }),
|
|
2462
|
+
coin.coinType === "post" && (coin.creatorHandle ?? coin.creatorAddress) && /* @__PURE__ */ jsx6(Row, { label: "Creator", children: coin.creatorHandle ?? coin.creatorAddress }),
|
|
2463
|
+
/* @__PURE__ */ jsx6(Row, { label: "Created", children: formatCreatedAt(coin.createdAt) })
|
|
2464
|
+
] }),
|
|
2465
|
+
/* @__PURE__ */ jsx6(Box5, { marginBottom: 1 })
|
|
2466
|
+
] });
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// src/commands/get.tsx
|
|
2470
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
2471
|
+
function formatCoinJson(coin) {
|
|
2472
|
+
return {
|
|
2473
|
+
name: coin.name,
|
|
2474
|
+
address: coin.address,
|
|
2475
|
+
coinType: coin.coinType,
|
|
2476
|
+
marketCap: coin.marketCap,
|
|
2477
|
+
marketCapDelta24h: coin.marketCapDelta24h,
|
|
2478
|
+
volume24h: coin.volume24h,
|
|
2479
|
+
uniqueHolders: coin.uniqueHolders,
|
|
2480
|
+
createdAt: coin.createdAt ?? null,
|
|
2481
|
+
creatorAddress: coin.creatorAddress ?? null,
|
|
2482
|
+
creatorHandle: coin.creatorHandle ?? null
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
var VALID_TYPES = ["creator-coin", "post", "trend"];
|
|
2486
|
+
var getCommand = new Command5("get").description("Look up a coin by address or name").argument("<identifier>", "Coin address (0x...) or creator name").option("--type <type>", "Coin type: creator-coin, post, trend").action(async function(identifier, opts) {
|
|
2487
|
+
const json = getJson(this);
|
|
2488
|
+
if (opts.type !== void 0 && !VALID_TYPES.includes(opts.type)) {
|
|
2489
|
+
outputErrorAndExit(
|
|
2490
|
+
json,
|
|
2491
|
+
`Invalid --type value: ${opts.type}.`,
|
|
2492
|
+
`Supported: ${VALID_TYPES.join(", ")}`
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
const type = opts.type;
|
|
2496
|
+
if (type === "post" && !identifier.startsWith("0x")) {
|
|
2497
|
+
outputErrorAndExit(
|
|
2498
|
+
json,
|
|
2499
|
+
"Posts can only be looked up by address.",
|
|
2500
|
+
"Use: zora get 0x..."
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
const ref = parseCoinRef(identifier, opts.type);
|
|
2504
|
+
const apiKey = getApiKey();
|
|
2505
|
+
if (apiKey) {
|
|
2506
|
+
setApiKey4(apiKey);
|
|
2507
|
+
}
|
|
2508
|
+
let result;
|
|
2509
|
+
try {
|
|
2510
|
+
result = await resolveCoin(ref);
|
|
2511
|
+
} catch (err) {
|
|
2512
|
+
outputErrorAndExit(
|
|
2513
|
+
json,
|
|
2514
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2515
|
+
);
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
if (type && result.kind === "found" && result.coin.coinType !== type) {
|
|
2519
|
+
outputErrorAndExit(
|
|
2520
|
+
json,
|
|
2521
|
+
`Coin at ${result.coin.address} is a ${result.coin.coinType}, not a ${type}.`,
|
|
2522
|
+
`Use: zora get ${result.coin.address} --type ${result.coin.coinType}`
|
|
2523
|
+
);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (result.kind === "not-found") {
|
|
2527
|
+
outputErrorAndExit(json, result.message);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
outputData(json, {
|
|
2531
|
+
json: formatCoinJson(result.coin),
|
|
2532
|
+
table: () => {
|
|
2533
|
+
renderOnce(/* @__PURE__ */ jsx7(CoinDetail, { coin: result.coin }));
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
track("cli_get", {
|
|
2537
|
+
lookup_type: identifier.startsWith("0x") ? "address" : "name",
|
|
2538
|
+
coin_type_filter: type ?? null,
|
|
2539
|
+
found: result.kind === "found",
|
|
2540
|
+
coin_type: result.kind === "found" ? result.coin.coinType : null,
|
|
2541
|
+
output_format: json ? "json" : "text"
|
|
2542
|
+
});
|
|
2543
|
+
});
|
|
2544
|
+
|
|
2545
|
+
// src/commands/price-history.tsx
|
|
2546
|
+
import { Command as Command6 } from "commander";
|
|
2547
|
+
import { setApiKey as setApiKey5, apiGet } from "@zoralabs/coins-sdk";
|
|
2548
|
+
|
|
2549
|
+
// src/components/PriceHistory.tsx
|
|
2550
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
2551
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2552
|
+
var LABEL_WIDTH2 = 18;
|
|
2553
|
+
var Row2 = ({
|
|
2554
|
+
label,
|
|
2555
|
+
children
|
|
2556
|
+
}) => /* @__PURE__ */ jsxs6(Box6, { children: [
|
|
2557
|
+
/* @__PURE__ */ jsx8(Box6, { width: LABEL_WIDTH2, flexShrink: 0, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: label }) }),
|
|
2558
|
+
/* @__PURE__ */ jsx8(Text6, { children })
|
|
2559
|
+
] });
|
|
2560
|
+
var PriceHistory = ({
|
|
2561
|
+
coin,
|
|
2562
|
+
coinType,
|
|
2563
|
+
interval,
|
|
2564
|
+
high,
|
|
2565
|
+
low,
|
|
2566
|
+
change,
|
|
2567
|
+
sparklineText
|
|
2568
|
+
}) => /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingLeft: 1, children: [
|
|
2569
|
+
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
|
|
2570
|
+
/* @__PURE__ */ jsx8(Row2, { label: "Coin", children: coin }),
|
|
2571
|
+
/* @__PURE__ */ jsx8(Row2, { label: "Type", children: coinType }),
|
|
2572
|
+
/* @__PURE__ */ jsx8(Row2, { label: "Interval", children: interval }),
|
|
2573
|
+
/* @__PURE__ */ jsx8(Row2, { label: "High", children: high }),
|
|
2574
|
+
/* @__PURE__ */ jsx8(Row2, { label: "Low", children: low }),
|
|
2575
|
+
/* @__PURE__ */ jsx8(Row2, { label: "Change", children: /* @__PURE__ */ jsx8(Text6, { color: change.color, children: change.text }) })
|
|
2576
|
+
] }),
|
|
2577
|
+
sparklineText.length > 0 && /* @__PURE__ */ jsx8(Box6, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx8(Text6, { children: sparklineText }) }),
|
|
2578
|
+
/* @__PURE__ */ jsx8(Box6, { marginBottom: 1 })
|
|
2579
|
+
] });
|
|
2580
|
+
|
|
2581
|
+
// src/lib/sparkline.ts
|
|
2582
|
+
var BLOCKS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
2583
|
+
var sparkline = (values) => {
|
|
2584
|
+
if (values.length <= 1) return values.length === 1 ? BLOCKS[4] : "";
|
|
2585
|
+
const min = Math.min(...values);
|
|
2586
|
+
const max = Math.max(...values);
|
|
2587
|
+
const range = max - min;
|
|
2588
|
+
if (range === 0) return BLOCKS[4].repeat(values.length);
|
|
2589
|
+
return values.map((v) => {
|
|
2590
|
+
const idx = Math.round((v - min) / range * (BLOCKS.length - 1));
|
|
2591
|
+
return BLOCKS[idx];
|
|
2592
|
+
}).join("");
|
|
2593
|
+
};
|
|
2594
|
+
var MAX_SPARKLINE_WIDTH = 50;
|
|
2595
|
+
var downsample = (values, maxWidth) => {
|
|
2596
|
+
if (values.length <= maxWidth) return values;
|
|
2597
|
+
const bucketSize = values.length / maxWidth;
|
|
2598
|
+
const result = [];
|
|
2599
|
+
for (let i = 0; i < maxWidth; i++) {
|
|
2600
|
+
const start = Math.floor(i * bucketSize);
|
|
2601
|
+
const end = Math.floor((i + 1) * bucketSize);
|
|
2602
|
+
let sum = 0;
|
|
2603
|
+
for (let j = start; j < end; j++) {
|
|
2604
|
+
sum += values[j];
|
|
2605
|
+
}
|
|
2606
|
+
result.push(sum / (end - start));
|
|
2607
|
+
}
|
|
2608
|
+
return result;
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
// src/commands/price-history.tsx
|
|
2612
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
2613
|
+
var VALID_TYPES2 = ["creator-coin", "post", "trend"];
|
|
2614
|
+
var VALID_INTERVALS = ["1h", "24h", "1w", "1m", "ALL"];
|
|
2615
|
+
var INTERVAL_TO_API_FIELD = {
|
|
2616
|
+
"1h": "oneHour",
|
|
2617
|
+
"24h": "oneDay",
|
|
2618
|
+
"1w": "oneWeek",
|
|
2619
|
+
"1m": "oneMonth",
|
|
2620
|
+
ALL: "all"
|
|
2621
|
+
};
|
|
2622
|
+
var formatPrice = (price) => {
|
|
2623
|
+
if (price >= 1) {
|
|
2624
|
+
return `$${price.toFixed(2)}`;
|
|
2625
|
+
}
|
|
2626
|
+
if (price >= 0.01) {
|
|
2627
|
+
return `$${price.toFixed(4)}`;
|
|
2628
|
+
}
|
|
2629
|
+
return `$${price.toPrecision(4)}`;
|
|
2630
|
+
};
|
|
2631
|
+
var formatChange = (first, last) => {
|
|
2632
|
+
if (first === 0) return { text: "-", color: void 0 };
|
|
2633
|
+
const pct = (last - first) / first * 100;
|
|
2634
|
+
const prefix = pct >= 0 ? "+" : "";
|
|
2635
|
+
const text = `${prefix}${pct.toFixed(1)}%`;
|
|
2636
|
+
const color = pct > 0 ? "green" : pct < 0 ? "red" : void 0;
|
|
2637
|
+
return { text, color };
|
|
2638
|
+
};
|
|
2639
|
+
var fetchPriceHistory = async (address, interval) => {
|
|
2640
|
+
const response = await apiGet("/coinPriceHistory", {
|
|
2641
|
+
address
|
|
2642
|
+
});
|
|
2643
|
+
const data = response.data;
|
|
2644
|
+
const token = data?.zora20Token;
|
|
2645
|
+
if (!token) return [];
|
|
2646
|
+
const field = INTERVAL_TO_API_FIELD[interval];
|
|
2647
|
+
const points = token[field];
|
|
2648
|
+
if (!points || points.length === 0) return [];
|
|
2649
|
+
return points.map((p) => ({
|
|
2650
|
+
timestamp: p.timestamp,
|
|
2651
|
+
price: Number(p.closePrice)
|
|
2652
|
+
}));
|
|
2653
|
+
};
|
|
2654
|
+
var priceHistoryCommand = new Command6("price-history").description("Display price history for a coin").argument("<identifier>", "Coin address (0x...) or name").option("--type <type>", "Coin type: creator-coin, post, trend").option(
|
|
2655
|
+
"--interval <interval>",
|
|
2656
|
+
`Time range: ${VALID_INTERVALS.join(", ")}`,
|
|
2657
|
+
"1w"
|
|
2658
|
+
).action(async function(identifier, opts) {
|
|
2659
|
+
const json = getJson(this);
|
|
2660
|
+
const interval = opts.interval ?? "1w";
|
|
2661
|
+
if (!VALID_INTERVALS.includes(interval)) {
|
|
2662
|
+
outputErrorAndExit(
|
|
2663
|
+
json,
|
|
2664
|
+
`Invalid --interval value: ${interval}.`,
|
|
2665
|
+
`Supported: ${VALID_INTERVALS.join(", ")}`
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
if (opts.type !== void 0 && !VALID_TYPES2.includes(opts.type)) {
|
|
2669
|
+
outputErrorAndExit(
|
|
2670
|
+
json,
|
|
2671
|
+
`Invalid --type value: ${opts.type}.`,
|
|
2672
|
+
`Supported: ${VALID_TYPES2.join(", ")}`
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
if (opts.type === "post" && !identifier.startsWith("0x")) {
|
|
2676
|
+
outputErrorAndExit(
|
|
2677
|
+
json,
|
|
2678
|
+
"Posts can only be looked up by address.",
|
|
2679
|
+
"Use: zora price-history 0x..."
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
const ref = parseCoinRef(identifier, opts.type);
|
|
2683
|
+
const apiKey = getApiKey();
|
|
2684
|
+
if (apiKey) {
|
|
2685
|
+
setApiKey5(apiKey);
|
|
2686
|
+
}
|
|
2687
|
+
let result;
|
|
2688
|
+
try {
|
|
2689
|
+
result = await resolveCoin(ref);
|
|
2690
|
+
} catch (err) {
|
|
2691
|
+
outputErrorAndExit(
|
|
2692
|
+
json,
|
|
2693
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2694
|
+
);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (result.kind === "not-found") {
|
|
2698
|
+
outputErrorAndExit(json, result.message, result.suggestion);
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
const { coin } = result;
|
|
2702
|
+
let prices;
|
|
2703
|
+
try {
|
|
2704
|
+
prices = await fetchPriceHistory(coin.address, interval);
|
|
2705
|
+
} catch (err) {
|
|
2706
|
+
outputErrorAndExit(
|
|
2707
|
+
json,
|
|
2708
|
+
`Failed to fetch price data: ${err instanceof Error ? err.message : String(err)}`
|
|
2709
|
+
);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
if (prices.length === 0) {
|
|
2713
|
+
outputErrorAndExit(
|
|
2714
|
+
json,
|
|
2715
|
+
`No price data found for ${coin.name} in the last ${interval}.`,
|
|
2716
|
+
"Try a longer interval with --interval"
|
|
2717
|
+
);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
const priceValues = prices.map((p) => p.price);
|
|
2721
|
+
const high = Math.max(...priceValues);
|
|
2722
|
+
const low = Math.min(...priceValues);
|
|
2723
|
+
const change = formatChange(
|
|
2724
|
+
priceValues[0],
|
|
2725
|
+
priceValues[priceValues.length - 1]
|
|
2726
|
+
);
|
|
2727
|
+
const sparklineText = sparkline(
|
|
2728
|
+
downsample(priceValues, MAX_SPARKLINE_WIDTH)
|
|
2729
|
+
);
|
|
2730
|
+
outputData(json, {
|
|
2731
|
+
json: {
|
|
2732
|
+
coin: coin.name,
|
|
2733
|
+
type: coin.coinType,
|
|
2734
|
+
interval,
|
|
2735
|
+
high,
|
|
2736
|
+
low,
|
|
2737
|
+
change: priceValues[0] === 0 ? null : (priceValues[priceValues.length - 1] - priceValues[0]) / priceValues[0],
|
|
2738
|
+
prices: prices.map((p) => ({
|
|
2739
|
+
timestamp: p.timestamp,
|
|
2740
|
+
price: p.price
|
|
2741
|
+
}))
|
|
2742
|
+
},
|
|
2743
|
+
table: () => {
|
|
2744
|
+
renderOnce(
|
|
2745
|
+
/* @__PURE__ */ jsx9(
|
|
2746
|
+
PriceHistory,
|
|
2747
|
+
{
|
|
2748
|
+
coin: coin.name,
|
|
2749
|
+
coinType: coin.coinType,
|
|
2750
|
+
interval,
|
|
2751
|
+
high: formatPrice(high),
|
|
2752
|
+
low: formatPrice(low),
|
|
2753
|
+
change,
|
|
2754
|
+
sparklineText
|
|
2755
|
+
}
|
|
2756
|
+
)
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
});
|
|
2760
|
+
track("cli_price_history", {
|
|
2761
|
+
lookup_type: identifier.startsWith("0x") ? "address" : "name",
|
|
2762
|
+
coin_type: coin.coinType,
|
|
2763
|
+
interval,
|
|
2764
|
+
data_points: prices.length,
|
|
2765
|
+
output_format: json ? "json" : "text"
|
|
2766
|
+
});
|
|
2767
|
+
});
|
|
2768
|
+
|
|
2769
|
+
// src/commands/sell.ts
|
|
2770
|
+
import { Command as Command7 } from "commander";
|
|
2771
|
+
import confirm3 from "@inquirer/confirm";
|
|
2772
|
+
import {
|
|
2773
|
+
erc20Abi as erc20Abi3,
|
|
2774
|
+
formatUnits as formatUnits4,
|
|
2775
|
+
isAddress as isAddress2,
|
|
2776
|
+
parseUnits as parseUnits2
|
|
2777
|
+
} from "viem";
|
|
2778
|
+
import {
|
|
2779
|
+
createTradeCall as createTradeCall2,
|
|
2780
|
+
getCoin as getCoin3,
|
|
2781
|
+
setApiKey as setApiKey6,
|
|
2782
|
+
tradeCoin as tradeCoin2
|
|
2783
|
+
} from "@zoralabs/coins-sdk";
|
|
2784
|
+
function printSellQuote(output, info) {
|
|
2785
|
+
if (output === "json") {
|
|
2786
|
+
outputJson({
|
|
2787
|
+
action: "quote",
|
|
2788
|
+
coin: info.coinSymbol,
|
|
2789
|
+
address: info.address,
|
|
2790
|
+
sell: {
|
|
2791
|
+
amount: formatUnits4(info.amountIn, info.coinDecimals),
|
|
2792
|
+
raw: info.amountIn.toString(),
|
|
2793
|
+
symbol: info.coinSymbol
|
|
2794
|
+
},
|
|
2795
|
+
estimated: {
|
|
2796
|
+
amount: formatUnits4(BigInt(info.quoteAmountOut), info.outputDecimals),
|
|
2797
|
+
raw: info.quoteAmountOut,
|
|
2798
|
+
symbol: info.outputSymbol
|
|
2799
|
+
},
|
|
2800
|
+
slippage: info.slippagePct
|
|
2801
|
+
});
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
console.log(`
|
|
2805
|
+
Sell ${info.coinName} (${info.coinSymbol})
|
|
2806
|
+
`);
|
|
2807
|
+
console.log(` Amount ${info.soldFormatted} ${info.coinSymbol}`);
|
|
2808
|
+
console.log(
|
|
2809
|
+
` You get ~${info.receivedFormatted} ${info.outputSymbol}`
|
|
2810
|
+
);
|
|
2811
|
+
console.log(` Slippage ${info.slippagePct}%
|
|
2812
|
+
`);
|
|
2813
|
+
}
|
|
2814
|
+
function printSellResult(output, info) {
|
|
2815
|
+
const receivedAmount = formatUnits4(
|
|
2816
|
+
info.receivedAmountOut,
|
|
2817
|
+
info.outputDecimals
|
|
2818
|
+
);
|
|
2819
|
+
const receivedFormatted = formatAmountDisplay(
|
|
2820
|
+
info.receivedAmountOut,
|
|
2821
|
+
info.outputDecimals
|
|
2822
|
+
);
|
|
2823
|
+
if (output === "json") {
|
|
2824
|
+
outputJson({
|
|
2825
|
+
action: "sell",
|
|
2826
|
+
coin: info.coinSymbol,
|
|
2827
|
+
address: info.address,
|
|
2828
|
+
sold: {
|
|
2829
|
+
amount: formatUnits4(info.amountIn, info.coinDecimals),
|
|
2830
|
+
raw: info.amountIn.toString(),
|
|
2831
|
+
symbol: info.coinSymbol
|
|
2832
|
+
},
|
|
2833
|
+
received: {
|
|
2834
|
+
amount: receivedAmount,
|
|
2835
|
+
raw: info.receivedAmountOut.toString(),
|
|
2836
|
+
symbol: info.outputSymbol,
|
|
2837
|
+
source: info.receivedSource
|
|
2838
|
+
},
|
|
2839
|
+
tx: info.txHash
|
|
2840
|
+
});
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
console.log(`
|
|
2844
|
+
Sold ${info.coinName}
|
|
2845
|
+
`);
|
|
2846
|
+
console.log(` Sold ${info.soldFormatted} ${info.coinSymbol}`);
|
|
2847
|
+
console.log(
|
|
2848
|
+
` Received ${info.receivedSource === "quote" ? "~" : ""}${receivedFormatted} ${info.outputSymbol}`
|
|
2849
|
+
);
|
|
2850
|
+
if (info.receivedSource === "quote") {
|
|
2851
|
+
console.log(" Note based on quote");
|
|
2852
|
+
}
|
|
2853
|
+
console.log(` Tx ${info.txHash}
|
|
2854
|
+
`);
|
|
2855
|
+
}
|
|
2856
|
+
var sellCommand = new Command7("sell").description("Sell a coin").argument("<address>", "Coin contract address (0x\u2026)").option("--amount <value>", "Sell specific number of coins").option("--usd <value>", "Sell USD equivalent worth of coins").option("--percent <value>", "Sell percentage of coin balance").option("--all", "Sell entire coin balance").option("--to <asset>", "Receive asset: eth, usdc, zora", "eth").option("--token <asset>", "Receive asset: eth, usdc, zora (alias for --to)").option("--quote", "Print quote and exit without trading").option("--yes", "Skip confirmation and execute directly").option("--slippage <pct>", "Slippage tolerance percent", "1").option("--debug", "Print full quote request/response JSON").action(async function(coinAddress, opts) {
|
|
2857
|
+
const json = getJson(this);
|
|
2858
|
+
const debug = opts.debug === true;
|
|
2859
|
+
if (!isAddress2(coinAddress)) {
|
|
2860
|
+
outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
|
|
2861
|
+
}
|
|
2862
|
+
const output = json ? "json" : "table";
|
|
2863
|
+
const outputAsset = opts.token ? opts.token.toLowerCase() : opts.to;
|
|
2864
|
+
if (!(outputAsset in BASE_TRADE_TOKENS)) {
|
|
2865
|
+
outputErrorAndExit(
|
|
2866
|
+
json,
|
|
2867
|
+
`Invalid --${opts.token ? "token" : "to"} value: ${outputAsset}. Use: eth, usdc, zora`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
const outputToken = BASE_TRADE_TOKENS[outputAsset];
|
|
2871
|
+
const amountMode = getAmountMode(
|
|
2872
|
+
json,
|
|
2873
|
+
opts,
|
|
2874
|
+
SELL_AMOUNT_CHECKS,
|
|
2875
|
+
"--amount, --usd, --percent, or --all"
|
|
2876
|
+
);
|
|
2877
|
+
const slippagePct = parsePercentageLikeValue(opts.slippage);
|
|
2878
|
+
if (slippagePct === void 0 || slippagePct < 0 || slippagePct > 99) {
|
|
2879
|
+
outputErrorAndExit(
|
|
2880
|
+
json,
|
|
2881
|
+
"Invalid --slippage value. Must be between 0 and 99."
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2884
|
+
const slippage = slippagePct / 100;
|
|
2885
|
+
const apiKey = getApiKey();
|
|
2886
|
+
if (apiKey) {
|
|
2887
|
+
setApiKey6(apiKey);
|
|
2888
|
+
}
|
|
2889
|
+
const account = resolveAccount(json);
|
|
2890
|
+
const { publicClient, walletClient } = createClients(account);
|
|
2891
|
+
let token;
|
|
2892
|
+
try {
|
|
2893
|
+
const response = await getCoin3({ address: coinAddress });
|
|
2894
|
+
token = response.data?.zora20Token;
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
outputErrorAndExit(
|
|
2897
|
+
json,
|
|
2898
|
+
`Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
if (!token) {
|
|
2902
|
+
outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
|
|
2903
|
+
}
|
|
2904
|
+
const coinName = token.name;
|
|
2905
|
+
const coinSymbol = token.symbol;
|
|
2906
|
+
const coinDecimals = Number(token.decimals ?? 18);
|
|
2907
|
+
let amountIn;
|
|
2908
|
+
if (amountMode === "usd") {
|
|
2909
|
+
const usdVal = parsePercentageLikeValue(opts.usd);
|
|
2910
|
+
if (usdVal === void 0 || usdVal <= 0) {
|
|
2911
|
+
outputErrorAndExit(
|
|
2912
|
+
json,
|
|
2913
|
+
"Invalid --usd value. Must be a positive number."
|
|
2914
|
+
);
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
const coinPriceUsd = await fetchTokenPriceUsd(coinAddress);
|
|
2918
|
+
if (coinPriceUsd === null || coinPriceUsd <= 0) {
|
|
2919
|
+
outputErrorAndExit(
|
|
2920
|
+
json,
|
|
2921
|
+
`Failed to fetch ${coinSymbol} price for USD conversion.`
|
|
2922
|
+
);
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
const coinAmount = usdVal / coinPriceUsd;
|
|
2926
|
+
amountIn = parseUnits2(coinAmount.toFixed(coinDecimals), coinDecimals);
|
|
2927
|
+
if (amountIn === 0n) {
|
|
2928
|
+
outputErrorAndExit(json, "Calculated amount is zero. USD too small.");
|
|
2929
|
+
}
|
|
2930
|
+
if (debug) {
|
|
2931
|
+
console.error(
|
|
2932
|
+
`[debug] $${usdVal} USD = ${formatUnits4(amountIn, coinDecimals)} ${coinSymbol} (coin price: $${coinPriceUsd})`
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
} else if (amountMode === "amount") {
|
|
2936
|
+
const val = parsePercentageLikeValue(opts.amount);
|
|
2937
|
+
if (val === void 0 || val <= 0) {
|
|
2938
|
+
outputErrorAndExit(
|
|
2939
|
+
json,
|
|
2940
|
+
"Invalid --amount value. Must be a positive number."
|
|
2941
|
+
);
|
|
2942
|
+
}
|
|
2943
|
+
try {
|
|
2944
|
+
amountIn = parseUnits2(opts.amount, coinDecimals);
|
|
2945
|
+
} catch {
|
|
2946
|
+
outputErrorAndExit(json, "Invalid --amount value for token decimals.");
|
|
2947
|
+
}
|
|
2948
|
+
} else {
|
|
2949
|
+
const balance = await publicClient.readContract({
|
|
2950
|
+
abi: erc20Abi3,
|
|
2951
|
+
address: coinAddress,
|
|
2952
|
+
functionName: "balanceOf",
|
|
2953
|
+
args: [account.address]
|
|
2954
|
+
});
|
|
2955
|
+
if (balance === 0n) {
|
|
2956
|
+
outputErrorAndExit(
|
|
2957
|
+
json,
|
|
2958
|
+
`No ${coinSymbol} balance. Buy some first or pick a different wallet.`
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
if (amountMode === "all") {
|
|
2962
|
+
amountIn = balance;
|
|
2963
|
+
} else {
|
|
2964
|
+
const pct = parsePercentageLikeValue(opts.percent);
|
|
2965
|
+
if (pct === void 0 || pct <= 0 || pct > 100) {
|
|
2966
|
+
outputErrorAndExit(
|
|
2967
|
+
json,
|
|
2968
|
+
"Invalid --percent value. Must be between 0 and 100."
|
|
2969
|
+
);
|
|
2970
|
+
}
|
|
2971
|
+
amountIn = pct === 100 ? balance : balance * BigInt(Math.round(pct * 100)) / 10000n;
|
|
2972
|
+
if (amountIn === 0n) {
|
|
2973
|
+
outputErrorAndExit(
|
|
2974
|
+
json,
|
|
2975
|
+
"Calculated amount is zero. Balance too low."
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
let swapAmountUsd;
|
|
2981
|
+
if (amountMode === "usd") {
|
|
2982
|
+
swapAmountUsd = parsePercentageLikeValue(opts.usd);
|
|
2983
|
+
} else {
|
|
2984
|
+
const coinPriceUsd = await fetchTokenPriceUsd(coinAddress);
|
|
2985
|
+
if (coinPriceUsd !== null && coinPriceUsd > 0) {
|
|
2986
|
+
swapAmountUsd = Number(
|
|
2987
|
+
(Number(formatUnits4(amountIn, coinDecimals)) * coinPriceUsd).toFixed(
|
|
2988
|
+
2
|
|
2989
|
+
)
|
|
2990
|
+
);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
const tradeParameters = {
|
|
2994
|
+
sell: { type: "erc20", address: coinAddress },
|
|
2995
|
+
buy: outputToken.trade,
|
|
2996
|
+
amountIn,
|
|
2997
|
+
slippage,
|
|
2998
|
+
sender: account.address
|
|
2999
|
+
};
|
|
3000
|
+
if (debug) {
|
|
3001
|
+
printDebugRequest("sell", tradeParameters);
|
|
3002
|
+
}
|
|
3003
|
+
let quoteAmountOut;
|
|
3004
|
+
try {
|
|
3005
|
+
const quote = await createTradeCall2(tradeParameters);
|
|
3006
|
+
if (debug) {
|
|
3007
|
+
printDebugResponse("sell", quote);
|
|
3008
|
+
}
|
|
3009
|
+
if (!quote.quote?.amountOut || quote.quote.amountOut === "0") {
|
|
3010
|
+
outputErrorAndExit(
|
|
3011
|
+
json,
|
|
3012
|
+
"Quote returned zero output. Amount may be too small."
|
|
3013
|
+
);
|
|
3014
|
+
}
|
|
3015
|
+
quoteAmountOut = quote.quote.amountOut;
|
|
3016
|
+
} catch (err) {
|
|
3017
|
+
if (debug) {
|
|
3018
|
+
console.error(
|
|
3019
|
+
`
|
|
3020
|
+
[debug] sell \u2014 Quote Error:
|
|
3021
|
+
${err instanceof Error ? err.stack || err.message : String(err)}
|
|
3022
|
+
`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3026
|
+
const errorType = err?.errorType;
|
|
3027
|
+
const errorBody = err?.errorBody;
|
|
3028
|
+
if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
|
|
3029
|
+
if (json) {
|
|
3030
|
+
outputJson({ error: errorBody ?? msg });
|
|
3031
|
+
process.exit(1);
|
|
3032
|
+
}
|
|
3033
|
+
outputErrorAndExit(
|
|
3034
|
+
json,
|
|
3035
|
+
"Not enough available liquidity for your swap. Please try swapping fewer tokens."
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
outputErrorAndExit(
|
|
3039
|
+
json,
|
|
3040
|
+
`Quote failed: ${msg}`,
|
|
3041
|
+
"Check the coin address and amount, then try again. Use --debug for full error details."
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
const soldFormatted = formatAmountDisplay(amountIn, coinDecimals);
|
|
3045
|
+
const receivedFormatted = formatAmountDisplay(
|
|
3046
|
+
BigInt(quoteAmountOut),
|
|
3047
|
+
outputToken.decimals
|
|
3048
|
+
);
|
|
3049
|
+
if (opts.quote) {
|
|
3050
|
+
printSellQuote(output, {
|
|
3051
|
+
coinName,
|
|
3052
|
+
coinSymbol,
|
|
3053
|
+
address: coinAddress,
|
|
3054
|
+
soldFormatted,
|
|
3055
|
+
amountIn,
|
|
3056
|
+
coinDecimals,
|
|
3057
|
+
receivedFormatted,
|
|
3058
|
+
quoteAmountOut,
|
|
3059
|
+
outputSymbol: outputToken.symbol,
|
|
3060
|
+
outputDecimals: outputToken.decimals,
|
|
3061
|
+
slippagePct
|
|
3062
|
+
});
|
|
3063
|
+
track("cli_sell", {
|
|
3064
|
+
action: "quote",
|
|
3065
|
+
coin_address: coinAddress,
|
|
3066
|
+
coin_name: coinName,
|
|
3067
|
+
coin_symbol: coinSymbol,
|
|
3068
|
+
amount_mode: amountMode,
|
|
3069
|
+
swap_amount_usd: swapAmountUsd,
|
|
3070
|
+
valueUsd: swapAmountUsd,
|
|
3071
|
+
swapCoinType: token.coinType ?? null,
|
|
3072
|
+
output_asset: outputAsset,
|
|
3073
|
+
slippage: slippagePct,
|
|
3074
|
+
output_format: output
|
|
3075
|
+
});
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
if (!opts.yes) {
|
|
3079
|
+
printSellQuote("table", {
|
|
3080
|
+
coinName,
|
|
3081
|
+
coinSymbol,
|
|
3082
|
+
address: coinAddress,
|
|
3083
|
+
soldFormatted,
|
|
3084
|
+
amountIn,
|
|
3085
|
+
coinDecimals,
|
|
3086
|
+
receivedFormatted,
|
|
3087
|
+
quoteAmountOut,
|
|
3088
|
+
outputSymbol: outputToken.symbol,
|
|
3089
|
+
outputDecimals: outputToken.decimals,
|
|
3090
|
+
slippagePct
|
|
3091
|
+
});
|
|
3092
|
+
const ok = await confirm3({
|
|
3093
|
+
message: "Confirm?",
|
|
3094
|
+
default: false
|
|
3095
|
+
});
|
|
3096
|
+
if (!ok) {
|
|
3097
|
+
console.error("Aborted.");
|
|
3098
|
+
process.exit(0);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
let receipt;
|
|
3102
|
+
let txHash;
|
|
3103
|
+
let receivedAmountOut = BigInt(quoteAmountOut);
|
|
3104
|
+
let receivedSource = "quote";
|
|
3105
|
+
let swapLogIndex = null;
|
|
3106
|
+
const swapCoinType = token.coinType ?? null;
|
|
3107
|
+
try {
|
|
3108
|
+
receipt = await tradeCoin2({
|
|
3109
|
+
tradeParameters,
|
|
3110
|
+
walletClient,
|
|
3111
|
+
publicClient,
|
|
3112
|
+
account
|
|
3113
|
+
});
|
|
3114
|
+
} catch (err) {
|
|
3115
|
+
track("cli_sell", {
|
|
3116
|
+
action: "trade",
|
|
3117
|
+
coin_address: coinAddress,
|
|
3118
|
+
coin_name: coinName,
|
|
3119
|
+
coin_symbol: coinSymbol,
|
|
3120
|
+
amount_mode: amountMode,
|
|
3121
|
+
swap_amount_usd: swapAmountUsd,
|
|
3122
|
+
valueUsd: swapAmountUsd,
|
|
3123
|
+
swapCoinType,
|
|
3124
|
+
output_asset: outputAsset,
|
|
3125
|
+
slippage: slippagePct,
|
|
3126
|
+
output_format: output,
|
|
3127
|
+
success: false,
|
|
3128
|
+
error_type: err instanceof Error ? err.constructor.name : "unknown"
|
|
3129
|
+
});
|
|
3130
|
+
await shutdownAnalytics();
|
|
3131
|
+
outputErrorAndExit(
|
|
3132
|
+
json,
|
|
3133
|
+
`Transaction failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
txHash = receipt.transactionHash;
|
|
3137
|
+
if (outputToken.trade.type === "erc20") {
|
|
3138
|
+
try {
|
|
3139
|
+
const result = getReceivedAmountFromReceipt({
|
|
3140
|
+
receipt,
|
|
3141
|
+
tokenAddress: outputToken.trade.address,
|
|
3142
|
+
recipient: account.address
|
|
3143
|
+
});
|
|
3144
|
+
receivedAmountOut = result.amount;
|
|
3145
|
+
swapLogIndex = result.logIndex;
|
|
3146
|
+
receivedSource = "receipt";
|
|
3147
|
+
} catch {
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
printSellResult(output, {
|
|
3151
|
+
coinName,
|
|
3152
|
+
coinSymbol,
|
|
3153
|
+
address: coinAddress,
|
|
3154
|
+
amountIn,
|
|
3155
|
+
coinDecimals,
|
|
3156
|
+
soldFormatted,
|
|
3157
|
+
receivedAmountOut,
|
|
3158
|
+
outputSymbol: outputToken.symbol,
|
|
3159
|
+
outputDecimals: outputToken.decimals,
|
|
3160
|
+
receivedSource,
|
|
3161
|
+
txHash
|
|
3162
|
+
});
|
|
3163
|
+
track("cli_sell", {
|
|
3164
|
+
action: "trade",
|
|
3165
|
+
coin_address: coinAddress,
|
|
3166
|
+
coin_name: coinName,
|
|
3167
|
+
coin_symbol: coinSymbol,
|
|
3168
|
+
amount_mode: amountMode,
|
|
3169
|
+
swap_amount_usd: swapAmountUsd,
|
|
3170
|
+
valueUsd: swapAmountUsd,
|
|
3171
|
+
swapCoinType,
|
|
3172
|
+
transactionHash: txHash,
|
|
3173
|
+
logIndex: swapLogIndex,
|
|
3174
|
+
output_asset: outputAsset,
|
|
3175
|
+
slippage: slippagePct,
|
|
3176
|
+
output_format: output,
|
|
3177
|
+
success: true,
|
|
3178
|
+
tx_hash: txHash
|
|
3179
|
+
});
|
|
3180
|
+
});
|
|
3181
|
+
|
|
3182
|
+
// src/commands/send.ts
|
|
3183
|
+
import { Command as Command8 } from "commander";
|
|
3184
|
+
import confirm4 from "@inquirer/confirm";
|
|
3185
|
+
import {
|
|
3186
|
+
erc20Abi as erc20Abi4,
|
|
3187
|
+
formatUnits as formatUnits5,
|
|
3188
|
+
isAddress as isAddress3,
|
|
3189
|
+
parseUnits as parseUnits3
|
|
3190
|
+
} from "viem";
|
|
3191
|
+
import { setApiKey as setApiKey7 } from "@zoralabs/coins-sdk";
|
|
3192
|
+
var SEND_AMOUNT_CHECKS = {
|
|
3193
|
+
amount: (opts) => opts.amount !== void 0,
|
|
3194
|
+
percent: (opts) => opts.percent !== void 0,
|
|
3195
|
+
all: (opts) => opts.all === true
|
|
3196
|
+
};
|
|
3197
|
+
var VALID_TYPES3 = ["creator-coin", "post", "trend"];
|
|
3198
|
+
function printSendPreview(info) {
|
|
3199
|
+
const usdStr = info.amountUsd != null ? ` ($${info.amountUsd.toFixed(2)})` : "";
|
|
3200
|
+
console.log(`
|
|
3201
|
+
Send ${info.name} (${info.symbol})
|
|
3202
|
+
`);
|
|
3203
|
+
console.log(
|
|
3204
|
+
` Amount ${info.amountFormatted} ${info.symbol}${usdStr}`
|
|
3205
|
+
);
|
|
3206
|
+
console.log(` To ${info.to}`);
|
|
3207
|
+
console.log(`
|
|
3208
|
+
Ensure receiving wallet can receive on Base.`);
|
|
3209
|
+
console.log("");
|
|
3210
|
+
}
|
|
3211
|
+
function printSendResult(json, info) {
|
|
3212
|
+
if (json) {
|
|
3213
|
+
outputJson({
|
|
3214
|
+
action: "send",
|
|
3215
|
+
coin: info.symbol,
|
|
3216
|
+
address: info.address,
|
|
3217
|
+
sent: {
|
|
3218
|
+
amount: formatUnits5(info.amount, info.decimals),
|
|
3219
|
+
raw: info.amount.toString(),
|
|
3220
|
+
symbol: info.symbol,
|
|
3221
|
+
amountUsd: info.amountUsd
|
|
3222
|
+
},
|
|
3223
|
+
to: info.to,
|
|
3224
|
+
tx: info.txHash
|
|
3225
|
+
});
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
const usdStr = info.amountUsd != null ? ` ($${info.amountUsd.toFixed(2)})` : "";
|
|
3229
|
+
console.log(`
|
|
3230
|
+
Sent ${info.name}
|
|
3231
|
+
`);
|
|
3232
|
+
console.log(
|
|
3233
|
+
` Amount ${info.amountFormatted} ${info.symbol}${usdStr}`
|
|
3234
|
+
);
|
|
3235
|
+
console.log(` To ${info.to}`);
|
|
3236
|
+
console.log(` Tx ${info.txHash}
|
|
3237
|
+
`);
|
|
3238
|
+
}
|
|
3239
|
+
var sendCommand = new Command8("send").description("Send coins or ETH to an address").argument("<identifier>", "Coin address, name, or token (eth, usdc, zora)").requiredOption("--to <address>", "Recipient address (0x...)").option("--type <type>", "Coin type: creator-coin, post, trend").option("--amount <value>", "Send specific amount").option("--percent <value>", "Send percentage of balance (1-100)").option("--all", "Send entire balance").option("--yes", "Skip confirmation").action(async function(identifier, opts) {
|
|
3240
|
+
const json = getJson(this);
|
|
3241
|
+
if (!isAddress3(opts.to)) {
|
|
3242
|
+
outputErrorAndExit(
|
|
3243
|
+
json,
|
|
3244
|
+
`Invalid recipient address: ${opts.to}`,
|
|
3245
|
+
"Must be a valid 0x address."
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
const recipient = opts.to;
|
|
3249
|
+
if (opts.type !== void 0 && !VALID_TYPES3.includes(opts.type)) {
|
|
3250
|
+
outputErrorAndExit(
|
|
3251
|
+
json,
|
|
3252
|
+
`Invalid --type value: ${opts.type}.`,
|
|
3253
|
+
`Supported: ${VALID_TYPES3.join(", ")}`
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
const amountMode = getAmountMode(
|
|
3257
|
+
json,
|
|
3258
|
+
opts,
|
|
3259
|
+
SEND_AMOUNT_CHECKS,
|
|
3260
|
+
"--amount, --percent, or --all"
|
|
3261
|
+
);
|
|
3262
|
+
const isEth = identifier.toLowerCase() === "eth";
|
|
3263
|
+
if (isEth) {
|
|
3264
|
+
if (opts.type) {
|
|
3265
|
+
outputErrorAndExit(json, "--type is not valid when sending ETH.");
|
|
3266
|
+
}
|
|
3267
|
+
const account = resolveAccount(json);
|
|
3268
|
+
const { publicClient, walletClient } = createClients(account);
|
|
3269
|
+
const balance = await publicClient.getBalance({
|
|
3270
|
+
address: account.address
|
|
3271
|
+
});
|
|
3272
|
+
if (balance === 0n) {
|
|
3273
|
+
outputErrorAndExit(
|
|
3274
|
+
json,
|
|
3275
|
+
`No ETH balance. Deposit ETH to ${account.address} on Base.`
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
let amount;
|
|
3279
|
+
if (amountMode === "amount") {
|
|
3280
|
+
const val = parsePercentageLikeValue(opts.amount);
|
|
3281
|
+
if (val === void 0 || val <= 0) {
|
|
3282
|
+
outputErrorAndExit(
|
|
3283
|
+
json,
|
|
3284
|
+
"Invalid --amount value. Must be a positive number."
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
try {
|
|
3288
|
+
amount = parseUnits3(opts.amount, 18);
|
|
3289
|
+
} catch {
|
|
3290
|
+
outputErrorAndExit(
|
|
3291
|
+
json,
|
|
3292
|
+
"Invalid --amount value. Must be a valid ETH amount."
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
if (amount === 0n) {
|
|
3296
|
+
outputErrorAndExit(
|
|
3297
|
+
json,
|
|
3298
|
+
"Amount too small \u2014 rounds to zero at 18 decimal places."
|
|
3299
|
+
);
|
|
3300
|
+
}
|
|
3301
|
+
if (amount + GAS_RESERVE > balance) {
|
|
3302
|
+
outputErrorAndExit(
|
|
3303
|
+
json,
|
|
3304
|
+
`Insufficient balance. Have ${formatEthDisplay(balance)} ETH (need to reserve ~${formatEthDisplay(GAS_RESERVE)} ETH for gas).`
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
} else {
|
|
3308
|
+
if (balance <= GAS_RESERVE) {
|
|
3309
|
+
outputErrorAndExit(
|
|
3310
|
+
json,
|
|
3311
|
+
`Balance too low (${formatEthDisplay(balance)} ETH). Need >${formatEthDisplay(GAS_RESERVE)} ETH for gas.`
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
const spendable = balance - GAS_RESERVE;
|
|
3315
|
+
if (amountMode === "all") {
|
|
3316
|
+
amount = spendable;
|
|
3317
|
+
} else {
|
|
3318
|
+
const pct = parsePercentageLikeValue(opts.percent);
|
|
3319
|
+
if (pct === void 0 || pct <= 0 || pct > 100) {
|
|
3320
|
+
outputErrorAndExit(
|
|
3321
|
+
json,
|
|
3322
|
+
"Invalid --percent value. Must be between 0 and 100."
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
amount = pct === 100 ? spendable : spendable * BigInt(Math.round(pct * 100)) / 10000n;
|
|
3326
|
+
if (amount === 0n) {
|
|
3327
|
+
outputErrorAndExit(
|
|
3328
|
+
json,
|
|
3329
|
+
"Calculated amount is zero. Balance too low."
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
const amountFormatted = formatEthDisplay(amount);
|
|
3335
|
+
let amountUsd = null;
|
|
3336
|
+
const ethPriceUsd = await fetchTokenPriceUsd(WETH_ADDRESS);
|
|
3337
|
+
if (ethPriceUsd != null) {
|
|
3338
|
+
amountUsd = Number(
|
|
3339
|
+
(Number(formatUnits5(amount, 18)) * ethPriceUsd).toFixed(2)
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
if (!opts.yes) {
|
|
3343
|
+
printSendPreview({
|
|
3344
|
+
name: "ETH",
|
|
3345
|
+
symbol: "ETH",
|
|
3346
|
+
amountFormatted,
|
|
3347
|
+
amountUsd,
|
|
3348
|
+
to: recipient
|
|
3349
|
+
});
|
|
3350
|
+
const ok = await confirm4({ message: "Confirm?", default: false });
|
|
3351
|
+
if (!ok) {
|
|
3352
|
+
console.error("Aborted.");
|
|
3353
|
+
process.exit(0);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
let txHash;
|
|
3357
|
+
try {
|
|
3358
|
+
txHash = await walletClient.sendTransaction({
|
|
3359
|
+
to: recipient,
|
|
3360
|
+
value: amount
|
|
3361
|
+
});
|
|
3362
|
+
} catch (err) {
|
|
3363
|
+
track("cli_send", {
|
|
3364
|
+
asset: "eth",
|
|
3365
|
+
output_format: json ? "json" : "table",
|
|
3366
|
+
success: false,
|
|
3367
|
+
error_type: err instanceof Error ? err.constructor.name : "unknown"
|
|
3368
|
+
});
|
|
3369
|
+
await shutdownAnalytics();
|
|
3370
|
+
outputErrorAndExit(
|
|
3371
|
+
json,
|
|
3372
|
+
`Transaction failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
await publicClient.waitForTransactionReceipt({
|
|
3376
|
+
hash: txHash
|
|
3377
|
+
});
|
|
3378
|
+
printSendResult(json, {
|
|
3379
|
+
name: "ETH",
|
|
3380
|
+
symbol: "ETH",
|
|
3381
|
+
address: null,
|
|
3382
|
+
amount,
|
|
3383
|
+
decimals: 18,
|
|
3384
|
+
amountFormatted,
|
|
3385
|
+
amountUsd,
|
|
3386
|
+
to: recipient,
|
|
3387
|
+
txHash
|
|
3388
|
+
});
|
|
3389
|
+
track("cli_send", {
|
|
3390
|
+
asset: "eth",
|
|
3391
|
+
amount_mode: amountMode,
|
|
3392
|
+
amount_usd: amountUsd,
|
|
3393
|
+
transactionHash: txHash,
|
|
3394
|
+
output_format: json ? "json" : "table",
|
|
3395
|
+
success: true,
|
|
3396
|
+
tx_hash: txHash
|
|
3397
|
+
});
|
|
3398
|
+
} else {
|
|
3399
|
+
const knownTokenKey = identifier.toLowerCase();
|
|
3400
|
+
const knownToken = knownTokenKey !== "eth" && knownTokenKey in BASE_TRADE_TOKENS ? BASE_TRADE_TOKENS[knownTokenKey] : void 0;
|
|
3401
|
+
if (knownToken && opts.type) {
|
|
3402
|
+
outputErrorAndExit(
|
|
3403
|
+
json,
|
|
3404
|
+
`--type is not valid when sending ${knownToken.symbol}.`
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
let tokenAddress;
|
|
3408
|
+
let tokenName;
|
|
3409
|
+
if (knownToken) {
|
|
3410
|
+
const trade = knownToken.trade;
|
|
3411
|
+
tokenAddress = trade.address;
|
|
3412
|
+
tokenName = knownToken.symbol;
|
|
3413
|
+
} else {
|
|
3414
|
+
const apiKey = getApiKey();
|
|
3415
|
+
if (apiKey) {
|
|
3416
|
+
setApiKey7(apiKey);
|
|
3417
|
+
}
|
|
3418
|
+
const ref = parseCoinRef(identifier, opts.type);
|
|
3419
|
+
let result;
|
|
3420
|
+
try {
|
|
3421
|
+
result = await resolveCoin(ref);
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
outputErrorAndExit(
|
|
3424
|
+
json,
|
|
3425
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
if (result.kind === "not-found") {
|
|
3429
|
+
outputErrorAndExit(json, result.message, result.suggestion);
|
|
3430
|
+
}
|
|
3431
|
+
tokenAddress = result.coin.address;
|
|
3432
|
+
tokenName = result.coin.name;
|
|
3433
|
+
}
|
|
3434
|
+
const account = resolveAccount(json);
|
|
3435
|
+
const { publicClient, walletClient } = createClients(account);
|
|
3436
|
+
let balance;
|
|
3437
|
+
let decimals;
|
|
3438
|
+
let symbol;
|
|
3439
|
+
if (knownToken) {
|
|
3440
|
+
balance = await publicClient.readContract({
|
|
3441
|
+
abi: erc20Abi4,
|
|
3442
|
+
address: tokenAddress,
|
|
3443
|
+
functionName: "balanceOf",
|
|
3444
|
+
args: [account.address]
|
|
3445
|
+
});
|
|
3446
|
+
decimals = knownToken.decimals;
|
|
3447
|
+
symbol = knownToken.symbol;
|
|
3448
|
+
} else {
|
|
3449
|
+
const results = await Promise.all([
|
|
3450
|
+
publicClient.readContract({
|
|
3451
|
+
abi: erc20Abi4,
|
|
3452
|
+
address: tokenAddress,
|
|
3453
|
+
functionName: "balanceOf",
|
|
3454
|
+
args: [account.address]
|
|
3455
|
+
}),
|
|
3456
|
+
publicClient.readContract({
|
|
3457
|
+
abi: erc20Abi4,
|
|
3458
|
+
address: tokenAddress,
|
|
3459
|
+
functionName: "decimals"
|
|
3460
|
+
}),
|
|
3461
|
+
publicClient.readContract({
|
|
3462
|
+
abi: erc20Abi4,
|
|
3463
|
+
address: tokenAddress,
|
|
3464
|
+
functionName: "symbol"
|
|
3465
|
+
})
|
|
3466
|
+
]);
|
|
3467
|
+
balance = results[0];
|
|
3468
|
+
decimals = results[1];
|
|
3469
|
+
symbol = results[2];
|
|
3470
|
+
}
|
|
3471
|
+
if (balance === 0n) {
|
|
3472
|
+
outputErrorAndExit(
|
|
3473
|
+
json,
|
|
3474
|
+
`No ${symbol} balance. Buy some first or pick a different wallet.`
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
let amount;
|
|
3478
|
+
if (amountMode === "amount") {
|
|
3479
|
+
const val = parsePercentageLikeValue(opts.amount);
|
|
3480
|
+
if (val === void 0 || val <= 0) {
|
|
3481
|
+
outputErrorAndExit(
|
|
3482
|
+
json,
|
|
3483
|
+
"Invalid --amount value. Must be a positive number."
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
try {
|
|
3487
|
+
amount = parseUnits3(opts.amount, decimals);
|
|
3488
|
+
} catch {
|
|
3489
|
+
outputErrorAndExit(
|
|
3490
|
+
json,
|
|
3491
|
+
"Invalid --amount value for token decimals."
|
|
3492
|
+
);
|
|
3493
|
+
}
|
|
3494
|
+
if (amount === 0n) {
|
|
3495
|
+
outputErrorAndExit(
|
|
3496
|
+
json,
|
|
3497
|
+
`Amount too small \u2014 rounds to zero at ${decimals} decimal places.`
|
|
3498
|
+
);
|
|
3499
|
+
}
|
|
3500
|
+
if (amount > balance) {
|
|
3501
|
+
outputErrorAndExit(
|
|
3502
|
+
json,
|
|
3503
|
+
`Insufficient balance. Have ${formatAmountDisplay(balance, decimals)} ${symbol}.`
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
} else if (amountMode === "all") {
|
|
3507
|
+
amount = balance;
|
|
3508
|
+
} else {
|
|
3509
|
+
const pct = parsePercentageLikeValue(opts.percent);
|
|
3510
|
+
if (pct === void 0 || pct <= 0 || pct > 100) {
|
|
3511
|
+
outputErrorAndExit(
|
|
3512
|
+
json,
|
|
3513
|
+
"Invalid --percent value. Must be between 0 and 100."
|
|
3514
|
+
);
|
|
3515
|
+
}
|
|
3516
|
+
amount = pct === 100 ? balance : balance * BigInt(Math.round(pct * 100)) / 10000n;
|
|
3517
|
+
if (amount === 0n) {
|
|
3518
|
+
outputErrorAndExit(
|
|
3519
|
+
json,
|
|
3520
|
+
"Calculated amount is zero. Balance too low."
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
const amountFormatted = formatAmountDisplay(amount, decimals);
|
|
3525
|
+
let amountUsd = null;
|
|
3526
|
+
const priceAddress = knownToken ? knownToken.priceAddress : tokenAddress;
|
|
3527
|
+
const priceUsd = knownToken?.fixedPriceUsd ?? await fetchTokenPriceUsd(priceAddress);
|
|
3528
|
+
if (priceUsd != null) {
|
|
3529
|
+
amountUsd = Number(
|
|
3530
|
+
(Number(formatUnits5(amount, decimals)) * priceUsd).toFixed(2)
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
if (!opts.yes) {
|
|
3534
|
+
printSendPreview({
|
|
3535
|
+
name: tokenName,
|
|
3536
|
+
symbol,
|
|
3537
|
+
amountFormatted,
|
|
3538
|
+
amountUsd,
|
|
3539
|
+
to: recipient
|
|
3540
|
+
});
|
|
3541
|
+
const ok = await confirm4({ message: "Confirm?", default: false });
|
|
3542
|
+
if (!ok) {
|
|
3543
|
+
console.error("Aborted.");
|
|
3544
|
+
process.exit(0);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
let txHash;
|
|
3548
|
+
try {
|
|
3549
|
+
txHash = await walletClient.writeContract({
|
|
3550
|
+
abi: erc20Abi4,
|
|
3551
|
+
address: tokenAddress,
|
|
3552
|
+
functionName: "transfer",
|
|
3553
|
+
args: [recipient, amount]
|
|
3554
|
+
});
|
|
3555
|
+
} catch (err) {
|
|
3556
|
+
track("cli_send", {
|
|
3557
|
+
asset: knownToken ? knownTokenKey : "coin",
|
|
3558
|
+
coin_address: tokenAddress,
|
|
3559
|
+
coin_name: tokenName,
|
|
3560
|
+
coin_symbol: symbol,
|
|
3561
|
+
output_format: json ? "json" : "table",
|
|
3562
|
+
success: false,
|
|
3563
|
+
error_type: err instanceof Error ? err.constructor.name : "unknown"
|
|
3564
|
+
});
|
|
3565
|
+
await shutdownAnalytics();
|
|
3566
|
+
outputErrorAndExit(
|
|
3567
|
+
json,
|
|
3568
|
+
`Transaction failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
3572
|
+
printSendResult(json, {
|
|
3573
|
+
name: tokenName,
|
|
3574
|
+
symbol,
|
|
3575
|
+
address: tokenAddress,
|
|
3576
|
+
amount,
|
|
3577
|
+
decimals,
|
|
3578
|
+
amountFormatted,
|
|
3579
|
+
amountUsd,
|
|
3580
|
+
to: recipient,
|
|
3581
|
+
txHash
|
|
3582
|
+
});
|
|
3583
|
+
track("cli_send", {
|
|
3584
|
+
asset: knownToken ? knownTokenKey : "coin",
|
|
3585
|
+
coin_address: tokenAddress,
|
|
3586
|
+
coin_name: tokenName,
|
|
3587
|
+
coin_symbol: symbol,
|
|
3588
|
+
amount_mode: amountMode,
|
|
3589
|
+
amount_usd: amountUsd,
|
|
3590
|
+
transactionHash: txHash,
|
|
3591
|
+
output_format: json ? "json" : "table",
|
|
3592
|
+
success: true,
|
|
3593
|
+
tx_hash: txHash
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
});
|
|
3597
|
+
|
|
3598
|
+
// src/commands/setup.ts
|
|
3599
|
+
import { Command as Command9 } from "commander";
|
|
3600
|
+
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
3601
|
+
|
|
3602
|
+
// src/lib/strings.ts
|
|
3603
|
+
var DEPOSIT_INSTRUCTIONS = "Deposit ETH or USDC to this address on Base to start trading.\n\n You can do this from:\n - Coinbase \u2014 withdraw directly to Base\n - Another wallet (MetaMask, Rainbow, etc.) \u2014 send on Base network\n - Bridge from other chains \u2014 use https://superbridge.app/base";
|
|
3604
|
+
var NO_WALLET_CONFIGURED = "No wallet configured.";
|
|
3605
|
+
var NO_WALLET_SUGGESTION = "Run 'zora setup' to create or import one.";
|
|
3606
|
+
var SAVE_ERROR_HINT = "Check that the directory exists and is writable.";
|
|
3607
|
+
var BACKUP_WARNING = "Back up this file \u2014 it's the only copy of your key.";
|
|
3608
|
+
|
|
3609
|
+
// src/commands/setup.ts
|
|
3610
|
+
var isValidPrivateKey = (key) => /^(0x)?[0-9a-fA-F]{64}$/.test(key);
|
|
3611
|
+
var toAccount = (json, key, errorPrefix) => {
|
|
3612
|
+
try {
|
|
3613
|
+
return privateKeyToAccount3(normalizeKey(key));
|
|
3614
|
+
} catch {
|
|
3615
|
+
outputErrorAndExit(
|
|
3616
|
+
json,
|
|
3617
|
+
`\u2717 ${errorPrefix} isn't a valid private key.`
|
|
3618
|
+
);
|
|
3619
|
+
}
|
|
3620
|
+
};
|
|
3621
|
+
var setupCommand = new Command9("setup").description("Set up your Zora wallet").option("--create", "Create a new wallet without prompting").option("--force", "Overwrite existing wallet without prompting").option("--yes", "Skip interactive prompt and execute directly").action(async function(options) {
|
|
3622
|
+
const json = getJson(this);
|
|
3623
|
+
const nonInteractive = getYes(this);
|
|
3624
|
+
const envKey = process.env.ZORA_PRIVATE_KEY;
|
|
3625
|
+
if (envKey !== void 0) {
|
|
3626
|
+
if (!isValidPrivateKey(envKey)) {
|
|
3627
|
+
outputErrorAndExit(
|
|
3628
|
+
json,
|
|
3629
|
+
"\u2717 ZORA_PRIVATE_KEY isn't a valid private key.",
|
|
3630
|
+
"Fix it and run zora setup again."
|
|
3631
|
+
);
|
|
3632
|
+
}
|
|
3633
|
+
const account = toAccount(json, envKey, "ZORA_PRIVATE_KEY");
|
|
3634
|
+
outputData(json, {
|
|
3635
|
+
json: { source: "env", address: account.address },
|
|
3636
|
+
table: () => {
|
|
3637
|
+
console.log(" Using wallet from ZORA_PRIVATE_KEY.\n");
|
|
3638
|
+
console.log(` Address: ${account.address}
|
|
3639
|
+
`);
|
|
3640
|
+
console.log(` ${DEPOSIT_INSTRUCTIONS}`);
|
|
3641
|
+
}
|
|
3642
|
+
});
|
|
3643
|
+
track("cli_setup", {
|
|
3644
|
+
action: "env_detected",
|
|
3645
|
+
source: "env",
|
|
3646
|
+
output_format: json ? "json" : "text"
|
|
3647
|
+
});
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
let existing;
|
|
3651
|
+
if (!options.force) {
|
|
3652
|
+
try {
|
|
3653
|
+
existing = getPrivateKey();
|
|
3654
|
+
} catch (err) {
|
|
3655
|
+
outputErrorAndExit(
|
|
3656
|
+
json,
|
|
3657
|
+
`\u2717 Could not read wallet: ${err.message}`,
|
|
3658
|
+
"Run 'zora setup --force' to overwrite it."
|
|
3659
|
+
);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
if (existing) {
|
|
3663
|
+
const account = toAccount(json, existing, "Stored private key");
|
|
3664
|
+
const truncated = `${account.address.slice(0, 6)}\u2026${account.address.slice(-4)}`;
|
|
3665
|
+
console.log(` Wallet already configured: ${truncated}
|
|
3666
|
+
`);
|
|
3667
|
+
if (!options.force) {
|
|
3668
|
+
outputErrorAndExit(
|
|
3669
|
+
json,
|
|
3670
|
+
"Wallet already exists.",
|
|
3671
|
+
"Use --force to overwrite."
|
|
3672
|
+
);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
let choice;
|
|
3676
|
+
if (options.create) {
|
|
3677
|
+
choice = "create";
|
|
3678
|
+
} else {
|
|
3679
|
+
choice = await selectOrDefault(
|
|
3680
|
+
{
|
|
3681
|
+
message: "How do you want to set up your wallet?",
|
|
3682
|
+
choices: [
|
|
3683
|
+
{
|
|
3684
|
+
name: "Create a new wallet (recommended)",
|
|
3685
|
+
value: "create"
|
|
3686
|
+
},
|
|
3687
|
+
{ name: "Import a private key", value: "import" }
|
|
3688
|
+
],
|
|
3689
|
+
default: "create"
|
|
3690
|
+
},
|
|
3691
|
+
nonInteractive
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3694
|
+
if (choice === "import") {
|
|
3695
|
+
let importedKey;
|
|
3696
|
+
while (!importedKey) {
|
|
3697
|
+
const input = await passwordOrFail(
|
|
3698
|
+
json,
|
|
3699
|
+
{ message: "Paste your private key:" },
|
|
3700
|
+
nonInteractive
|
|
3701
|
+
);
|
|
3702
|
+
if (isValidPrivateKey(input.trim())) {
|
|
3703
|
+
importedKey = input.trim();
|
|
3704
|
+
} else {
|
|
3705
|
+
console.error(
|
|
3706
|
+
"\u2717 Not a valid private key. Must be 64 hex characters, with or without a 0x prefix.\n"
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
const account = toAccount(json, importedKey, "Imported key");
|
|
3711
|
+
try {
|
|
3712
|
+
savePrivateKey(importedKey);
|
|
3713
|
+
} catch {
|
|
3714
|
+
outputErrorAndExit(
|
|
3715
|
+
json,
|
|
3716
|
+
`\u2717 Couldn't save to ${getWalletPath()}.`,
|
|
3717
|
+
SAVE_ERROR_HINT
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
3720
|
+
outputData(json, {
|
|
3721
|
+
json: {
|
|
3722
|
+
action: "imported",
|
|
3723
|
+
address: account.address,
|
|
3724
|
+
path: getWalletPath()
|
|
3725
|
+
},
|
|
3726
|
+
table: () => {
|
|
3727
|
+
console.log("\n\u2713 Wallet imported\n");
|
|
3728
|
+
console.log(` Address: ${account.address}`);
|
|
3729
|
+
console.log(` Private key: saved to ${getWalletPath()}
|
|
3730
|
+
`);
|
|
3731
|
+
console.log(` ${BACKUP_WARNING}
|
|
3732
|
+
`);
|
|
3733
|
+
console.log(` ${DEPOSIT_INSTRUCTIONS}`);
|
|
3734
|
+
}
|
|
3735
|
+
});
|
|
3736
|
+
track("cli_setup", {
|
|
3737
|
+
action: "imported",
|
|
3738
|
+
source: "file",
|
|
3739
|
+
output_format: json ? "json" : "text"
|
|
3740
|
+
});
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
if (choice === "create") {
|
|
3744
|
+
const privateKey = generatePrivateKey();
|
|
3745
|
+
const account = toAccount(json, privateKey, "Generated key");
|
|
3746
|
+
try {
|
|
3747
|
+
savePrivateKey(privateKey);
|
|
3748
|
+
} catch {
|
|
3749
|
+
outputErrorAndExit(
|
|
3750
|
+
json,
|
|
3751
|
+
`\u2717 Couldn't save to ${getWalletPath()}.`,
|
|
3752
|
+
SAVE_ERROR_HINT
|
|
3753
|
+
);
|
|
3754
|
+
}
|
|
3755
|
+
outputData(json, {
|
|
3756
|
+
json: {
|
|
3757
|
+
action: "created",
|
|
3758
|
+
address: account.address,
|
|
3759
|
+
path: getWalletPath()
|
|
3760
|
+
},
|
|
3761
|
+
table: () => {
|
|
3762
|
+
console.log("\n\u2713 Wallet created\n");
|
|
3763
|
+
console.log(` Address: ${account.address}`);
|
|
3764
|
+
console.log(` Private key: saved to ${getWalletPath()}
|
|
3765
|
+
`);
|
|
3766
|
+
console.log(` ${BACKUP_WARNING}
|
|
3767
|
+
`);
|
|
3768
|
+
console.log(` ${DEPOSIT_INSTRUCTIONS}`);
|
|
3769
|
+
}
|
|
3770
|
+
});
|
|
3771
|
+
track("cli_setup", {
|
|
3772
|
+
action: "created",
|
|
3773
|
+
source: "file",
|
|
3774
|
+
output_format: json ? "json" : "text"
|
|
3775
|
+
});
|
|
3776
|
+
}
|
|
3777
|
+
});
|
|
3778
|
+
|
|
3779
|
+
// src/commands/wallet.ts
|
|
3780
|
+
import { Command as Command10 } from "commander";
|
|
3781
|
+
import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
|
|
3782
|
+
var resolvePrivateKey = () => {
|
|
3783
|
+
const envKey = process.env.ZORA_PRIVATE_KEY;
|
|
3784
|
+
if (envKey) {
|
|
3785
|
+
return { key: envKey, source: "env" };
|
|
3786
|
+
}
|
|
3787
|
+
const fileKey = getPrivateKey();
|
|
3788
|
+
if (fileKey !== void 0) {
|
|
3789
|
+
return { key: fileKey, source: "file" };
|
|
3790
|
+
}
|
|
3791
|
+
return void 0;
|
|
3792
|
+
};
|
|
3793
|
+
var walletCommand = new Command10("wallet").description(
|
|
3794
|
+
"Manage your Zora wallet"
|
|
3795
|
+
);
|
|
3796
|
+
walletCommand.command("info").description("Show wallet address and storage location").action(function() {
|
|
3797
|
+
const json = getJson(this);
|
|
3798
|
+
const resolved = resolvePrivateKey();
|
|
3799
|
+
if (!resolved) {
|
|
3800
|
+
outputErrorAndExit(json, NO_WALLET_CONFIGURED, NO_WALLET_SUGGESTION);
|
|
3801
|
+
}
|
|
3802
|
+
let account;
|
|
3803
|
+
try {
|
|
3804
|
+
account = privateKeyToAccount4(normalizeKey(resolved.key));
|
|
3805
|
+
} catch {
|
|
3806
|
+
const msg = resolved.source === "env" ? "ZORA_PRIVATE_KEY is not a valid private key." : "Stored private key is invalid.";
|
|
3807
|
+
const suggestion = resolved.source === "env" ? void 0 : "Run 'zora setup --force' to replace it.";
|
|
3808
|
+
outputErrorAndExit(json, `\u2717 ${msg}`, suggestion);
|
|
3809
|
+
}
|
|
3810
|
+
const source = resolved.source === "env" ? "env (ZORA_PRIVATE_KEY)" : getWalletPath();
|
|
3811
|
+
outputData(json, {
|
|
3812
|
+
json: { address: account.address, source },
|
|
3813
|
+
table: () => {
|
|
3814
|
+
console.log(` Address: ${account.address}`);
|
|
3815
|
+
console.log(` Source: ${source}`);
|
|
3816
|
+
}
|
|
3817
|
+
});
|
|
3818
|
+
track("cli_wallet_info", {
|
|
3819
|
+
source: resolved.source,
|
|
3820
|
+
output_format: json ? "json" : "text"
|
|
3821
|
+
});
|
|
3822
|
+
});
|
|
3823
|
+
walletCommand.command("export").description("Print the raw private key to stdout").option("--force", "Skip the confirmation prompt").option("--yes", "Skip interactive prompt and execute directly").action(async function(options) {
|
|
3824
|
+
const json = getJson(this);
|
|
3825
|
+
const nonInteractive = getYes(this);
|
|
3826
|
+
const resolved = resolvePrivateKey();
|
|
3827
|
+
if (!resolved) {
|
|
3828
|
+
outputErrorAndExit(json, NO_WALLET_CONFIGURED, NO_WALLET_SUGGESTION);
|
|
3829
|
+
}
|
|
3830
|
+
if (!options.force) {
|
|
3831
|
+
console.log(
|
|
3832
|
+
" \u26A0 Your private key grants full access to your wallet."
|
|
3833
|
+
);
|
|
3834
|
+
console.log(
|
|
3835
|
+
" Anyone who sees it can steal your funds. Never share it.\n"
|
|
3836
|
+
);
|
|
3837
|
+
const ok = await confirmOrDefault(
|
|
3838
|
+
{ message: "Export private key?", default: false },
|
|
3839
|
+
nonInteractive
|
|
3840
|
+
);
|
|
3841
|
+
if (!ok) {
|
|
3842
|
+
console.error("Aborted.");
|
|
3843
|
+
process.exit(0);
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
console.log(resolved.key);
|
|
3847
|
+
track("cli_wallet_export", {
|
|
3848
|
+
output_format: json ? "json" : "text"
|
|
3849
|
+
});
|
|
3850
|
+
});
|
|
3851
|
+
|
|
3852
|
+
// src/components/Zorb.tsx
|
|
3853
|
+
import { Text as Text7, Box as Box7 } from "ink";
|
|
3854
|
+
|
|
3855
|
+
// src/lib/zorb-pixels.ts
|
|
3856
|
+
function supportsTruecolor() {
|
|
3857
|
+
if (!process.stdout.isTTY) return false;
|
|
3858
|
+
const ct = process.env.COLORTERM;
|
|
3859
|
+
if (ct === "truecolor" || ct === "24bit") return true;
|
|
3860
|
+
if (typeof process.stdout.getColorDepth === "function") {
|
|
3861
|
+
return process.stdout.getColorDepth() >= 24;
|
|
3862
|
+
}
|
|
3863
|
+
return false;
|
|
3864
|
+
}
|
|
3865
|
+
function hexToRgb(hex) {
|
|
3866
|
+
const n = parseInt(hex.replace("#", ""), 16);
|
|
3867
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
3868
|
+
}
|
|
3869
|
+
function lerp(a, b, t) {
|
|
3870
|
+
return a + (b - a) * t;
|
|
3871
|
+
}
|
|
3872
|
+
function clamp(v, min, max) {
|
|
3873
|
+
return v < min ? min : v > max ? max : v;
|
|
3874
|
+
}
|
|
3875
|
+
function alphaOver(bg, fg, a) {
|
|
3876
|
+
return [lerp(bg[0], fg[0], a), lerp(bg[1], fg[1], a), lerp(bg[2], fg[2], a)];
|
|
3877
|
+
}
|
|
3878
|
+
function gaussian(dist, sigma) {
|
|
3879
|
+
if (sigma <= 0) return dist <= 0 ? 1 : 0;
|
|
3880
|
+
return Math.exp(-(dist * dist) / (2 * sigma * sigma));
|
|
3881
|
+
}
|
|
3882
|
+
var BASE_COLOR = hexToRgb("#A1723A");
|
|
3883
|
+
var LAYERS = [
|
|
3884
|
+
// 1: Dark maroon shadow
|
|
3885
|
+
{
|
|
3886
|
+
cx: 0.54,
|
|
3887
|
+
cy: 0.45,
|
|
3888
|
+
radius: 0.53,
|
|
3889
|
+
color: hexToRgb("#531002"),
|
|
3890
|
+
blur: 0.062,
|
|
3891
|
+
opacity: 1
|
|
3892
|
+
},
|
|
3893
|
+
// 2: Blue body
|
|
3894
|
+
{
|
|
3895
|
+
cx: 0.6,
|
|
3896
|
+
cy: 0.38,
|
|
3897
|
+
radius: 0.43,
|
|
3898
|
+
color: hexToRgb("#2B5DF0"),
|
|
3899
|
+
blur: 0.124,
|
|
3900
|
+
opacity: 1
|
|
3901
|
+
},
|
|
3902
|
+
// 3: Blue accent (gradient from center color to transparent)
|
|
3903
|
+
{
|
|
3904
|
+
cx: 0.59,
|
|
3905
|
+
cy: 0.38,
|
|
3906
|
+
radius: 0.45,
|
|
3907
|
+
color: hexToRgb("#387AFA"),
|
|
3908
|
+
blur: 0.046,
|
|
3909
|
+
opacity: 1,
|
|
3910
|
+
gradient: { gcx: 0.66, gcy: 0.26 }
|
|
3911
|
+
},
|
|
3912
|
+
// 4: Pink glow
|
|
3913
|
+
{
|
|
3914
|
+
cx: 0.66,
|
|
3915
|
+
cy: 0.27,
|
|
3916
|
+
radius: 0.23,
|
|
3917
|
+
color: hexToRgb("#FCB8D4"),
|
|
3918
|
+
blur: 0.093,
|
|
3919
|
+
opacity: 1
|
|
3920
|
+
},
|
|
3921
|
+
// 5: White specular
|
|
3922
|
+
{
|
|
3923
|
+
cx: 0.66,
|
|
3924
|
+
cy: 0.27,
|
|
3925
|
+
radius: 0.09,
|
|
3926
|
+
color: hexToRgb("#FFFFFF"),
|
|
3927
|
+
blur: 0.062,
|
|
3928
|
+
opacity: 1
|
|
3929
|
+
},
|
|
3930
|
+
// 6: Dark ring (transparent → black → transparent, opacity 0.9)
|
|
3931
|
+
{
|
|
3932
|
+
cx: 0.6,
|
|
3933
|
+
cy: 0.36,
|
|
3934
|
+
radius: 0.82,
|
|
3935
|
+
color: [0, 0, 0],
|
|
3936
|
+
blur: 0.046,
|
|
3937
|
+
opacity: 0.9,
|
|
3938
|
+
ring: true
|
|
3939
|
+
}
|
|
3940
|
+
];
|
|
3941
|
+
function computeLayerAlpha(nx, ny, layer) {
|
|
3942
|
+
const dx = nx - layer.cx;
|
|
3943
|
+
const dy = ny - layer.cy;
|
|
3944
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
3945
|
+
const radialFalloff = gaussian(Math.max(0, dist - layer.radius), layer.blur);
|
|
3946
|
+
if (layer.ring) {
|
|
3947
|
+
const normalizedDist = dist / layer.radius;
|
|
3948
|
+
const ringProfile = gaussian(normalizedDist - 0.7, 0.15) * radialFalloff;
|
|
3949
|
+
return { alpha: ringProfile * layer.opacity, color: layer.color };
|
|
3950
|
+
}
|
|
3951
|
+
if (layer.gradient) {
|
|
3952
|
+
const gdx = nx - layer.gradient.gcx;
|
|
3953
|
+
const gdy = ny - layer.gradient.gcy;
|
|
3954
|
+
const gDist = Math.sqrt(gdx * gdx + gdy * gdy);
|
|
3955
|
+
const gradientT = clamp(gDist / (layer.radius * 1.2), 0, 1);
|
|
3956
|
+
const alpha = radialFalloff * (1 - gradientT);
|
|
3957
|
+
return { alpha: alpha * layer.opacity, color: layer.color };
|
|
3958
|
+
}
|
|
3959
|
+
return { alpha: radialFalloff * layer.opacity, color: layer.color };
|
|
3960
|
+
}
|
|
3961
|
+
function circleAlpha(px, py, size) {
|
|
3962
|
+
const cx = (size - 1) / 2;
|
|
3963
|
+
const cy = (size - 1) / 2;
|
|
3964
|
+
const r = size / 2;
|
|
3965
|
+
const dx = px - cx;
|
|
3966
|
+
const dy = py - cy;
|
|
3967
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
3968
|
+
return clamp(r - dist + 0.5, 0, 1);
|
|
3969
|
+
}
|
|
3970
|
+
function generateZorbPixels(size) {
|
|
3971
|
+
const grid = [];
|
|
3972
|
+
for (let y = 0; y < size; y++) {
|
|
3973
|
+
const row = [];
|
|
3974
|
+
for (let x = 0; x < size; x++) {
|
|
3975
|
+
const nx = x / (size - 1);
|
|
3976
|
+
const ny = y / (size - 1);
|
|
3977
|
+
let pixel = [...BASE_COLOR];
|
|
3978
|
+
for (const layer of LAYERS) {
|
|
3979
|
+
const { alpha, color } = computeLayerAlpha(nx, ny, layer);
|
|
3980
|
+
if (alpha > 1e-3) {
|
|
3981
|
+
pixel = alphaOver(pixel, color, alpha);
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
const ca = circleAlpha(x, y, size);
|
|
3985
|
+
pixel = [
|
|
3986
|
+
Math.round(pixel[0] * ca),
|
|
3987
|
+
Math.round(pixel[1] * ca),
|
|
3988
|
+
Math.round(pixel[2] * ca)
|
|
3989
|
+
];
|
|
3990
|
+
row.push(pixel);
|
|
3991
|
+
}
|
|
3992
|
+
grid.push(row);
|
|
3993
|
+
}
|
|
3994
|
+
return grid;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
// src/components/Zorb.tsx
|
|
3998
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3999
|
+
var LOWER_HALF_BLOCK = "\u2584";
|
|
4000
|
+
var UPPER_HALF_BLOCK = "\u2580";
|
|
4001
|
+
function rgbString([r, g, b]) {
|
|
4002
|
+
return `rgb(${r},${g},${b})`;
|
|
4003
|
+
}
|
|
4004
|
+
function isBlack([r, g, b]) {
|
|
4005
|
+
return r === 0 && g === 0 && b === 0;
|
|
4006
|
+
}
|
|
4007
|
+
function Zorb({ size = 20 }) {
|
|
4008
|
+
if (!supportsTruecolor()) return null;
|
|
4009
|
+
const grid = generateZorbPixels(size);
|
|
4010
|
+
const rows = [];
|
|
4011
|
+
for (let y = 0; y < size; y += 2) {
|
|
4012
|
+
const topRow = grid[y];
|
|
4013
|
+
const bottomRow = y + 1 < size ? grid[y + 1] : void 0;
|
|
4014
|
+
const cells = [];
|
|
4015
|
+
for (let x = 0; x < size; x++) {
|
|
4016
|
+
const top = topRow[x];
|
|
4017
|
+
const bottom = bottomRow ? bottomRow[x] : [0, 0, 0];
|
|
4018
|
+
const topIsBlack = isBlack(top);
|
|
4019
|
+
const bottomIsBlack = isBlack(bottom);
|
|
4020
|
+
if (topIsBlack && bottomIsBlack) {
|
|
4021
|
+
cells.push(/* @__PURE__ */ jsx10(Text7, { children: " " }, x));
|
|
4022
|
+
} else if (topIsBlack) {
|
|
4023
|
+
cells.push(
|
|
4024
|
+
/* @__PURE__ */ jsx10(Text7, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
|
|
4025
|
+
);
|
|
4026
|
+
} else if (bottomIsBlack) {
|
|
4027
|
+
cells.push(
|
|
4028
|
+
/* @__PURE__ */ jsx10(Text7, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
|
|
4029
|
+
);
|
|
4030
|
+
} else {
|
|
4031
|
+
cells.push(
|
|
4032
|
+
/* @__PURE__ */ jsx10(
|
|
4033
|
+
Text7,
|
|
4034
|
+
{
|
|
4035
|
+
backgroundColor: rgbString(top),
|
|
4036
|
+
color: rgbString(bottom),
|
|
4037
|
+
children: LOWER_HALF_BLOCK
|
|
4038
|
+
},
|
|
4039
|
+
x
|
|
4040
|
+
)
|
|
4041
|
+
);
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
rows.push(/* @__PURE__ */ jsx10(Text7, { children: cells }, y));
|
|
4045
|
+
}
|
|
4046
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
4047
|
+
/* @__PURE__ */ jsx10(Text7, { children: " " }),
|
|
4048
|
+
rows,
|
|
4049
|
+
/* @__PURE__ */ jsx10(Text7, { children: " " })
|
|
4050
|
+
] });
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
// src/index.tsx
|
|
4054
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
4055
|
+
if (process.env.ZORA_API_TARGET) {
|
|
4056
|
+
setApiBaseUrl(process.env.ZORA_API_TARGET);
|
|
4057
|
+
}
|
|
4058
|
+
var version = true ? "0.2.4" : JSON.parse(
|
|
4059
|
+
readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
|
|
4060
|
+
).version;
|
|
4061
|
+
var buildProgram = () => {
|
|
4062
|
+
const program2 = new Command11().name("zora").description("Zora CLI").version(version).option(
|
|
4063
|
+
"--output <format>",
|
|
4064
|
+
"Output format: table, json, live (default varies by command)"
|
|
4065
|
+
).option(
|
|
4066
|
+
"--interval <seconds>",
|
|
4067
|
+
"Auto-refresh interval in seconds (min 5)",
|
|
4068
|
+
"30"
|
|
4069
|
+
);
|
|
4070
|
+
program2.addCommand(authCommand);
|
|
4071
|
+
program2.addCommand(balanceCommand);
|
|
4072
|
+
program2.addCommand(buyCommand);
|
|
4073
|
+
program2.addCommand(exploreCommand);
|
|
4074
|
+
program2.addCommand(getCommand);
|
|
4075
|
+
program2.addCommand(priceHistoryCommand);
|
|
4076
|
+
program2.addCommand(setupCommand);
|
|
4077
|
+
program2.addCommand(walletCommand);
|
|
4078
|
+
program2.addCommand(sellCommand);
|
|
4079
|
+
program2.addCommand(sendCommand);
|
|
4080
|
+
return program2;
|
|
4081
|
+
};
|
|
4082
|
+
var program = buildProgram();
|
|
4083
|
+
if (!process.env.VITEST) {
|
|
4084
|
+
const showingHelp = process.argv.length <= 2 || process.argv.includes("--help") || process.argv.includes("-h");
|
|
4085
|
+
if (showingHelp && !process.argv.includes("--output") && supportsTruecolor()) {
|
|
4086
|
+
renderOnce(/* @__PURE__ */ jsx11(Zorb, { size: 20 }));
|
|
4087
|
+
}
|
|
4088
|
+
console.warn(
|
|
4089
|
+
"\x1B[33m\u26A0 Beta:\x1B[0m This CLI is in beta and should be used with caution."
|
|
4090
|
+
);
|
|
4091
|
+
identify();
|
|
4092
|
+
try {
|
|
4093
|
+
await program.parseAsync();
|
|
4094
|
+
} catch (err) {
|
|
4095
|
+
if (err instanceof ExitPromptError) {
|
|
4096
|
+
console.log("\nAborted.");
|
|
4097
|
+
process.exit(0);
|
|
4098
|
+
}
|
|
4099
|
+
throw err;
|
|
4100
|
+
} finally {
|
|
4101
|
+
await shutdownAnalytics();
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
export {
|
|
4105
|
+
buildProgram
|
|
4106
|
+
};
|