@zoralabs/cli 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/index.js +2936 -9
- package/package.json +47 -29
- package/README.md +0 -55
- package/bin/zora +0 -2
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -13
- package/dist/cli.js.map +0 -1
- package/dist/commands/default.d.ts +0 -2
- package/dist/commands/default.js +0 -10
- package/dist/commands/default.js.map +0 -1
- package/dist/commands/mint/default.d.ts +0 -2
- package/dist/commands/mint/default.js +0 -6
- package/dist/commands/mint/default.js.map +0 -1
- package/dist/commands/mint/file.d.ts +0 -4
- package/dist/commands/mint/file.js +0 -22
- package/dist/commands/mint/file.js.map +0 -1
- package/dist/commands/mint/uri.d.ts +0 -15
- package/dist/commands/mint/uri.js +0 -132
- package/dist/commands/mint/uri.js.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,2936 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import { Command as Command9 } 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 getJson = (cmd) => cmd.optsWithGlobals().json;
|
|
159
|
+
var getYes = (cmd) => cmd.optsWithGlobals().yes ?? false;
|
|
160
|
+
var outputJson = (data) => {
|
|
161
|
+
console.log(JSON.stringify(data, null, 2));
|
|
162
|
+
};
|
|
163
|
+
var outputErrorAndExit = (json, message, suggestion) => {
|
|
164
|
+
if (json) {
|
|
165
|
+
const payload = { error: message };
|
|
166
|
+
if (suggestion) payload.suggestion = suggestion;
|
|
167
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
168
|
+
} else {
|
|
169
|
+
console.error(`\x1B[31mError:\x1B[0m ${message}`);
|
|
170
|
+
if (suggestion) {
|
|
171
|
+
console.error(`\x1B[2m${suggestion}\x1B[0m`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
process.exit(1);
|
|
175
|
+
};
|
|
176
|
+
var outputData = (json, opts) => {
|
|
177
|
+
if (json) {
|
|
178
|
+
outputJson(opts.json);
|
|
179
|
+
} else {
|
|
180
|
+
opts.table();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/lib/prompt.ts
|
|
185
|
+
import confirm from "@inquirer/confirm";
|
|
186
|
+
import select from "@inquirer/select";
|
|
187
|
+
import password from "@inquirer/password";
|
|
188
|
+
var confirmOrDefault = async (opts, nonInteractive) => {
|
|
189
|
+
if (nonInteractive) return true;
|
|
190
|
+
return confirm(opts);
|
|
191
|
+
};
|
|
192
|
+
var selectOrDefault = async (opts, nonInteractive) => {
|
|
193
|
+
if (nonInteractive) return opts.default;
|
|
194
|
+
return select(opts);
|
|
195
|
+
};
|
|
196
|
+
var passwordOrFail = async (json, opts, nonInteractive) => {
|
|
197
|
+
if (nonInteractive) {
|
|
198
|
+
outputErrorAndExit(
|
|
199
|
+
json,
|
|
200
|
+
"This command requires interactive input. Remove --yes to proceed."
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return password(opts);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// src/lib/analytics.ts
|
|
207
|
+
import { PostHog } from "posthog-node";
|
|
208
|
+
import { createHash, randomUUID } from "crypto";
|
|
209
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
210
|
+
|
|
211
|
+
// src/lib/constants.ts
|
|
212
|
+
var BASE_CHAIN_ID = 8453;
|
|
213
|
+
var WETH_ADDRESS = "0x4200000000000000000000000000000000000006";
|
|
214
|
+
var USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
215
|
+
var ZORA_ADDRESS = "0x1111111111166b7FE7bd91427724B487980aFc69";
|
|
216
|
+
var USDC_DECIMALS = 6;
|
|
217
|
+
var BASE_TRADE_TOKENS = {
|
|
218
|
+
eth: {
|
|
219
|
+
symbol: "ETH",
|
|
220
|
+
decimals: 18,
|
|
221
|
+
trade: { type: "eth" },
|
|
222
|
+
priceAddress: WETH_ADDRESS,
|
|
223
|
+
fixedPriceUsd: void 0
|
|
224
|
+
},
|
|
225
|
+
usdc: {
|
|
226
|
+
symbol: "USDC",
|
|
227
|
+
decimals: USDC_DECIMALS,
|
|
228
|
+
trade: {
|
|
229
|
+
type: "erc20",
|
|
230
|
+
address: USDC_ADDRESS
|
|
231
|
+
},
|
|
232
|
+
priceAddress: USDC_ADDRESS,
|
|
233
|
+
fixedPriceUsd: 1
|
|
234
|
+
},
|
|
235
|
+
zora: {
|
|
236
|
+
symbol: "ZORA",
|
|
237
|
+
decimals: 18,
|
|
238
|
+
trade: {
|
|
239
|
+
type: "erc20",
|
|
240
|
+
address: ZORA_ADDRESS
|
|
241
|
+
},
|
|
242
|
+
priceAddress: ZORA_ADDRESS,
|
|
243
|
+
fixedPriceUsd: void 0
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
var POSTHOG_TOKEN = "phc_F3nLidy5mjn4xWQ6PYujO96MVig7UoszINhUUY0usOx";
|
|
247
|
+
var POSTHOG_HOST = "https://us.i.posthog.com";
|
|
248
|
+
|
|
249
|
+
// src/lib/wallet.ts
|
|
250
|
+
import { apiPost } from "@zoralabs/coins-sdk";
|
|
251
|
+
import { createPublicClient, createWalletClient, custom } from "viem";
|
|
252
|
+
import { base } from "viem/chains";
|
|
253
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
254
|
+
var normalizeKey = (key) => key.startsWith("0x") ? key : `0x${key}`;
|
|
255
|
+
var resolveAccount = (json = false) => {
|
|
256
|
+
const envKey = process.env.ZORA_PRIVATE_KEY;
|
|
257
|
+
const key = envKey || getPrivateKey();
|
|
258
|
+
if (!key) {
|
|
259
|
+
console.error(
|
|
260
|
+
"No wallet configured. Run 'zora setup' to create or import one."
|
|
261
|
+
);
|
|
262
|
+
return process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
return privateKeyToAccount(normalizeKey(key));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(
|
|
268
|
+
`\u2717 Invalid private key: ${err instanceof Error ? err.message : String(err)}`
|
|
269
|
+
);
|
|
270
|
+
console.error(" Run 'zora setup --force' to replace it.");
|
|
271
|
+
return process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
function formatRpcError(error) {
|
|
275
|
+
if (typeof error === "string") return error;
|
|
276
|
+
if (error instanceof Error) return error.message;
|
|
277
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
278
|
+
const message = error.message;
|
|
279
|
+
if (typeof message === "string") return message;
|
|
280
|
+
}
|
|
281
|
+
return JSON.stringify(error);
|
|
282
|
+
}
|
|
283
|
+
function createCliRpcTransport(chainId = base.id) {
|
|
284
|
+
return custom({
|
|
285
|
+
async request({
|
|
286
|
+
method,
|
|
287
|
+
params
|
|
288
|
+
}) {
|
|
289
|
+
let response;
|
|
290
|
+
try {
|
|
291
|
+
response = await apiPost("/cli-rpc", {
|
|
292
|
+
chainId,
|
|
293
|
+
method,
|
|
294
|
+
params: params ?? []
|
|
295
|
+
});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
throw new Error(`CLI RPC request failed: ${formatRpcError(err)}`);
|
|
298
|
+
}
|
|
299
|
+
if (response.error) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`CLI RPC request failed: ${formatRpcError(response.error)}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const payload = response.data;
|
|
305
|
+
if (payload && typeof payload === "object" && "error" in payload && payload.error) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`CLI RPC request failed: ${formatRpcError(payload.error)}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (payload && typeof payload === "object" && "result" in payload) {
|
|
311
|
+
return payload.result;
|
|
312
|
+
}
|
|
313
|
+
return payload;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function createClients(account) {
|
|
318
|
+
const transport = createCliRpcTransport();
|
|
319
|
+
const publicClient = createPublicClient({
|
|
320
|
+
chain: base,
|
|
321
|
+
transport
|
|
322
|
+
});
|
|
323
|
+
const walletClient = createWalletClient({
|
|
324
|
+
chain: base,
|
|
325
|
+
transport,
|
|
326
|
+
account
|
|
327
|
+
});
|
|
328
|
+
return { publicClient, walletClient };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/lib/analytics.ts
|
|
332
|
+
var SHUTDOWN_TIMEOUT_MS = 2e3;
|
|
333
|
+
var client = null;
|
|
334
|
+
var distinctId = null;
|
|
335
|
+
var isDisabled = () => process.env.ZORA_NO_ANALYTICS === "1" || process.env.DO_NOT_TRACK === "1" || process.env.CI !== void 0 || process.env.NODE_ENV === "test";
|
|
336
|
+
var getOrCreateDistinctId = () => {
|
|
337
|
+
if (distinctId) return distinctId;
|
|
338
|
+
try {
|
|
339
|
+
const stored = getAnalyticsId();
|
|
340
|
+
if (stored) {
|
|
341
|
+
distinctId = stored;
|
|
342
|
+
return distinctId;
|
|
343
|
+
}
|
|
344
|
+
distinctId = randomUUID();
|
|
345
|
+
saveAnalyticsId(distinctId);
|
|
346
|
+
return distinctId;
|
|
347
|
+
} catch {
|
|
348
|
+
distinctId = randomUUID();
|
|
349
|
+
return distinctId;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
var getClient = () => {
|
|
353
|
+
if (!client) {
|
|
354
|
+
client = new PostHog(POSTHOG_TOKEN, { host: POSTHOG_HOST });
|
|
355
|
+
}
|
|
356
|
+
return client;
|
|
357
|
+
};
|
|
358
|
+
var commonProperties = () => ({
|
|
359
|
+
cli_version: true ? "0.2.3" : "development",
|
|
360
|
+
os: process.platform,
|
|
361
|
+
arch: process.arch,
|
|
362
|
+
node_version: process.version
|
|
363
|
+
});
|
|
364
|
+
var hashApiKey = (key) => createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
365
|
+
var getWalletAddress = () => {
|
|
366
|
+
try {
|
|
367
|
+
const key = process.env.ZORA_PRIVATE_KEY || getPrivateKey();
|
|
368
|
+
if (!key) return void 0;
|
|
369
|
+
return privateKeyToAccount2(normalizeKey(key)).address;
|
|
370
|
+
} catch {
|
|
371
|
+
return void 0;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
var identified = false;
|
|
375
|
+
var identify = () => {
|
|
376
|
+
try {
|
|
377
|
+
if (isDisabled() || identified) return;
|
|
378
|
+
identified = true;
|
|
379
|
+
const id = getOrCreateDistinctId();
|
|
380
|
+
const apiKey = getApiKey();
|
|
381
|
+
const walletAddress = getWalletAddress();
|
|
382
|
+
if (!apiKey && !walletAddress) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
getClient().identify({
|
|
386
|
+
distinctId: id,
|
|
387
|
+
properties: {
|
|
388
|
+
api_key_hash: apiKey ? hashApiKey(apiKey) : void 0,
|
|
389
|
+
wallet_address: walletAddress ?? void 0
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
var track = (event, properties) => {
|
|
396
|
+
try {
|
|
397
|
+
if (isDisabled()) return;
|
|
398
|
+
getClient().capture({
|
|
399
|
+
distinctId: getOrCreateDistinctId(),
|
|
400
|
+
event,
|
|
401
|
+
properties: { ...commonProperties(), ...properties }
|
|
402
|
+
});
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
var shutdownAnalytics = async () => {
|
|
407
|
+
if (!client) return;
|
|
408
|
+
const flushing = client;
|
|
409
|
+
client = null;
|
|
410
|
+
try {
|
|
411
|
+
await Promise.race([
|
|
412
|
+
flushing.shutdown(),
|
|
413
|
+
new Promise((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS))
|
|
414
|
+
]);
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// src/commands/auth.ts
|
|
420
|
+
var authCommand = new Command("auth").description(
|
|
421
|
+
"Manage API key authentication.\nAPI key is optional \u2014 without one, requests are rate-limited.\nGet a key at https://zora.co/settings/developer"
|
|
422
|
+
);
|
|
423
|
+
authCommand.command("configure").description("Set your Zora API key").option("--yes", "Skip interactive prompt and execute directly").action(async function() {
|
|
424
|
+
const json = getJson(this);
|
|
425
|
+
const nonInteractive = getYes(this);
|
|
426
|
+
if (getEnvApiKey()) {
|
|
427
|
+
outputData(json, {
|
|
428
|
+
json: {
|
|
429
|
+
status: "env_override",
|
|
430
|
+
message: "API key is set via ZORA_API_KEY environment variable."
|
|
431
|
+
},
|
|
432
|
+
table: () => console.log(
|
|
433
|
+
"API key is set via ZORA_API_KEY environment variable. Unset it to configure manually."
|
|
434
|
+
)
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const existing = getApiKey();
|
|
439
|
+
if (existing) {
|
|
440
|
+
console.log(`Current key: ${maskKey(existing)}`);
|
|
441
|
+
}
|
|
442
|
+
console.log("Get your API key from: https://zora.co/settings/developer\n");
|
|
443
|
+
const apiKey = await passwordOrFail(
|
|
444
|
+
json,
|
|
445
|
+
{ message: "Paste your API key:" },
|
|
446
|
+
nonInteractive
|
|
447
|
+
);
|
|
448
|
+
const trimmed = apiKey.trim();
|
|
449
|
+
if (!trimmed) {
|
|
450
|
+
outputErrorAndExit(
|
|
451
|
+
json,
|
|
452
|
+
"No API key provided.",
|
|
453
|
+
"Usage: zora auth configure"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
saveApiKey(trimmed);
|
|
458
|
+
outputData(json, {
|
|
459
|
+
json: { saved: true, path: getConfigPath() },
|
|
460
|
+
table: () => console.log(`API key saved to ${getConfigPath()}`)
|
|
461
|
+
});
|
|
462
|
+
track("cli_auth_configure", {
|
|
463
|
+
output_format: json ? "json" : "text"
|
|
464
|
+
});
|
|
465
|
+
} catch (err) {
|
|
466
|
+
outputErrorAndExit(
|
|
467
|
+
json,
|
|
468
|
+
`Failed to save API key: ${err.message}`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
authCommand.command("status").description("Check authentication status").action(function() {
|
|
473
|
+
const json = getJson(this);
|
|
474
|
+
const apiKey = getApiKey();
|
|
475
|
+
if (!apiKey) {
|
|
476
|
+
outputData(json, {
|
|
477
|
+
json: { authenticated: false },
|
|
478
|
+
table: () => {
|
|
479
|
+
console.log(
|
|
480
|
+
"No API key configured. The CLI works without one, but requests are rate-limited."
|
|
481
|
+
);
|
|
482
|
+
console.log(
|
|
483
|
+
"Run 'zora auth configure' to set an API key for higher rate limits."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
track("cli_auth_status", {
|
|
488
|
+
authenticated: false,
|
|
489
|
+
source: null,
|
|
490
|
+
output_format: json ? "json" : "text"
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const source = getEnvApiKey() ? "env (ZORA_API_KEY)" : getConfigPath();
|
|
495
|
+
outputData(json, {
|
|
496
|
+
json: { authenticated: true, key: maskKey(apiKey), source },
|
|
497
|
+
table: () => {
|
|
498
|
+
console.log(`Authenticated: ${maskKey(apiKey)}`);
|
|
499
|
+
console.log(`Source: ${source}`);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
track("cli_auth_status", {
|
|
503
|
+
authenticated: true,
|
|
504
|
+
source: getEnvApiKey() ? "env" : "file",
|
|
505
|
+
output_format: json ? "json" : "text"
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// src/commands/balance.ts
|
|
510
|
+
import { Command as Command3 } from "commander";
|
|
511
|
+
import { getProfileBalances, setApiKey as setApiKey2 } from "@zoralabs/coins-sdk";
|
|
512
|
+
|
|
513
|
+
// src/lib/render.tsx
|
|
514
|
+
import { renderToString } from "ink";
|
|
515
|
+
var renderOnce = (element) => {
|
|
516
|
+
const output = renderToString(element);
|
|
517
|
+
console.log(output);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/components/table.tsx
|
|
521
|
+
import { Box, Text } from "ink";
|
|
522
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
523
|
+
var truncate = (str, max) => {
|
|
524
|
+
if (str.length <= max) return str;
|
|
525
|
+
return str.slice(0, max - 1) + "\u2026";
|
|
526
|
+
};
|
|
527
|
+
var TableComponent = ({
|
|
528
|
+
columns,
|
|
529
|
+
data,
|
|
530
|
+
title,
|
|
531
|
+
subtitle
|
|
532
|
+
}) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1, children: [
|
|
533
|
+
title && /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, marginBottom: 1, children: [
|
|
534
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: title }),
|
|
535
|
+
subtitle && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
536
|
+
" ",
|
|
537
|
+
subtitle
|
|
538
|
+
] })
|
|
539
|
+
] }),
|
|
540
|
+
/* @__PURE__ */ jsx(Box, { paddingLeft: 1, children: columns.map((col) => /* @__PURE__ */ jsx(Box, { width: col.width, children: /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: col.header }) }, col.header)) }),
|
|
541
|
+
data.map((row, i) => /* @__PURE__ */ jsx(Box, { paddingLeft: 1, children: columns.map((col) => {
|
|
542
|
+
const value = col.noTruncate ? col.accessor(row) : truncate(col.accessor(row), col.width - 2);
|
|
543
|
+
const colorName = col.color?.(row);
|
|
544
|
+
return /* @__PURE__ */ jsx(Box, { width: col.width, children: /* @__PURE__ */ jsx(Text, { color: colorName, children: value }) }, col.header);
|
|
545
|
+
}) }, i))
|
|
546
|
+
] });
|
|
547
|
+
|
|
548
|
+
// src/commands/explore.tsx
|
|
549
|
+
import { Command as Command2 } from "commander";
|
|
550
|
+
import {
|
|
551
|
+
setApiKey,
|
|
552
|
+
getCoinsTopVolume24h,
|
|
553
|
+
getCoinsMostValuable,
|
|
554
|
+
getCoinsNew,
|
|
555
|
+
getCoinsTopGainers,
|
|
556
|
+
getCoinsLastTraded,
|
|
557
|
+
getCoinsLastTradedUnique,
|
|
558
|
+
getExploreTopVolumeAll24h,
|
|
559
|
+
getExploreTopVolumeCreators24h,
|
|
560
|
+
getExploreNewAll,
|
|
561
|
+
getExploreFeaturedCreators,
|
|
562
|
+
getExploreFeaturedVideos,
|
|
563
|
+
getCreatorCoins,
|
|
564
|
+
getMostValuableCreatorCoins,
|
|
565
|
+
getMostValuableAll,
|
|
566
|
+
getMostValuableTrends,
|
|
567
|
+
getNewTrends,
|
|
568
|
+
getTopVolumeTrends24h,
|
|
569
|
+
getTrendingAll,
|
|
570
|
+
getTrendingCreators,
|
|
571
|
+
getTrendingPosts,
|
|
572
|
+
getTrendingTrends
|
|
573
|
+
} from "@zoralabs/coins-sdk";
|
|
574
|
+
|
|
575
|
+
// src/lib/format.ts
|
|
576
|
+
import { format, formatDistanceStrict } from "date-fns";
|
|
577
|
+
import { formatEther } from "viem";
|
|
578
|
+
var ANSI_CODES = {
|
|
579
|
+
dim: ["\x1B[2m", "\x1B[22m"],
|
|
580
|
+
bold: ["\x1B[1m", "\x1B[22m"]
|
|
581
|
+
};
|
|
582
|
+
function styledText(text, style) {
|
|
583
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
584
|
+
if (!useColor) return text;
|
|
585
|
+
const [open, close] = ANSI_CODES[style];
|
|
586
|
+
return `${open}${text}${close}`;
|
|
587
|
+
}
|
|
588
|
+
function formatCurrency(value) {
|
|
589
|
+
if (!value || Number(value) === 0) return "$0";
|
|
590
|
+
return new Intl.NumberFormat("en-US", {
|
|
591
|
+
style: "currency",
|
|
592
|
+
currency: "USD",
|
|
593
|
+
notation: "compact",
|
|
594
|
+
minimumFractionDigits: 1,
|
|
595
|
+
maximumFractionDigits: 1
|
|
596
|
+
}).format(Number(value));
|
|
597
|
+
}
|
|
598
|
+
var NO_CHANGE = { text: "-", color: void 0 };
|
|
599
|
+
function formatMcapChange(marketCap, delta) {
|
|
600
|
+
if (!delta || !marketCap) return NO_CHANGE;
|
|
601
|
+
const currentMCap = Number(marketCap);
|
|
602
|
+
const absoluteDelta = Number(delta);
|
|
603
|
+
const previousMCap = currentMCap - absoluteDelta;
|
|
604
|
+
if (currentMCap === 0 || previousMCap === 0) return NO_CHANGE;
|
|
605
|
+
const percentChange = absoluteDelta / previousMCap * 100;
|
|
606
|
+
const plusPrefix = percentChange >= 0 ? "+" : "";
|
|
607
|
+
const text = `${plusPrefix}${percentChange.toFixed(1)}%`;
|
|
608
|
+
const color = percentChange > 0 ? "green" : percentChange < 0 ? "red" : void 0;
|
|
609
|
+
return { text, color };
|
|
610
|
+
}
|
|
611
|
+
function formatUsd(value) {
|
|
612
|
+
return new Intl.NumberFormat("en-US", {
|
|
613
|
+
style: "currency",
|
|
614
|
+
currency: "USD",
|
|
615
|
+
minimumFractionDigits: 2,
|
|
616
|
+
maximumFractionDigits: 2
|
|
617
|
+
}).format(value);
|
|
618
|
+
}
|
|
619
|
+
function formatHolders(count) {
|
|
620
|
+
return new Intl.NumberFormat("en-US").format(count);
|
|
621
|
+
}
|
|
622
|
+
function formatRelativeTime(date, now = /* @__PURE__ */ new Date()) {
|
|
623
|
+
const diffMs = now.getTime() - date.getTime();
|
|
624
|
+
if (diffMs < 6e4) return "just now";
|
|
625
|
+
return formatDistanceStrict(date, now, { addSuffix: true });
|
|
626
|
+
}
|
|
627
|
+
function formatAbsoluteTime(date) {
|
|
628
|
+
return format(date, "yyyy-MM-dd h:mm a");
|
|
629
|
+
}
|
|
630
|
+
function formatCreatedAt(isoDate, now) {
|
|
631
|
+
if (!isoDate) return "-";
|
|
632
|
+
const date = new Date(isoDate);
|
|
633
|
+
if (isNaN(date.getTime())) return "-";
|
|
634
|
+
return `${formatRelativeTime(date, now)} (${formatAbsoluteTime(date)})`;
|
|
635
|
+
}
|
|
636
|
+
var formatEthDisplay = (wei) => {
|
|
637
|
+
const eth = formatEther(wei);
|
|
638
|
+
const parts = eth.split(".");
|
|
639
|
+
if (!parts[1]) return eth;
|
|
640
|
+
const trimmed = parts[1].replace(/0+$/, "") || "0";
|
|
641
|
+
return `${parts[0]}.${trimmed}`;
|
|
642
|
+
};
|
|
643
|
+
var formatCoinsDisplay = (coinsOut) => new Intl.NumberFormat("en-US", {
|
|
644
|
+
maximumFractionDigits: 2
|
|
645
|
+
}).format(Number(coinsOut));
|
|
646
|
+
|
|
647
|
+
// src/lib/types.ts
|
|
648
|
+
var SORT_LABELS = {
|
|
649
|
+
mcap: "Top by Market Cap",
|
|
650
|
+
volume: "Top by 24h Volume",
|
|
651
|
+
new: "New",
|
|
652
|
+
gainers: "Top Gainers (24h)",
|
|
653
|
+
"last-traded": "Last Traded",
|
|
654
|
+
"last-traded-unique": "Last Traded (Unique)",
|
|
655
|
+
trending: "Trending",
|
|
656
|
+
featured: "Featured"
|
|
657
|
+
};
|
|
658
|
+
var TYPE_LABELS = {
|
|
659
|
+
all: "all",
|
|
660
|
+
trend: "trends",
|
|
661
|
+
"creator-coin": "creator coins",
|
|
662
|
+
post: "posts"
|
|
663
|
+
};
|
|
664
|
+
var COIN_TYPE_DISPLAY = {
|
|
665
|
+
CONTENT: "post",
|
|
666
|
+
CREATOR: "creator-coin",
|
|
667
|
+
TREND: "trend"
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/commands/explore.tsx
|
|
671
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
672
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
673
|
+
var QUERY_MAP = {
|
|
674
|
+
mcap: {
|
|
675
|
+
all: getMostValuableAll,
|
|
676
|
+
trend: getMostValuableTrends,
|
|
677
|
+
"creator-coin": getMostValuableCreatorCoins,
|
|
678
|
+
post: getCoinsMostValuable
|
|
679
|
+
},
|
|
680
|
+
volume: {
|
|
681
|
+
all: getExploreTopVolumeAll24h,
|
|
682
|
+
trend: getTopVolumeTrends24h,
|
|
683
|
+
"creator-coin": getExploreTopVolumeCreators24h,
|
|
684
|
+
post: getCoinsTopVolume24h
|
|
685
|
+
},
|
|
686
|
+
new: {
|
|
687
|
+
all: getExploreNewAll,
|
|
688
|
+
trend: getNewTrends,
|
|
689
|
+
"creator-coin": getCreatorCoins,
|
|
690
|
+
post: getCoinsNew
|
|
691
|
+
},
|
|
692
|
+
gainers: {
|
|
693
|
+
post: getCoinsTopGainers
|
|
694
|
+
},
|
|
695
|
+
"last-traded": {
|
|
696
|
+
post: getCoinsLastTraded
|
|
697
|
+
},
|
|
698
|
+
"last-traded-unique": {
|
|
699
|
+
post: getCoinsLastTradedUnique
|
|
700
|
+
},
|
|
701
|
+
trending: {
|
|
702
|
+
all: getTrendingAll,
|
|
703
|
+
trend: getTrendingTrends,
|
|
704
|
+
"creator-coin": getTrendingCreators,
|
|
705
|
+
post: getTrendingPosts
|
|
706
|
+
},
|
|
707
|
+
featured: {
|
|
708
|
+
"creator-coin": getExploreFeaturedCreators,
|
|
709
|
+
post: getExploreFeaturedVideos
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
var formatCompactCurrency = (value) => {
|
|
713
|
+
if (!value) return "$0";
|
|
714
|
+
return new Intl.NumberFormat("en-US", {
|
|
715
|
+
style: "currency",
|
|
716
|
+
currency: "USD",
|
|
717
|
+
notation: "compact",
|
|
718
|
+
maximumFractionDigits: 1
|
|
719
|
+
}).format(Number(value));
|
|
720
|
+
};
|
|
721
|
+
var formatChange = (marketCap, delta) => {
|
|
722
|
+
if (!delta || !marketCap) return "-";
|
|
723
|
+
const cap = Number(marketCap);
|
|
724
|
+
const d = Number(delta);
|
|
725
|
+
if (cap === 0) return "-";
|
|
726
|
+
const prevCap = cap - d;
|
|
727
|
+
if (prevCap === 0) return "-";
|
|
728
|
+
const pct = d / prevCap * 100;
|
|
729
|
+
const sign = pct >= 0 ? "+" : "";
|
|
730
|
+
return `${sign}${pct.toFixed(1)}%`;
|
|
731
|
+
};
|
|
732
|
+
var changeColor = (row) => {
|
|
733
|
+
if (!row.marketCapDelta24h || !row.marketCap) return void 0;
|
|
734
|
+
const cap = Number(row.marketCap);
|
|
735
|
+
const d = Number(row.marketCapDelta24h);
|
|
736
|
+
if (cap === 0 || cap - d === 0) return void 0;
|
|
737
|
+
const pct = d / (cap - d) * 100;
|
|
738
|
+
if (pct > 0) return "green";
|
|
739
|
+
if (pct < 0) return "red";
|
|
740
|
+
return void 0;
|
|
741
|
+
};
|
|
742
|
+
var SORT_OPTIONS = Object.keys(SORT_LABELS).join(", ");
|
|
743
|
+
var rankColumn = {
|
|
744
|
+
header: "#",
|
|
745
|
+
width: 5,
|
|
746
|
+
accessor: (r) => String(r.rank)
|
|
747
|
+
};
|
|
748
|
+
var exploreColumns = [
|
|
749
|
+
{ header: "Name", width: 27, accessor: (r) => r.name ?? "Unknown" },
|
|
750
|
+
{ header: "Address", width: 44, accessor: (r) => r.address ?? "" },
|
|
751
|
+
{
|
|
752
|
+
header: "Type",
|
|
753
|
+
width: 16,
|
|
754
|
+
accessor: (r) => COIN_TYPE_DISPLAY[r.coinType ?? ""] ?? r.coinType ?? ""
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
header: "Market Cap",
|
|
758
|
+
width: 14,
|
|
759
|
+
accessor: (r) => formatCompactCurrency(r.marketCap)
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
header: "24h Vol",
|
|
763
|
+
width: 14,
|
|
764
|
+
accessor: (r) => formatCompactCurrency(r.volume24h)
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
header: "24h Change",
|
|
768
|
+
width: 12,
|
|
769
|
+
accessor: (r) => formatChange(r.marketCap, r.marketCapDelta24h),
|
|
770
|
+
color: changeColor
|
|
771
|
+
}
|
|
772
|
+
];
|
|
773
|
+
var exploreCommand = new Command2("explore").description("Browse top, new, and highest volume coins").option("--sort <sort>", `Sort by: ${SORT_OPTIONS}`, "mcap").option(
|
|
774
|
+
"--type <type>",
|
|
775
|
+
"Filter by type: all, trend, creator-coin, post (availability varies by sort)",
|
|
776
|
+
"post"
|
|
777
|
+
).option("--limit <n>", "Number of results (max 20)", "10").option("--after <cursor>", "Pagination cursor from a previous result").action(async function(opts) {
|
|
778
|
+
const json = getJson(this);
|
|
779
|
+
const sort = opts.sort;
|
|
780
|
+
const type = opts.type;
|
|
781
|
+
const limit = parseInt(opts.limit, 10);
|
|
782
|
+
const after = opts.after;
|
|
783
|
+
if (isNaN(limit) || limit <= 0 || limit > 20) {
|
|
784
|
+
outputErrorAndExit(
|
|
785
|
+
json,
|
|
786
|
+
`Invalid --limit value: ${opts.limit}. Must be an integer between 1 and 20.`,
|
|
787
|
+
"Usage: zora explore --limit 10"
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (!QUERY_MAP[sort]) {
|
|
791
|
+
outputErrorAndExit(
|
|
792
|
+
json,
|
|
793
|
+
`Invalid --sort value: ${sort}.`,
|
|
794
|
+
`Supported: ${SORT_OPTIONS}`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (!QUERY_MAP[sort][type]) {
|
|
798
|
+
const supported = Object.keys(QUERY_MAP[sort]);
|
|
799
|
+
outputErrorAndExit(
|
|
800
|
+
json,
|
|
801
|
+
`Invalid --type for --sort ${sort}.`,
|
|
802
|
+
`Supported: ${supported.join(", ")}`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const apiKey = getApiKey();
|
|
806
|
+
if (apiKey) {
|
|
807
|
+
setApiKey(apiKey);
|
|
808
|
+
}
|
|
809
|
+
const queryFn = QUERY_MAP[sort][type];
|
|
810
|
+
let response;
|
|
811
|
+
try {
|
|
812
|
+
response = await queryFn({ count: limit, after });
|
|
813
|
+
} catch (err) {
|
|
814
|
+
outputErrorAndExit(
|
|
815
|
+
json,
|
|
816
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
if (response.error) {
|
|
820
|
+
const msg = typeof response.error === "object" && response.error.error ? response.error.error : JSON.stringify(response.error);
|
|
821
|
+
outputErrorAndExit(json, `API error: ${msg}`);
|
|
822
|
+
}
|
|
823
|
+
const edges = response.data?.exploreList?.edges ?? [];
|
|
824
|
+
const coins = edges.map((e) => e.node);
|
|
825
|
+
const pageInfo = response.data?.exploreList?.pageInfo;
|
|
826
|
+
if (coins.length === 0) {
|
|
827
|
+
outputData(json, {
|
|
828
|
+
json: { coins: [], pageInfo: pageInfo ?? null },
|
|
829
|
+
table: () => {
|
|
830
|
+
renderOnce(
|
|
831
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 1, marginTop: 1, children: [
|
|
832
|
+
/* @__PURE__ */ jsx2(Text2, { children: "No coins found." }),
|
|
833
|
+
/* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
|
|
834
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Try a different sort or type (defaults to posts):" }),
|
|
835
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " zora explore --sort volume --type all" }),
|
|
836
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " zora explore --sort new --type all" })
|
|
837
|
+
] })
|
|
838
|
+
] })
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const rankedCoins = coins.map((c, i) => ({ ...c, rank: i + 1 }));
|
|
845
|
+
const columns = after ? exploreColumns : [rankColumn, ...exploreColumns];
|
|
846
|
+
const title = type !== "all" ? `${SORT_LABELS[sort]} \xB7 ${TYPE_LABELS[type]}` : SORT_LABELS[sort];
|
|
847
|
+
const subtitle = `${coins.length} result${coins.length !== 1 ? "s" : ""}`;
|
|
848
|
+
outputData(json, {
|
|
849
|
+
json: { coins, pageInfo: pageInfo ?? null },
|
|
850
|
+
table: () => {
|
|
851
|
+
renderOnce(
|
|
852
|
+
/* @__PURE__ */ jsx2(
|
|
853
|
+
TableComponent,
|
|
854
|
+
{
|
|
855
|
+
columns,
|
|
856
|
+
data: rankedCoins,
|
|
857
|
+
title,
|
|
858
|
+
subtitle
|
|
859
|
+
}
|
|
860
|
+
)
|
|
861
|
+
);
|
|
862
|
+
if (pageInfo?.hasNextPage && pageInfo.endCursor) {
|
|
863
|
+
console.log(
|
|
864
|
+
`
|
|
865
|
+
${styledText(`Next page: zora explore --sort ${sort} --type ${type} --limit ${limit} --after ${pageInfo.endCursor}`, "dim")}`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
track("cli_explore", {
|
|
871
|
+
sort,
|
|
872
|
+
type,
|
|
873
|
+
limit,
|
|
874
|
+
paginated: after !== void 0,
|
|
875
|
+
result_count: coins.length,
|
|
876
|
+
has_next_page: pageInfo?.hasNextPage ?? false,
|
|
877
|
+
output_format: json ? "json" : "text"
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// src/lib/balance-format.ts
|
|
882
|
+
var COIN_DECIMALS = 18;
|
|
883
|
+
var toHumanBalance = (rawBalance) => Number(normalizeTokenAmount(rawBalance));
|
|
884
|
+
var normalizeTokenAmount = (rawBalance, decimals = COIN_DECIMALS) => {
|
|
885
|
+
try {
|
|
886
|
+
const value = BigInt(rawBalance);
|
|
887
|
+
const divisor = 10n ** BigInt(decimals);
|
|
888
|
+
const whole = value / divisor;
|
|
889
|
+
const fraction = value % divisor;
|
|
890
|
+
if (fraction === 0n) return whole.toString();
|
|
891
|
+
const fractionText = fraction.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
892
|
+
return `${whole}.${fractionText}`;
|
|
893
|
+
} catch {
|
|
894
|
+
console.warn(`Warning: could not parse token amount "${rawBalance}"`);
|
|
895
|
+
return rawBalance;
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
var formatUsdValue = (balance, priceInUsdc) => {
|
|
899
|
+
if (!priceInUsdc) return "-";
|
|
900
|
+
const value = toHumanBalance(balance) * Number(priceInUsdc);
|
|
901
|
+
if (value < 0.01) return "<$0.01";
|
|
902
|
+
return formatUsd(value);
|
|
903
|
+
};
|
|
904
|
+
var formatBalance = (balance) => {
|
|
905
|
+
const n = toHumanBalance(balance);
|
|
906
|
+
if (n === 0) return "0";
|
|
907
|
+
if (n < 1e-3) return "<0.001";
|
|
908
|
+
if (n < 1) return n.toFixed(4);
|
|
909
|
+
return new Intl.NumberFormat("en-US", {
|
|
910
|
+
notation: "compact",
|
|
911
|
+
compactDisplay: "long",
|
|
912
|
+
maximumFractionDigits: 1
|
|
913
|
+
}).format(n);
|
|
914
|
+
};
|
|
915
|
+
var trimTrailingZeros = (value) => {
|
|
916
|
+
if (!value.includes(".")) return value;
|
|
917
|
+
const trimmed = value.replace(/0+$/, "").replace(/\.$/, "");
|
|
918
|
+
return trimmed || "0";
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/lib/wallet-balances.ts
|
|
922
|
+
import { getTokenInfo } from "@zoralabs/coins-sdk";
|
|
923
|
+
import {
|
|
924
|
+
createPublicClient as createPublicClient2,
|
|
925
|
+
erc20Abi,
|
|
926
|
+
formatUnits,
|
|
927
|
+
http
|
|
928
|
+
} from "viem";
|
|
929
|
+
import { base as base2 } from "viem/chains";
|
|
930
|
+
var TRACKED_TOKENS = [
|
|
931
|
+
{
|
|
932
|
+
name: "Ether",
|
|
933
|
+
symbol: "ETH",
|
|
934
|
+
address: WETH_ADDRESS,
|
|
935
|
+
decimals: 18,
|
|
936
|
+
priceAddress: WETH_ADDRESS,
|
|
937
|
+
isNative: true
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
name: "USD Coin",
|
|
941
|
+
symbol: "USDC",
|
|
942
|
+
address: USDC_ADDRESS,
|
|
943
|
+
decimals: USDC_DECIMALS,
|
|
944
|
+
priceAddress: USDC_ADDRESS,
|
|
945
|
+
fixedPriceUsd: 1
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
name: "ZORA",
|
|
949
|
+
symbol: "ZORA",
|
|
950
|
+
address: ZORA_ADDRESS,
|
|
951
|
+
decimals: 18,
|
|
952
|
+
priceAddress: ZORA_ADDRESS
|
|
953
|
+
}
|
|
954
|
+
];
|
|
955
|
+
var fetchTokenPriceUsd = async (address, chainId = BASE_CHAIN_ID) => {
|
|
956
|
+
try {
|
|
957
|
+
const res = await getTokenInfo({ address, chainId });
|
|
958
|
+
return res.data?.erc20Token?.currency?.priceUsd ? Number(res.data.erc20Token.currency.priceUsd) : null;
|
|
959
|
+
} catch (err) {
|
|
960
|
+
console.warn(
|
|
961
|
+
`Warning: failed to fetch price for ${address}: ${err instanceof Error ? err.message : String(err)}`
|
|
962
|
+
);
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
var fetchWalletBalances = async (walletAddress) => {
|
|
967
|
+
const publicClient = createPublicClient2({ chain: base2, transport: http() });
|
|
968
|
+
const nativeToken = TRACKED_TOKENS.find((t) => t.isNative);
|
|
969
|
+
const erc20Tokens = TRACKED_TOKENS.filter((t) => !t.isNative);
|
|
970
|
+
const [ethBalance, multicallResults] = await Promise.all([
|
|
971
|
+
publicClient.getBalance({ address: walletAddress }),
|
|
972
|
+
publicClient.multicall({
|
|
973
|
+
contracts: erc20Tokens.map((t) => ({
|
|
974
|
+
address: t.address,
|
|
975
|
+
abi: erc20Abi,
|
|
976
|
+
functionName: "balanceOf",
|
|
977
|
+
args: [walletAddress]
|
|
978
|
+
}))
|
|
979
|
+
})
|
|
980
|
+
]);
|
|
981
|
+
const rawBalances = /* @__PURE__ */ new Map();
|
|
982
|
+
if (nativeToken) rawBalances.set(nativeToken, ethBalance);
|
|
983
|
+
erc20Tokens.forEach((token, i) => {
|
|
984
|
+
if (multicallResults[i].status === "success") {
|
|
985
|
+
rawBalances.set(token, multicallResults[i].result);
|
|
986
|
+
} else {
|
|
987
|
+
console.warn(`Warning: failed to fetch balance for ${token.symbol}`);
|
|
988
|
+
rawBalances.set(token, 0n);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
const priceResults = await Promise.allSettled(
|
|
992
|
+
TRACKED_TOKENS.map(async (token) => {
|
|
993
|
+
const balance = rawBalances.get(token) ?? 0n;
|
|
994
|
+
let priceUsd = null;
|
|
995
|
+
if (token.fixedPriceUsd != null) {
|
|
996
|
+
priceUsd = token.fixedPriceUsd;
|
|
997
|
+
} else if (balance > 0n || token.isNative) {
|
|
998
|
+
priceUsd = await fetchTokenPriceUsd(token.priceAddress);
|
|
999
|
+
}
|
|
1000
|
+
return { token, balance, priceUsd };
|
|
1001
|
+
})
|
|
1002
|
+
);
|
|
1003
|
+
const resolved = priceResults.map((result, i) => {
|
|
1004
|
+
if (result.status === "fulfilled") return result.value;
|
|
1005
|
+
const token = TRACKED_TOKENS[i];
|
|
1006
|
+
console.warn(`Warning: failed to resolve token ${token.symbol}`);
|
|
1007
|
+
return { token, balance: rawBalances.get(token) ?? 0n, priceUsd: null };
|
|
1008
|
+
});
|
|
1009
|
+
const visible = resolved.filter((r) => r.balance > 0n || r.token.isNative);
|
|
1010
|
+
const intermediate = visible.map(({ token, balance, priceUsd }) => {
|
|
1011
|
+
const human = formatUnits(balance, token.decimals);
|
|
1012
|
+
const usdValue = priceUsd !== null ? Number(human) * priceUsd : null;
|
|
1013
|
+
return { token, human, priceUsd, usdValue };
|
|
1014
|
+
});
|
|
1015
|
+
const walletBalances = intermediate.map(
|
|
1016
|
+
({ token, human, usdValue }) => ({
|
|
1017
|
+
name: token.name,
|
|
1018
|
+
symbol: token.symbol,
|
|
1019
|
+
balance: trimTrailingZeros(human),
|
|
1020
|
+
usdValue: usdValue !== null ? formatUsd(usdValue) : "-"
|
|
1021
|
+
})
|
|
1022
|
+
);
|
|
1023
|
+
const walletBalancesJson = intermediate.map(
|
|
1024
|
+
({ token, human, priceUsd, usdValue }) => ({
|
|
1025
|
+
name: token.name,
|
|
1026
|
+
symbol: token.symbol,
|
|
1027
|
+
address: token.isNative ? null : token.address,
|
|
1028
|
+
balance: trimTrailingZeros(human),
|
|
1029
|
+
priceUsd,
|
|
1030
|
+
usdValue: usdValue !== null ? Number(usdValue.toFixed(6)) : null
|
|
1031
|
+
})
|
|
1032
|
+
);
|
|
1033
|
+
return { walletBalances, walletBalancesJson };
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// src/commands/balance.ts
|
|
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_LABELS2 = {
|
|
1044
|
+
"usd-value": "USD Value",
|
|
1045
|
+
balance: "Balance",
|
|
1046
|
+
"market-cap": "Market Cap",
|
|
1047
|
+
"price-change": "Price Change"
|
|
1048
|
+
};
|
|
1049
|
+
var SORT_OPTIONS2 = Object.keys(SORT_MAP).join(", ");
|
|
1050
|
+
var extractErrorMessage = (error) => {
|
|
1051
|
+
if (typeof error === "object" && error !== null && "error" in error) {
|
|
1052
|
+
return String(error.error);
|
|
1053
|
+
}
|
|
1054
|
+
return JSON.stringify(error);
|
|
1055
|
+
};
|
|
1056
|
+
var changeColor2 = (row) => {
|
|
1057
|
+
if (!row.coin?.marketCap || !row.coin.marketCapDelta24h) return void 0;
|
|
1058
|
+
const cap = Number(row.coin.marketCap);
|
|
1059
|
+
const d = Number(row.coin.marketCapDelta24h);
|
|
1060
|
+
if (cap === 0 || cap - d === 0) return void 0;
|
|
1061
|
+
const pct = d / (cap - d) * 100;
|
|
1062
|
+
if (pct > 0) return "green";
|
|
1063
|
+
if (pct < 0) return "red";
|
|
1064
|
+
return void 0;
|
|
1065
|
+
};
|
|
1066
|
+
var walletColumns = [
|
|
1067
|
+
{ header: "Name", width: 14, accessor: (row) => row.name },
|
|
1068
|
+
{
|
|
1069
|
+
header: "Symbol",
|
|
1070
|
+
width: 10,
|
|
1071
|
+
noTruncate: true,
|
|
1072
|
+
accessor: (row) => row.symbol
|
|
1073
|
+
},
|
|
1074
|
+
{ header: "Balance", width: 20, accessor: (row) => row.balance },
|
|
1075
|
+
{ header: "USD Value", width: 16, accessor: (row) => row.usdValue }
|
|
1076
|
+
];
|
|
1077
|
+
var balanceColumns = [
|
|
1078
|
+
{ header: "#", width: 5, accessor: (row) => String(row.rank) },
|
|
1079
|
+
{ header: "Name", width: 24, accessor: (row) => row.coin?.name ?? "Unknown" },
|
|
1080
|
+
{
|
|
1081
|
+
header: "Symbol",
|
|
1082
|
+
width: 12,
|
|
1083
|
+
noTruncate: true,
|
|
1084
|
+
accessor: (row) => row.coin?.symbol ?? ""
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
header: "Balance",
|
|
1088
|
+
width: 14,
|
|
1089
|
+
accessor: (row) => formatBalance(row.balance)
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
header: "USD Value",
|
|
1093
|
+
width: 14,
|
|
1094
|
+
accessor: (row) => formatUsdValue(row.balance, row.coin?.tokenPrice?.priceInUsdc)
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
header: "Market Cap",
|
|
1098
|
+
width: 14,
|
|
1099
|
+
accessor: (row) => formatCompactCurrency(row.coin?.marketCap)
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
header: "24h Change",
|
|
1103
|
+
width: 12,
|
|
1104
|
+
accessor: (row) => formatChange(row.coin?.marketCap, row.coin?.marketCapDelta24h),
|
|
1105
|
+
color: changeColor2
|
|
1106
|
+
}
|
|
1107
|
+
];
|
|
1108
|
+
var formatBalanceJson = (balance, rank) => {
|
|
1109
|
+
const priceUsd = balance.coin?.tokenPrice?.priceInUsdc;
|
|
1110
|
+
const marketCap = balance.coin?.marketCap ? Number(balance.coin.marketCap) : null;
|
|
1111
|
+
const marketCapDelta24h = balance.coin?.marketCapDelta24h ? Number(balance.coin.marketCapDelta24h) : null;
|
|
1112
|
+
const volume24h = balance.coin?.volume24h ? Number(balance.coin.volume24h) : null;
|
|
1113
|
+
const totalVolume = balance.coin?.totalVolume ? Number(balance.coin.totalVolume) : null;
|
|
1114
|
+
const priceUsdValue = priceUsd ? Number(priceUsd) : null;
|
|
1115
|
+
const usdValue = priceUsdValue !== null ? Number((toHumanBalance(balance.balance) * priceUsdValue).toFixed(6)) : null;
|
|
1116
|
+
const marketCapChange24h = marketCap !== null && marketCapDelta24h !== null && marketCap - marketCapDelta24h !== 0 ? Number(
|
|
1117
|
+
(marketCapDelta24h / (marketCap - marketCapDelta24h) * 100).toFixed(
|
|
1118
|
+
4
|
|
1119
|
+
)
|
|
1120
|
+
) : null;
|
|
1121
|
+
return {
|
|
1122
|
+
rank,
|
|
1123
|
+
name: balance.coin?.name ?? null,
|
|
1124
|
+
symbol: balance.coin?.symbol ?? null,
|
|
1125
|
+
coinType: balance.coin?.coinType ?? null,
|
|
1126
|
+
chainId: balance.coin?.chainId ?? null,
|
|
1127
|
+
address: balance.coin?.address ?? null,
|
|
1128
|
+
creatorHandle: balance.coin?.creatorProfile?.handle ?? null,
|
|
1129
|
+
previewImage: balance.coin?.mediaContent?.previewImage?.medium ?? null,
|
|
1130
|
+
balance: normalizeTokenAmount(balance.balance),
|
|
1131
|
+
usdValue,
|
|
1132
|
+
priceUsd: priceUsdValue,
|
|
1133
|
+
marketCap,
|
|
1134
|
+
marketCapDelta24h,
|
|
1135
|
+
marketCapChange24h,
|
|
1136
|
+
volume24h,
|
|
1137
|
+
totalVolume
|
|
1138
|
+
};
|
|
1139
|
+
};
|
|
1140
|
+
function resolveContext(json) {
|
|
1141
|
+
const account = resolveAccount(json);
|
|
1142
|
+
const apiKey = getApiKey();
|
|
1143
|
+
if (!apiKey) {
|
|
1144
|
+
outputErrorAndExit(
|
|
1145
|
+
json,
|
|
1146
|
+
"Not authenticated. Run 'zora auth configure' to set your API key."
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
setApiKey2(apiKey);
|
|
1150
|
+
return account;
|
|
1151
|
+
}
|
|
1152
|
+
function renderWallet(json, walletResult) {
|
|
1153
|
+
outputData(json, {
|
|
1154
|
+
json: { wallet: walletResult.walletBalancesJson },
|
|
1155
|
+
table: () => {
|
|
1156
|
+
renderOnce(
|
|
1157
|
+
TableComponent({
|
|
1158
|
+
columns: walletColumns,
|
|
1159
|
+
data: walletResult.walletBalances,
|
|
1160
|
+
title: "Wallet"
|
|
1161
|
+
})
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
function renderCoins(json, balances, total, sort) {
|
|
1167
|
+
const rankedBalances = balances.map((balance, index) => ({
|
|
1168
|
+
...balance,
|
|
1169
|
+
rank: index + 1
|
|
1170
|
+
}));
|
|
1171
|
+
outputData(json, {
|
|
1172
|
+
json: {
|
|
1173
|
+
coins: rankedBalances.map(
|
|
1174
|
+
(balance) => formatBalanceJson(balance, balance.rank)
|
|
1175
|
+
)
|
|
1176
|
+
},
|
|
1177
|
+
table: () => {
|
|
1178
|
+
if (balances.length === 0) {
|
|
1179
|
+
console.log("\n No coin balances found.\n");
|
|
1180
|
+
console.log(" Buy coins to see them here:");
|
|
1181
|
+
console.log(" zora buy <address> --eth 0.001\n");
|
|
1182
|
+
} else {
|
|
1183
|
+
renderOnce(
|
|
1184
|
+
TableComponent({
|
|
1185
|
+
columns: balanceColumns,
|
|
1186
|
+
data: rankedBalances,
|
|
1187
|
+
title: `Coins \xB7 sorted by ${SORT_LABELS2[sort]}`,
|
|
1188
|
+
subtitle: `${balances.length} of ${total}`
|
|
1189
|
+
})
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
async function fetchCoins(json, address, sort, limit) {
|
|
1196
|
+
let response;
|
|
1197
|
+
try {
|
|
1198
|
+
response = await getProfileBalances({
|
|
1199
|
+
identifier: address,
|
|
1200
|
+
count: limit,
|
|
1201
|
+
sortOption: SORT_MAP[sort]
|
|
1202
|
+
});
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
outputErrorAndExit(
|
|
1205
|
+
json,
|
|
1206
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
if (response.error) {
|
|
1210
|
+
outputErrorAndExit(
|
|
1211
|
+
json,
|
|
1212
|
+
`API error: ${extractErrorMessage(response.error)}`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
const edges = response.data?.profile?.coinBalances?.edges ?? [];
|
|
1216
|
+
const balances = edges.map(
|
|
1217
|
+
(e) => e.node
|
|
1218
|
+
);
|
|
1219
|
+
const total = response.data?.profile?.coinBalances?.count ?? balances.length;
|
|
1220
|
+
return { balances, total };
|
|
1221
|
+
}
|
|
1222
|
+
function validateCoinOpts(json, sort, limitStr) {
|
|
1223
|
+
if (!SORT_MAP[sort]) {
|
|
1224
|
+
outputErrorAndExit(
|
|
1225
|
+
json,
|
|
1226
|
+
`Invalid --sort value: ${sort}.`,
|
|
1227
|
+
`Supported: ${SORT_OPTIONS2}`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const limit = parseInt(limitStr, 10);
|
|
1231
|
+
if (isNaN(limit) || limit <= 0 || limit > 20) {
|
|
1232
|
+
outputErrorAndExit(
|
|
1233
|
+
json,
|
|
1234
|
+
`Invalid --limit value: ${limitStr}. Must be an integer between 1 and 20.`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return { sort, limit };
|
|
1238
|
+
}
|
|
1239
|
+
var balanceCommand = new Command3("balance").description("Show balances in your wallet").action(async function() {
|
|
1240
|
+
const json = getJson(this);
|
|
1241
|
+
const account = resolveContext(json);
|
|
1242
|
+
const sort = "usd-value";
|
|
1243
|
+
const limit = 10;
|
|
1244
|
+
let walletResult;
|
|
1245
|
+
let coinsResult;
|
|
1246
|
+
try {
|
|
1247
|
+
[walletResult, coinsResult] = await Promise.all([
|
|
1248
|
+
fetchWalletBalances(account.address),
|
|
1249
|
+
fetchCoins(json, account.address, sort, limit)
|
|
1250
|
+
]);
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
outputErrorAndExit(
|
|
1253
|
+
json,
|
|
1254
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
const rankedBalances = coinsResult.balances.map((balance, index) => ({
|
|
1258
|
+
...balance,
|
|
1259
|
+
rank: index + 1
|
|
1260
|
+
}));
|
|
1261
|
+
outputData(json, {
|
|
1262
|
+
json: {
|
|
1263
|
+
wallet: walletResult.walletBalancesJson,
|
|
1264
|
+
coins: rankedBalances.map(
|
|
1265
|
+
(balance) => formatBalanceJson(balance, balance.rank)
|
|
1266
|
+
)
|
|
1267
|
+
},
|
|
1268
|
+
table: () => {
|
|
1269
|
+
renderOnce(
|
|
1270
|
+
TableComponent({
|
|
1271
|
+
columns: walletColumns,
|
|
1272
|
+
data: walletResult.walletBalances,
|
|
1273
|
+
title: "Wallet"
|
|
1274
|
+
})
|
|
1275
|
+
);
|
|
1276
|
+
if (coinsResult.balances.length === 0) {
|
|
1277
|
+
console.log("\n No coin balances found.\n");
|
|
1278
|
+
console.log(" Buy coins to see them here:");
|
|
1279
|
+
console.log(" zora buy <address> --eth 0.001\n");
|
|
1280
|
+
} else {
|
|
1281
|
+
renderOnce(
|
|
1282
|
+
TableComponent({
|
|
1283
|
+
columns: balanceColumns,
|
|
1284
|
+
data: rankedBalances,
|
|
1285
|
+
title: `Coins \xB7 sorted by ${SORT_LABELS2[sort]}`,
|
|
1286
|
+
subtitle: `${coinsResult.balances.length} of ${coinsResult.total}`
|
|
1287
|
+
})
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
track("cli_balances", {
|
|
1293
|
+
sort,
|
|
1294
|
+
limit,
|
|
1295
|
+
result_count: coinsResult.balances.length,
|
|
1296
|
+
total_count: coinsResult.total,
|
|
1297
|
+
output_format: json ? "json" : "text"
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
balanceCommand.command("spendable").description("Show wallet token balances (ETH, USDC, ZORA)").action(async function() {
|
|
1301
|
+
const json = getJson(this);
|
|
1302
|
+
const account = resolveContext(json);
|
|
1303
|
+
let walletResult;
|
|
1304
|
+
try {
|
|
1305
|
+
walletResult = await fetchWalletBalances(account.address);
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
outputErrorAndExit(
|
|
1308
|
+
json,
|
|
1309
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
renderWallet(json, walletResult);
|
|
1313
|
+
});
|
|
1314
|
+
balanceCommand.command("coins").description("Show coin positions").option("--sort <sort>", `Sort by: ${SORT_OPTIONS2}`, "usd-value").option("--limit <n>", "Number of results (max 20)", "10").action(async function(opts) {
|
|
1315
|
+
const json = getJson(this);
|
|
1316
|
+
const { sort, limit } = validateCoinOpts(json, opts.sort, opts.limit);
|
|
1317
|
+
const account = resolveContext(json);
|
|
1318
|
+
const { balances, total } = await fetchCoins(
|
|
1319
|
+
json,
|
|
1320
|
+
account.address,
|
|
1321
|
+
sort,
|
|
1322
|
+
limit
|
|
1323
|
+
);
|
|
1324
|
+
renderCoins(json, balances, total, sort);
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
// src/commands/buy.ts
|
|
1328
|
+
import { Command as Command4 } from "commander";
|
|
1329
|
+
import confirm2 from "@inquirer/confirm";
|
|
1330
|
+
import { parseUnits, formatUnits as formatUnits3, isAddress } from "viem";
|
|
1331
|
+
import {
|
|
1332
|
+
setApiKey as setApiKey3,
|
|
1333
|
+
getCoin,
|
|
1334
|
+
tradeCoin,
|
|
1335
|
+
createTradeCall
|
|
1336
|
+
} from "@zoralabs/coins-sdk";
|
|
1337
|
+
|
|
1338
|
+
// src/lib/trade-helpers.ts
|
|
1339
|
+
import {
|
|
1340
|
+
parseEther,
|
|
1341
|
+
formatUnits as formatUnits2,
|
|
1342
|
+
isAddressEqual,
|
|
1343
|
+
parseEventLogs,
|
|
1344
|
+
erc20Abi as erc20Abi2
|
|
1345
|
+
} from "viem";
|
|
1346
|
+
var GAS_RESERVE = parseEther("0.001");
|
|
1347
|
+
var BUY_AMOUNT_CHECKS = {
|
|
1348
|
+
eth: (opts) => opts.eth !== void 0,
|
|
1349
|
+
usd: (opts) => opts.usd !== void 0,
|
|
1350
|
+
percent: (opts) => opts.percent !== void 0,
|
|
1351
|
+
all: (opts) => opts.all === true
|
|
1352
|
+
};
|
|
1353
|
+
var SELL_AMOUNT_CHECKS = {
|
|
1354
|
+
amount: (opts) => opts.amount !== void 0,
|
|
1355
|
+
usd: (opts) => opts.usd !== void 0,
|
|
1356
|
+
percent: (opts) => opts.percent !== void 0,
|
|
1357
|
+
all: (opts) => opts.all === true
|
|
1358
|
+
};
|
|
1359
|
+
var getAmountMode = (json, opts, checks, flagNames) => {
|
|
1360
|
+
const provided = Object.entries(checks).filter(([, isProvided]) => isProvided(opts)).map(([mode]) => mode);
|
|
1361
|
+
if (provided.length === 0) {
|
|
1362
|
+
outputErrorAndExit(json, `Specify one amount flag: ${flagNames}`);
|
|
1363
|
+
}
|
|
1364
|
+
if (provided.length > 1) {
|
|
1365
|
+
outputErrorAndExit(json, `Only one amount flag allowed: ${flagNames}`);
|
|
1366
|
+
}
|
|
1367
|
+
return provided[0];
|
|
1368
|
+
};
|
|
1369
|
+
var parsePercentageLikeValue = (value) => {
|
|
1370
|
+
if (!/^\d+(\.\d+)?$/.test(value)) return void 0;
|
|
1371
|
+
const parsed = Number(value);
|
|
1372
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1373
|
+
};
|
|
1374
|
+
var formatAmountDisplay = (amount, decimals) => {
|
|
1375
|
+
const formatted = formatUnits2(amount, decimals);
|
|
1376
|
+
const parts = formatted.split(".");
|
|
1377
|
+
if (!parts[1]) {
|
|
1378
|
+
return new Intl.NumberFormat("en-US", {
|
|
1379
|
+
maximumFractionDigits: 2
|
|
1380
|
+
}).format(Number(formatted));
|
|
1381
|
+
}
|
|
1382
|
+
const twoDecimal = `${parts[0]}.${parts[1].slice(0, 2)}`;
|
|
1383
|
+
let maxDecimals = 2;
|
|
1384
|
+
if (Number(twoDecimal) === 0 && amount > 0n) {
|
|
1385
|
+
const sigIndex = parts[1].search(/[1-9]/);
|
|
1386
|
+
maxDecimals = sigIndex === -1 ? 6 : Math.min(sigIndex + 4, parts[1].length);
|
|
1387
|
+
}
|
|
1388
|
+
const truncated = `${parts[0]}.${parts[1].slice(0, maxDecimals)}`;
|
|
1389
|
+
return new Intl.NumberFormat("en-US", {
|
|
1390
|
+
maximumFractionDigits: maxDecimals
|
|
1391
|
+
}).format(Number(truncated));
|
|
1392
|
+
};
|
|
1393
|
+
var getReceivedAmountFromReceipt = ({
|
|
1394
|
+
receipt,
|
|
1395
|
+
tokenAddress,
|
|
1396
|
+
recipient
|
|
1397
|
+
}) => {
|
|
1398
|
+
const transfers = parseEventLogs({
|
|
1399
|
+
abi: erc20Abi2,
|
|
1400
|
+
eventName: "Transfer",
|
|
1401
|
+
logs: receipt.logs,
|
|
1402
|
+
strict: false
|
|
1403
|
+
});
|
|
1404
|
+
const matchingTransfers = transfers.filter((transfer) => {
|
|
1405
|
+
const to = transfer.args?.to;
|
|
1406
|
+
if (!to) return false;
|
|
1407
|
+
return isAddressEqual(transfer.address, tokenAddress) && isAddressEqual(to, recipient);
|
|
1408
|
+
});
|
|
1409
|
+
if (matchingTransfers.length === 0) {
|
|
1410
|
+
throw new Error("No matching Transfer event found in receipt.");
|
|
1411
|
+
}
|
|
1412
|
+
return matchingTransfers.reduce((total, transfer) => {
|
|
1413
|
+
const value = transfer.args?.value;
|
|
1414
|
+
if (value === void 0) {
|
|
1415
|
+
throw new Error("Transfer event missing amount.");
|
|
1416
|
+
}
|
|
1417
|
+
return total + value;
|
|
1418
|
+
}, 0n);
|
|
1419
|
+
};
|
|
1420
|
+
var printDebugRequest = (label, tradeParameters) => {
|
|
1421
|
+
if (process.env.ZORA_API_TARGET) {
|
|
1422
|
+
console.error(`[debug] API target: ${process.env.ZORA_API_TARGET}`);
|
|
1423
|
+
}
|
|
1424
|
+
console.error(`
|
|
1425
|
+
[debug] ${label} \u2014 Quote Request:`);
|
|
1426
|
+
console.error(
|
|
1427
|
+
JSON.stringify(
|
|
1428
|
+
{
|
|
1429
|
+
tokenIn: tradeParameters.sell,
|
|
1430
|
+
tokenOut: tradeParameters.buy,
|
|
1431
|
+
amountIn: tradeParameters.amountIn.toString(),
|
|
1432
|
+
slippage: tradeParameters.slippage,
|
|
1433
|
+
chainId: 8453,
|
|
1434
|
+
sender: tradeParameters.sender,
|
|
1435
|
+
recipient: tradeParameters.recipient || tradeParameters.sender
|
|
1436
|
+
},
|
|
1437
|
+
null,
|
|
1438
|
+
2
|
|
1439
|
+
)
|
|
1440
|
+
);
|
|
1441
|
+
};
|
|
1442
|
+
var printDebugResponse = (label, quoteResponse) => {
|
|
1443
|
+
console.error(`
|
|
1444
|
+
[debug] ${label} \u2014 Quote Response:`);
|
|
1445
|
+
console.error(JSON.stringify(quoteResponse, null, 2));
|
|
1446
|
+
console.error("");
|
|
1447
|
+
};
|
|
1448
|
+
var printQuote = (json, info) => {
|
|
1449
|
+
if (json) {
|
|
1450
|
+
outputJson({
|
|
1451
|
+
action: "quote",
|
|
1452
|
+
coin: info.coinSymbol,
|
|
1453
|
+
address: info.address,
|
|
1454
|
+
spend: {
|
|
1455
|
+
amount: formatUnits2(info.amountIn, info.inputTokenDecimals),
|
|
1456
|
+
raw: info.amountIn.toString(),
|
|
1457
|
+
symbol: info.inputTokenSymbol
|
|
1458
|
+
},
|
|
1459
|
+
estimated: {
|
|
1460
|
+
amount: formatUnits2(BigInt(info.amountOut), 18),
|
|
1461
|
+
raw: info.amountOut,
|
|
1462
|
+
symbol: info.coinSymbol
|
|
1463
|
+
},
|
|
1464
|
+
slippage: info.slippagePct
|
|
1465
|
+
});
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
console.log(`
|
|
1469
|
+
Buy ${info.coinName} (${info.coinSymbol})
|
|
1470
|
+
`);
|
|
1471
|
+
console.log(` Amount ${info.spendAmount}`);
|
|
1472
|
+
console.log(` You get ~${info.coinsFormatted} ${info.coinSymbol}`);
|
|
1473
|
+
console.log(` Slippage ${info.slippagePct}%
|
|
1474
|
+
`);
|
|
1475
|
+
};
|
|
1476
|
+
var printTradeResult = (json, info) => {
|
|
1477
|
+
const receivedAmount = formatUnits2(info.receivedAmountOut, 18);
|
|
1478
|
+
const receivedFormatted = formatCoinsDisplay(receivedAmount);
|
|
1479
|
+
if (json) {
|
|
1480
|
+
outputJson({
|
|
1481
|
+
action: "buy",
|
|
1482
|
+
coin: info.coinSymbol,
|
|
1483
|
+
address: info.address,
|
|
1484
|
+
spent: {
|
|
1485
|
+
amount: formatUnits2(info.amountIn, info.inputTokenDecimals),
|
|
1486
|
+
raw: info.amountIn.toString(),
|
|
1487
|
+
symbol: info.inputTokenSymbol
|
|
1488
|
+
},
|
|
1489
|
+
received: {
|
|
1490
|
+
amount: receivedAmount,
|
|
1491
|
+
raw: info.receivedAmountOut.toString(),
|
|
1492
|
+
symbol: info.coinSymbol
|
|
1493
|
+
},
|
|
1494
|
+
tx: info.txHash
|
|
1495
|
+
});
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
console.log(`
|
|
1499
|
+
Bought ${info.coinName}
|
|
1500
|
+
`);
|
|
1501
|
+
console.log(` Spent ${info.spendAmount} ${info.inputTokenSymbol}`);
|
|
1502
|
+
console.log(` Received ${receivedFormatted} ${info.coinSymbol}`);
|
|
1503
|
+
console.log(` Tx ${info.txHash}
|
|
1504
|
+
`);
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
// src/commands/buy.ts
|
|
1508
|
+
var buyCommand = new Command4("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").option("-o, --output <format>", "Output format: table, json", "table").action(async (coinAddress, opts) => {
|
|
1509
|
+
const json = opts.output === "json";
|
|
1510
|
+
const debug = opts.debug === true;
|
|
1511
|
+
if (!isAddress(coinAddress)) {
|
|
1512
|
+
outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
|
|
1513
|
+
}
|
|
1514
|
+
const output = opts.output;
|
|
1515
|
+
if (output !== "table" && output !== "json") {
|
|
1516
|
+
outputErrorAndExit(
|
|
1517
|
+
false,
|
|
1518
|
+
`Invalid --output value: ${output}. Use: table, json`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
const tokenKey = opts.token.toLowerCase();
|
|
1522
|
+
if (!(tokenKey in BASE_TRADE_TOKENS)) {
|
|
1523
|
+
outputErrorAndExit(
|
|
1524
|
+
json,
|
|
1525
|
+
`Invalid --token value: ${opts.token}. Use: eth, usdc, zora`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
const inputToken = BASE_TRADE_TOKENS[tokenKey];
|
|
1529
|
+
const amountMode = getAmountMode(
|
|
1530
|
+
json,
|
|
1531
|
+
opts,
|
|
1532
|
+
BUY_AMOUNT_CHECKS,
|
|
1533
|
+
"--eth, --usd, --percent, or --all"
|
|
1534
|
+
);
|
|
1535
|
+
const slippagePct = parsePercentageLikeValue(opts.slippage);
|
|
1536
|
+
if (slippagePct === void 0 || slippagePct < 0 || slippagePct > 99) {
|
|
1537
|
+
outputErrorAndExit(
|
|
1538
|
+
json,
|
|
1539
|
+
"Invalid --slippage value. Must be between 0 and 99."
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
const slippage = slippagePct / 100;
|
|
1543
|
+
const apiKey = getApiKey();
|
|
1544
|
+
if (apiKey) {
|
|
1545
|
+
setApiKey3(apiKey);
|
|
1546
|
+
}
|
|
1547
|
+
const account = resolveAccount(json);
|
|
1548
|
+
const { publicClient, walletClient } = createClients(account);
|
|
1549
|
+
let token;
|
|
1550
|
+
try {
|
|
1551
|
+
const response = await getCoin({ address: coinAddress });
|
|
1552
|
+
token = response.data?.zora20Token;
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
outputErrorAndExit(
|
|
1555
|
+
json,
|
|
1556
|
+
`Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
if (!token) {
|
|
1560
|
+
outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
|
|
1561
|
+
}
|
|
1562
|
+
const coinName = token.name;
|
|
1563
|
+
const coinSymbol = token.symbol;
|
|
1564
|
+
let amountIn;
|
|
1565
|
+
if (amountMode === "usd") {
|
|
1566
|
+
const usdVal = parsePercentageLikeValue(opts.usd);
|
|
1567
|
+
if (usdVal === void 0 || usdVal <= 0) {
|
|
1568
|
+
outputErrorAndExit(
|
|
1569
|
+
json,
|
|
1570
|
+
"Invalid --usd value. Must be a positive number."
|
|
1571
|
+
);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
let priceUsd;
|
|
1575
|
+
if (inputToken.fixedPriceUsd != null) {
|
|
1576
|
+
priceUsd = inputToken.fixedPriceUsd;
|
|
1577
|
+
} else {
|
|
1578
|
+
const fetched = await fetchTokenPriceUsd(inputToken.priceAddress);
|
|
1579
|
+
if (fetched === null) {
|
|
1580
|
+
outputErrorAndExit(
|
|
1581
|
+
json,
|
|
1582
|
+
`Failed to fetch ${inputToken.symbol} price.`
|
|
1583
|
+
);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
priceUsd = fetched;
|
|
1587
|
+
}
|
|
1588
|
+
const tokenAmount = usdVal / priceUsd;
|
|
1589
|
+
amountIn = parseUnits(
|
|
1590
|
+
tokenAmount.toFixed(inputToken.decimals),
|
|
1591
|
+
inputToken.decimals
|
|
1592
|
+
);
|
|
1593
|
+
if (amountIn === 0n) {
|
|
1594
|
+
outputErrorAndExit(json, "Calculated amount is zero. USD too small.");
|
|
1595
|
+
}
|
|
1596
|
+
if (debug) {
|
|
1597
|
+
console.error(
|
|
1598
|
+
`[debug] $${usdVal} USD = ${formatUnits3(amountIn, inputToken.decimals)} ${inputToken.symbol} (price: $${priceUsd})`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
} else if (amountMode === "eth") {
|
|
1602
|
+
const val = parsePercentageLikeValue(opts.eth);
|
|
1603
|
+
if (val === void 0 || val <= 0) {
|
|
1604
|
+
outputErrorAndExit(
|
|
1605
|
+
json,
|
|
1606
|
+
"Invalid --eth value. Must be a positive number."
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
try {
|
|
1610
|
+
amountIn = parseUnits(opts.eth, inputToken.decimals);
|
|
1611
|
+
} catch {
|
|
1612
|
+
outputErrorAndExit(
|
|
1613
|
+
json,
|
|
1614
|
+
"Invalid --eth value. Must be a positive number."
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
} else {
|
|
1618
|
+
const isEth = tokenKey === "eth";
|
|
1619
|
+
let balance;
|
|
1620
|
+
if (isEth) {
|
|
1621
|
+
balance = await publicClient.getBalance({
|
|
1622
|
+
address: account.address
|
|
1623
|
+
});
|
|
1624
|
+
} else {
|
|
1625
|
+
const tokenAddress = inputToken.trade.address;
|
|
1626
|
+
balance = await publicClient.readContract({
|
|
1627
|
+
address: tokenAddress,
|
|
1628
|
+
abi: [
|
|
1629
|
+
{
|
|
1630
|
+
name: "balanceOf",
|
|
1631
|
+
type: "function",
|
|
1632
|
+
stateMutability: "view",
|
|
1633
|
+
inputs: [{ name: "account", type: "address" }],
|
|
1634
|
+
outputs: [{ name: "", type: "uint256" }]
|
|
1635
|
+
}
|
|
1636
|
+
],
|
|
1637
|
+
functionName: "balanceOf",
|
|
1638
|
+
args: [account.address]
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
if (balance === 0n) {
|
|
1642
|
+
outputErrorAndExit(
|
|
1643
|
+
json,
|
|
1644
|
+
`No ${inputToken.symbol} balance. Deposit ${inputToken.symbol} to ${account.address} on Base.`
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const gasReserve = isEth ? GAS_RESERVE : 0n;
|
|
1648
|
+
if (isEth && balance <= gasReserve) {
|
|
1649
|
+
outputErrorAndExit(
|
|
1650
|
+
json,
|
|
1651
|
+
`Balance too low (${formatEthDisplay(balance)} ETH). Need >0.001 ETH for gas.`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
const spendableBalance = balance - gasReserve;
|
|
1655
|
+
if (amountMode === "all") {
|
|
1656
|
+
amountIn = spendableBalance;
|
|
1657
|
+
} else {
|
|
1658
|
+
const pct = parsePercentageLikeValue(opts.percent);
|
|
1659
|
+
if (pct === void 0 || pct <= 0 || pct > 100) {
|
|
1660
|
+
outputErrorAndExit(
|
|
1661
|
+
json,
|
|
1662
|
+
"Invalid --percent value. Must be between 0 and 100."
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
amountIn = pct === 100 ? spendableBalance : balance * BigInt(Math.round(pct * 100)) / 10000n;
|
|
1666
|
+
if (amountIn === 0n) {
|
|
1667
|
+
outputErrorAndExit(
|
|
1668
|
+
json,
|
|
1669
|
+
"Calculated amount is zero. Balance too low."
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
const tradeParameters = {
|
|
1675
|
+
sell: inputToken.trade,
|
|
1676
|
+
buy: { type: "erc20", address: coinAddress },
|
|
1677
|
+
amountIn,
|
|
1678
|
+
slippage,
|
|
1679
|
+
sender: account.address
|
|
1680
|
+
};
|
|
1681
|
+
if (debug) {
|
|
1682
|
+
printDebugRequest("buy", tradeParameters);
|
|
1683
|
+
}
|
|
1684
|
+
let amountOut;
|
|
1685
|
+
try {
|
|
1686
|
+
const quote = await createTradeCall(tradeParameters);
|
|
1687
|
+
if (debug) {
|
|
1688
|
+
printDebugResponse("buy", quote);
|
|
1689
|
+
}
|
|
1690
|
+
if (!quote.quote?.amountOut || quote.quote.amountOut === "0") {
|
|
1691
|
+
outputErrorAndExit(
|
|
1692
|
+
json,
|
|
1693
|
+
"Quote returned zero output. Amount may be too small."
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
amountOut = quote.quote.amountOut;
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
if (debug) {
|
|
1699
|
+
console.error(
|
|
1700
|
+
`
|
|
1701
|
+
[debug] buy \u2014 Quote Error:
|
|
1702
|
+
${err instanceof Error ? err.stack || err.message : String(err)}
|
|
1703
|
+
`
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1707
|
+
const errorType = err?.errorType;
|
|
1708
|
+
const errorBody = err?.errorBody;
|
|
1709
|
+
if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
|
|
1710
|
+
if (json) {
|
|
1711
|
+
outputJson({ error: errorBody ?? msg });
|
|
1712
|
+
process.exit(1);
|
|
1713
|
+
}
|
|
1714
|
+
outputErrorAndExit(
|
|
1715
|
+
json,
|
|
1716
|
+
"Not enough available liquidity for your swap. Please try swapping fewer tokens."
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
outputErrorAndExit(
|
|
1720
|
+
json,
|
|
1721
|
+
`Quote failed: ${msg}`,
|
|
1722
|
+
"Check the coin address is valid and try again. Use --debug for full error details."
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
const spendAmount = formatUnits3(amountIn, inputToken.decimals);
|
|
1726
|
+
const spendFormatted = new Intl.NumberFormat("en-US", {
|
|
1727
|
+
maximumFractionDigits: 6
|
|
1728
|
+
}).format(Number(spendAmount));
|
|
1729
|
+
const coinsOut = formatUnits3(BigInt(amountOut), 18);
|
|
1730
|
+
const coinsFormatted = formatCoinsDisplay(coinsOut);
|
|
1731
|
+
if (opts.quote) {
|
|
1732
|
+
printQuote(json, {
|
|
1733
|
+
coinName,
|
|
1734
|
+
coinSymbol,
|
|
1735
|
+
address: coinAddress,
|
|
1736
|
+
spendAmount: `${spendFormatted} ${inputToken.symbol}`,
|
|
1737
|
+
amountIn,
|
|
1738
|
+
inputTokenSymbol: inputToken.symbol,
|
|
1739
|
+
inputTokenDecimals: inputToken.decimals,
|
|
1740
|
+
coinsFormatted,
|
|
1741
|
+
amountOut,
|
|
1742
|
+
slippagePct
|
|
1743
|
+
});
|
|
1744
|
+
track("cli_buy", {
|
|
1745
|
+
action: "quote",
|
|
1746
|
+
coin_address: coinAddress,
|
|
1747
|
+
coin_name: coinName,
|
|
1748
|
+
coin_symbol: coinSymbol,
|
|
1749
|
+
amount_mode: amountMode,
|
|
1750
|
+
slippage: slippagePct,
|
|
1751
|
+
output_format: opts.output
|
|
1752
|
+
});
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (!opts.yes) {
|
|
1756
|
+
printQuote(false, {
|
|
1757
|
+
coinName,
|
|
1758
|
+
coinSymbol,
|
|
1759
|
+
address: coinAddress,
|
|
1760
|
+
spendAmount: `${spendFormatted} ${inputToken.symbol}`,
|
|
1761
|
+
amountIn,
|
|
1762
|
+
inputTokenSymbol: inputToken.symbol,
|
|
1763
|
+
inputTokenDecimals: inputToken.decimals,
|
|
1764
|
+
coinsFormatted,
|
|
1765
|
+
amountOut,
|
|
1766
|
+
slippagePct
|
|
1767
|
+
});
|
|
1768
|
+
const ok = await confirm2({
|
|
1769
|
+
message: "Confirm?",
|
|
1770
|
+
default: false
|
|
1771
|
+
});
|
|
1772
|
+
if (!ok) {
|
|
1773
|
+
process.exit(0);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
let receipt;
|
|
1777
|
+
let txHash;
|
|
1778
|
+
let receivedAmountOut = BigInt(amountOut);
|
|
1779
|
+
try {
|
|
1780
|
+
receipt = await tradeCoin({
|
|
1781
|
+
tradeParameters,
|
|
1782
|
+
walletClient,
|
|
1783
|
+
publicClient,
|
|
1784
|
+
account
|
|
1785
|
+
});
|
|
1786
|
+
} catch (err) {
|
|
1787
|
+
track("cli_buy", {
|
|
1788
|
+
action: "trade",
|
|
1789
|
+
coin_address: coinAddress,
|
|
1790
|
+
coin_name: coinName,
|
|
1791
|
+
coin_symbol: coinSymbol,
|
|
1792
|
+
amount_mode: amountMode,
|
|
1793
|
+
slippage: slippagePct,
|
|
1794
|
+
output_format: opts.output,
|
|
1795
|
+
success: false,
|
|
1796
|
+
error_type: err instanceof Error ? err.constructor.name : "unknown"
|
|
1797
|
+
});
|
|
1798
|
+
await shutdownAnalytics();
|
|
1799
|
+
outputErrorAndExit(
|
|
1800
|
+
json,
|
|
1801
|
+
`Transaction failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
txHash = receipt.transactionHash;
|
|
1805
|
+
try {
|
|
1806
|
+
receivedAmountOut = getReceivedAmountFromReceipt({
|
|
1807
|
+
receipt,
|
|
1808
|
+
tokenAddress: coinAddress,
|
|
1809
|
+
recipient: account.address
|
|
1810
|
+
});
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
console.warn(
|
|
1813
|
+
`Warning: transaction succeeded but could not determine received amount: ${err instanceof Error ? err.message : String(err)}`
|
|
1814
|
+
);
|
|
1815
|
+
console.warn(`Tx: ${txHash}`);
|
|
1816
|
+
}
|
|
1817
|
+
printTradeResult(json, {
|
|
1818
|
+
coinName,
|
|
1819
|
+
coinSymbol,
|
|
1820
|
+
address: coinAddress,
|
|
1821
|
+
spendAmount,
|
|
1822
|
+
amountIn,
|
|
1823
|
+
inputTokenSymbol: inputToken.symbol,
|
|
1824
|
+
inputTokenDecimals: inputToken.decimals,
|
|
1825
|
+
receivedAmountOut,
|
|
1826
|
+
txHash
|
|
1827
|
+
});
|
|
1828
|
+
track("cli_buy", {
|
|
1829
|
+
action: "trade",
|
|
1830
|
+
coin_address: coinAddress,
|
|
1831
|
+
coin_name: coinName,
|
|
1832
|
+
coin_symbol: coinSymbol,
|
|
1833
|
+
amount_mode: amountMode,
|
|
1834
|
+
input_amount: amountIn.toString(),
|
|
1835
|
+
input_token_symbol: inputToken.symbol,
|
|
1836
|
+
slippage: slippagePct,
|
|
1837
|
+
output_format: opts.output,
|
|
1838
|
+
success: true,
|
|
1839
|
+
tx_hash: txHash
|
|
1840
|
+
});
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
// src/commands/get.tsx
|
|
1844
|
+
import { Command as Command5 } from "commander";
|
|
1845
|
+
import { setApiKey as setApiKey4 } from "@zoralabs/coins-sdk";
|
|
1846
|
+
|
|
1847
|
+
// src/lib/coin-ref.ts
|
|
1848
|
+
import { getCoin as getCoin2, getProfile, getTrend } from "@zoralabs/coins-sdk";
|
|
1849
|
+
var COIN_TYPE_MAP = {
|
|
1850
|
+
CONTENT: "post",
|
|
1851
|
+
CREATOR: "creator-coin",
|
|
1852
|
+
TREND: "trend"
|
|
1853
|
+
};
|
|
1854
|
+
function mapCoinType(raw) {
|
|
1855
|
+
if (!raw) return "unknown";
|
|
1856
|
+
return COIN_TYPE_MAP[raw] ?? "unknown";
|
|
1857
|
+
}
|
|
1858
|
+
function coinFromToken(token) {
|
|
1859
|
+
return {
|
|
1860
|
+
name: token.name ?? "Unknown",
|
|
1861
|
+
address: token.address ?? "",
|
|
1862
|
+
coinType: mapCoinType(token.coinType),
|
|
1863
|
+
marketCap: token.marketCap ?? "0",
|
|
1864
|
+
marketCapDelta24h: token.marketCapDelta24h ?? "0",
|
|
1865
|
+
volume24h: token.volume24h ?? "0",
|
|
1866
|
+
uniqueHolders: token.uniqueHolders ?? 0,
|
|
1867
|
+
createdAt: token.createdAt,
|
|
1868
|
+
creatorAddress: token.creatorAddress,
|
|
1869
|
+
creatorHandle: token.creatorProfile?.handle
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function parseCoinRef(identifier, type) {
|
|
1873
|
+
if (identifier.startsWith("0x")) {
|
|
1874
|
+
return { kind: "address", address: identifier };
|
|
1875
|
+
}
|
|
1876
|
+
if (type === "creator-coin") {
|
|
1877
|
+
return { kind: "prefixed", type: "creator-coin", name: identifier };
|
|
1878
|
+
}
|
|
1879
|
+
if (type === "trend") {
|
|
1880
|
+
return { kind: "prefixed", type: "trend", name: identifier };
|
|
1881
|
+
}
|
|
1882
|
+
return { kind: "ambiguous", name: identifier };
|
|
1883
|
+
}
|
|
1884
|
+
async function resolveByAddress(address) {
|
|
1885
|
+
const response = await getCoin2({ address });
|
|
1886
|
+
if (response.error || !response.data?.zora20Token) {
|
|
1887
|
+
return {
|
|
1888
|
+
kind: "not-found",
|
|
1889
|
+
message: `No coin found at address ${address}`
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
return { kind: "found", coin: coinFromToken(response.data.zora20Token) };
|
|
1893
|
+
}
|
|
1894
|
+
async function resolveByTrendTicker(ticker) {
|
|
1895
|
+
const response = await getTrend({ ticker });
|
|
1896
|
+
if (response.error || !response.data?.trendCoin) {
|
|
1897
|
+
return {
|
|
1898
|
+
kind: "not-found",
|
|
1899
|
+
message: `No trend coin found with ticker "${ticker}"`
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
return { kind: "found", coin: coinFromToken(response.data.trendCoin) };
|
|
1903
|
+
}
|
|
1904
|
+
async function resolveByCreatorName(name) {
|
|
1905
|
+
const response = await getProfile({ identifier: name });
|
|
1906
|
+
if (response.error || !response.data?.profile) {
|
|
1907
|
+
return {
|
|
1908
|
+
kind: "not-found",
|
|
1909
|
+
message: `No creator found with name "${name}"`
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
const profile = response.data.profile;
|
|
1913
|
+
if (!profile.creatorCoin) {
|
|
1914
|
+
return {
|
|
1915
|
+
kind: "not-found",
|
|
1916
|
+
message: `"${name}" does not have a creator coin`
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
return resolveByAddress(profile.creatorCoin.address);
|
|
1920
|
+
}
|
|
1921
|
+
async function resolveCoin(ref) {
|
|
1922
|
+
switch (ref.kind) {
|
|
1923
|
+
case "address":
|
|
1924
|
+
return resolveByAddress(ref.address);
|
|
1925
|
+
case "prefixed":
|
|
1926
|
+
if (ref.type === "trend") {
|
|
1927
|
+
return resolveByTrendTicker(ref.name);
|
|
1928
|
+
}
|
|
1929
|
+
return resolveByCreatorName(ref.name);
|
|
1930
|
+
case "ambiguous":
|
|
1931
|
+
return resolveByCreatorName(ref.name);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// src/components/CoinDetail.tsx
|
|
1936
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
1937
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1938
|
+
var LABEL_WIDTH = 18;
|
|
1939
|
+
function Row({
|
|
1940
|
+
label,
|
|
1941
|
+
children
|
|
1942
|
+
}) {
|
|
1943
|
+
return /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
1944
|
+
/* @__PURE__ */ jsx3(Box3, { width: LABEL_WIDTH, flexShrink: 0, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: label }) }),
|
|
1945
|
+
/* @__PURE__ */ jsx3(Text3, { children })
|
|
1946
|
+
] });
|
|
1947
|
+
}
|
|
1948
|
+
function CoinDetail({ coin }) {
|
|
1949
|
+
const change = formatMcapChange(coin.marketCap, coin.marketCapDelta24h);
|
|
1950
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingLeft: 1, children: [
|
|
1951
|
+
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
1952
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: coin.name }),
|
|
1953
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1954
|
+
coin.coinType,
|
|
1955
|
+
" ",
|
|
1956
|
+
"\xB7",
|
|
1957
|
+
" ",
|
|
1958
|
+
coin.address
|
|
1959
|
+
] })
|
|
1960
|
+
] }),
|
|
1961
|
+
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
1962
|
+
/* @__PURE__ */ jsx3(Row, { label: "Market Cap", children: formatCurrency(coin.marketCap) }),
|
|
1963
|
+
/* @__PURE__ */ jsx3(Row, { label: "24h Volume", children: formatCurrency(coin.volume24h) }),
|
|
1964
|
+
/* @__PURE__ */ jsx3(Row, { label: "24h Change", children: /* @__PURE__ */ jsx3(Text3, { color: change.color, children: change.text }) }),
|
|
1965
|
+
/* @__PURE__ */ jsx3(Row, { label: "Holders", children: formatHolders(coin.uniqueHolders) }),
|
|
1966
|
+
coin.coinType === "post" && (coin.creatorHandle ?? coin.creatorAddress) && /* @__PURE__ */ jsx3(Row, { label: "Creator", children: coin.creatorHandle ?? coin.creatorAddress }),
|
|
1967
|
+
/* @__PURE__ */ jsx3(Row, { label: "Created", children: formatCreatedAt(coin.createdAt) })
|
|
1968
|
+
] }),
|
|
1969
|
+
/* @__PURE__ */ jsx3(Box3, { marginBottom: 1 })
|
|
1970
|
+
] });
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// src/commands/get.tsx
|
|
1974
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1975
|
+
function formatCoinJson(coin) {
|
|
1976
|
+
return {
|
|
1977
|
+
name: coin.name,
|
|
1978
|
+
address: coin.address,
|
|
1979
|
+
coinType: coin.coinType,
|
|
1980
|
+
marketCap: coin.marketCap,
|
|
1981
|
+
marketCapDelta24h: coin.marketCapDelta24h,
|
|
1982
|
+
volume24h: coin.volume24h,
|
|
1983
|
+
uniqueHolders: coin.uniqueHolders,
|
|
1984
|
+
createdAt: coin.createdAt ?? null,
|
|
1985
|
+
creatorAddress: coin.creatorAddress ?? null,
|
|
1986
|
+
creatorHandle: coin.creatorHandle ?? null
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
var VALID_TYPES = ["creator-coin", "post", "trend"];
|
|
1990
|
+
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) {
|
|
1991
|
+
const json = getJson(this);
|
|
1992
|
+
if (opts.type !== void 0 && !VALID_TYPES.includes(opts.type)) {
|
|
1993
|
+
outputErrorAndExit(
|
|
1994
|
+
json,
|
|
1995
|
+
`Invalid --type value: ${opts.type}.`,
|
|
1996
|
+
`Supported: ${VALID_TYPES.join(", ")}`
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
const type = opts.type;
|
|
2000
|
+
if (type === "post" && !identifier.startsWith("0x")) {
|
|
2001
|
+
outputErrorAndExit(
|
|
2002
|
+
json,
|
|
2003
|
+
"Posts can only be looked up by address.",
|
|
2004
|
+
"Use: zora get 0x..."
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
const ref = parseCoinRef(identifier, opts.type);
|
|
2008
|
+
const apiKey = getApiKey();
|
|
2009
|
+
if (apiKey) {
|
|
2010
|
+
setApiKey4(apiKey);
|
|
2011
|
+
}
|
|
2012
|
+
let result;
|
|
2013
|
+
try {
|
|
2014
|
+
result = await resolveCoin(ref);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
outputErrorAndExit(
|
|
2017
|
+
json,
|
|
2018
|
+
`Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2019
|
+
);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
if (type && result.kind === "found" && result.coin.coinType !== type) {
|
|
2023
|
+
outputErrorAndExit(
|
|
2024
|
+
json,
|
|
2025
|
+
`Coin at ${result.coin.address} is a ${result.coin.coinType}, not a ${type}.`,
|
|
2026
|
+
`Use: zora get ${result.coin.address} --type ${result.coin.coinType}`
|
|
2027
|
+
);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
if (result.kind === "not-found") {
|
|
2031
|
+
outputErrorAndExit(json, result.message);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
outputData(json, {
|
|
2035
|
+
json: formatCoinJson(result.coin),
|
|
2036
|
+
table: () => {
|
|
2037
|
+
renderOnce(/* @__PURE__ */ jsx4(CoinDetail, { coin: result.coin }));
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
track("cli_get", {
|
|
2041
|
+
lookup_type: identifier.startsWith("0x") ? "address" : "name",
|
|
2042
|
+
coin_type_filter: type ?? null,
|
|
2043
|
+
found: result.kind === "found",
|
|
2044
|
+
coin_type: result.kind === "found" ? result.coin.coinType : null,
|
|
2045
|
+
output_format: json ? "json" : "text"
|
|
2046
|
+
});
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
// src/commands/sell.ts
|
|
2050
|
+
import { Command as Command6 } from "commander";
|
|
2051
|
+
import confirm3 from "@inquirer/confirm";
|
|
2052
|
+
import {
|
|
2053
|
+
erc20Abi as erc20Abi3,
|
|
2054
|
+
formatUnits as formatUnits4,
|
|
2055
|
+
isAddress as isAddress2,
|
|
2056
|
+
parseUnits as parseUnits2
|
|
2057
|
+
} from "viem";
|
|
2058
|
+
import {
|
|
2059
|
+
createTradeCall as createTradeCall2,
|
|
2060
|
+
getCoin as getCoin3,
|
|
2061
|
+
setApiKey as setApiKey5,
|
|
2062
|
+
tradeCoin as tradeCoin2
|
|
2063
|
+
} from "@zoralabs/coins-sdk";
|
|
2064
|
+
function printSellQuote(output, info) {
|
|
2065
|
+
if (output === "json") {
|
|
2066
|
+
outputJson({
|
|
2067
|
+
action: "quote",
|
|
2068
|
+
coin: info.coinSymbol,
|
|
2069
|
+
address: info.address,
|
|
2070
|
+
sell: {
|
|
2071
|
+
amount: formatUnits4(info.amountIn, info.coinDecimals),
|
|
2072
|
+
raw: info.amountIn.toString(),
|
|
2073
|
+
symbol: info.coinSymbol
|
|
2074
|
+
},
|
|
2075
|
+
estimated: {
|
|
2076
|
+
amount: formatUnits4(BigInt(info.quoteAmountOut), info.outputDecimals),
|
|
2077
|
+
raw: info.quoteAmountOut,
|
|
2078
|
+
symbol: info.outputSymbol
|
|
2079
|
+
},
|
|
2080
|
+
slippage: info.slippagePct
|
|
2081
|
+
});
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
console.log(`
|
|
2085
|
+
Sell ${info.coinName} (${info.coinSymbol})
|
|
2086
|
+
`);
|
|
2087
|
+
console.log(` Amount ${info.soldFormatted} ${info.coinSymbol}`);
|
|
2088
|
+
console.log(
|
|
2089
|
+
` You get ~${info.receivedFormatted} ${info.outputSymbol}`
|
|
2090
|
+
);
|
|
2091
|
+
console.log(` Slippage ${info.slippagePct}%
|
|
2092
|
+
`);
|
|
2093
|
+
}
|
|
2094
|
+
function printSellResult(output, info) {
|
|
2095
|
+
const receivedAmount = formatUnits4(
|
|
2096
|
+
info.receivedAmountOut,
|
|
2097
|
+
info.outputDecimals
|
|
2098
|
+
);
|
|
2099
|
+
const receivedFormatted = formatAmountDisplay(
|
|
2100
|
+
info.receivedAmountOut,
|
|
2101
|
+
info.outputDecimals
|
|
2102
|
+
);
|
|
2103
|
+
if (output === "json") {
|
|
2104
|
+
outputJson({
|
|
2105
|
+
action: "sell",
|
|
2106
|
+
coin: info.coinSymbol,
|
|
2107
|
+
address: info.address,
|
|
2108
|
+
sold: {
|
|
2109
|
+
amount: formatUnits4(info.amountIn, info.coinDecimals),
|
|
2110
|
+
raw: info.amountIn.toString(),
|
|
2111
|
+
symbol: info.coinSymbol
|
|
2112
|
+
},
|
|
2113
|
+
received: {
|
|
2114
|
+
amount: receivedAmount,
|
|
2115
|
+
raw: info.receivedAmountOut.toString(),
|
|
2116
|
+
symbol: info.outputSymbol,
|
|
2117
|
+
source: info.receivedSource
|
|
2118
|
+
},
|
|
2119
|
+
tx: info.txHash
|
|
2120
|
+
});
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
console.log(`
|
|
2124
|
+
Sold ${info.coinName}
|
|
2125
|
+
`);
|
|
2126
|
+
console.log(` Sold ${info.soldFormatted} ${info.coinSymbol}`);
|
|
2127
|
+
console.log(
|
|
2128
|
+
` Received ${info.receivedSource === "quote" ? "~" : ""}${receivedFormatted} ${info.outputSymbol}`
|
|
2129
|
+
);
|
|
2130
|
+
if (info.receivedSource === "quote") {
|
|
2131
|
+
console.log(" Note based on quote");
|
|
2132
|
+
}
|
|
2133
|
+
console.log(` Tx ${info.txHash}
|
|
2134
|
+
`);
|
|
2135
|
+
}
|
|
2136
|
+
var sellCommand = new Command6("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").option("-o, --output <format>", "Output format: table, json", "table").action(async (coinAddress, opts) => {
|
|
2137
|
+
const json = opts.output === "json";
|
|
2138
|
+
const debug = opts.debug === true;
|
|
2139
|
+
if (!isAddress2(coinAddress)) {
|
|
2140
|
+
outputErrorAndExit(json, `Invalid address: ${coinAddress}`);
|
|
2141
|
+
}
|
|
2142
|
+
const output = opts.output;
|
|
2143
|
+
if (output !== "table" && output !== "json") {
|
|
2144
|
+
outputErrorAndExit(
|
|
2145
|
+
false,
|
|
2146
|
+
`Invalid --output value: ${output}. Use: table, json`
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
const outputAsset = opts.token ? opts.token.toLowerCase() : opts.to;
|
|
2150
|
+
if (!(outputAsset in BASE_TRADE_TOKENS)) {
|
|
2151
|
+
outputErrorAndExit(
|
|
2152
|
+
json,
|
|
2153
|
+
`Invalid --${opts.token ? "token" : "to"} value: ${outputAsset}. Use: eth, usdc, zora`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
const outputToken = BASE_TRADE_TOKENS[outputAsset];
|
|
2157
|
+
const amountMode = getAmountMode(
|
|
2158
|
+
json,
|
|
2159
|
+
opts,
|
|
2160
|
+
SELL_AMOUNT_CHECKS,
|
|
2161
|
+
"--amount, --usd, --percent, or --all"
|
|
2162
|
+
);
|
|
2163
|
+
const slippagePct = parsePercentageLikeValue(opts.slippage);
|
|
2164
|
+
if (slippagePct === void 0 || slippagePct < 0 || slippagePct > 99) {
|
|
2165
|
+
outputErrorAndExit(
|
|
2166
|
+
json,
|
|
2167
|
+
"Invalid --slippage value. Must be between 0 and 99."
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
const slippage = slippagePct / 100;
|
|
2171
|
+
const apiKey = getApiKey();
|
|
2172
|
+
if (apiKey) {
|
|
2173
|
+
setApiKey5(apiKey);
|
|
2174
|
+
}
|
|
2175
|
+
const account = resolveAccount(json);
|
|
2176
|
+
const { publicClient, walletClient } = createClients(account);
|
|
2177
|
+
let token;
|
|
2178
|
+
try {
|
|
2179
|
+
const response = await getCoin3({ address: coinAddress });
|
|
2180
|
+
token = response.data?.zora20Token;
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
outputErrorAndExit(
|
|
2183
|
+
json,
|
|
2184
|
+
`Failed to fetch coin: ${err instanceof Error ? err.message : String(err)}`
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
if (!token) {
|
|
2188
|
+
outputErrorAndExit(json, `Coin not found: ${coinAddress}`);
|
|
2189
|
+
}
|
|
2190
|
+
const coinName = token.name;
|
|
2191
|
+
const coinSymbol = token.symbol;
|
|
2192
|
+
const coinDecimals = Number(token.decimals ?? 18);
|
|
2193
|
+
let amountIn;
|
|
2194
|
+
if (amountMode === "usd") {
|
|
2195
|
+
const usdVal = parsePercentageLikeValue(opts.usd);
|
|
2196
|
+
if (usdVal === void 0 || usdVal <= 0) {
|
|
2197
|
+
outputErrorAndExit(
|
|
2198
|
+
json,
|
|
2199
|
+
"Invalid --usd value. Must be a positive number."
|
|
2200
|
+
);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const coinPriceUsd = await fetchTokenPriceUsd(coinAddress);
|
|
2204
|
+
if (coinPriceUsd === null || coinPriceUsd <= 0) {
|
|
2205
|
+
outputErrorAndExit(
|
|
2206
|
+
json,
|
|
2207
|
+
`Failed to fetch ${coinSymbol} price for USD conversion.`
|
|
2208
|
+
);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
const coinAmount = usdVal / coinPriceUsd;
|
|
2212
|
+
amountIn = parseUnits2(coinAmount.toFixed(coinDecimals), coinDecimals);
|
|
2213
|
+
if (amountIn === 0n) {
|
|
2214
|
+
outputErrorAndExit(json, "Calculated amount is zero. USD too small.");
|
|
2215
|
+
}
|
|
2216
|
+
if (debug) {
|
|
2217
|
+
console.error(
|
|
2218
|
+
`[debug] $${usdVal} USD = ${formatUnits4(amountIn, coinDecimals)} ${coinSymbol} (coin price: $${coinPriceUsd})`
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
} else if (amountMode === "amount") {
|
|
2222
|
+
const val = parsePercentageLikeValue(opts.amount);
|
|
2223
|
+
if (val === void 0 || val <= 0) {
|
|
2224
|
+
outputErrorAndExit(
|
|
2225
|
+
json,
|
|
2226
|
+
"Invalid --amount value. Must be a positive number."
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
try {
|
|
2230
|
+
amountIn = parseUnits2(opts.amount, coinDecimals);
|
|
2231
|
+
} catch {
|
|
2232
|
+
outputErrorAndExit(json, "Invalid --amount value for token decimals.");
|
|
2233
|
+
}
|
|
2234
|
+
} else {
|
|
2235
|
+
const balance = await publicClient.readContract({
|
|
2236
|
+
abi: erc20Abi3,
|
|
2237
|
+
address: coinAddress,
|
|
2238
|
+
functionName: "balanceOf",
|
|
2239
|
+
args: [account.address]
|
|
2240
|
+
});
|
|
2241
|
+
if (balance === 0n) {
|
|
2242
|
+
outputErrorAndExit(
|
|
2243
|
+
json,
|
|
2244
|
+
`No ${coinSymbol} balance. Buy some first or pick a different wallet.`
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
if (amountMode === "all") {
|
|
2248
|
+
amountIn = balance;
|
|
2249
|
+
} else {
|
|
2250
|
+
const pct = parsePercentageLikeValue(opts.percent);
|
|
2251
|
+
if (pct === void 0 || pct <= 0 || pct > 100) {
|
|
2252
|
+
outputErrorAndExit(
|
|
2253
|
+
json,
|
|
2254
|
+
"Invalid --percent value. Must be between 0 and 100."
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
amountIn = pct === 100 ? balance : balance * BigInt(Math.round(pct * 100)) / 10000n;
|
|
2258
|
+
if (amountIn === 0n) {
|
|
2259
|
+
outputErrorAndExit(
|
|
2260
|
+
json,
|
|
2261
|
+
"Calculated amount is zero. Balance too low."
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
const tradeParameters = {
|
|
2267
|
+
sell: { type: "erc20", address: coinAddress },
|
|
2268
|
+
buy: outputToken.trade,
|
|
2269
|
+
amountIn,
|
|
2270
|
+
slippage,
|
|
2271
|
+
sender: account.address
|
|
2272
|
+
};
|
|
2273
|
+
if (debug) {
|
|
2274
|
+
printDebugRequest("sell", tradeParameters);
|
|
2275
|
+
}
|
|
2276
|
+
let quoteAmountOut;
|
|
2277
|
+
try {
|
|
2278
|
+
const quote = await createTradeCall2(tradeParameters);
|
|
2279
|
+
if (debug) {
|
|
2280
|
+
printDebugResponse("sell", quote);
|
|
2281
|
+
}
|
|
2282
|
+
if (!quote.quote?.amountOut || quote.quote.amountOut === "0") {
|
|
2283
|
+
outputErrorAndExit(
|
|
2284
|
+
json,
|
|
2285
|
+
"Quote returned zero output. Amount may be too small."
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
quoteAmountOut = quote.quote.amountOut;
|
|
2289
|
+
} catch (err) {
|
|
2290
|
+
if (debug) {
|
|
2291
|
+
console.error(
|
|
2292
|
+
`
|
|
2293
|
+
[debug] sell \u2014 Quote Error:
|
|
2294
|
+
${err instanceof Error ? err.stack || err.message : String(err)}
|
|
2295
|
+
`
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2299
|
+
const errorType = err?.errorType;
|
|
2300
|
+
const errorBody = err?.errorBody;
|
|
2301
|
+
if (errorType === "LIQUIDITY" || msg.includes("Not enough liquidity")) {
|
|
2302
|
+
if (json) {
|
|
2303
|
+
outputJson({ error: errorBody ?? msg });
|
|
2304
|
+
process.exit(1);
|
|
2305
|
+
}
|
|
2306
|
+
outputErrorAndExit(
|
|
2307
|
+
json,
|
|
2308
|
+
"Not enough available liquidity for your swap. Please try swapping fewer tokens."
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
outputErrorAndExit(
|
|
2312
|
+
json,
|
|
2313
|
+
`Quote failed: ${msg}`,
|
|
2314
|
+
"Check the coin address and amount, then try again. Use --debug for full error details."
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
const soldFormatted = formatAmountDisplay(amountIn, coinDecimals);
|
|
2318
|
+
const receivedFormatted = formatAmountDisplay(
|
|
2319
|
+
BigInt(quoteAmountOut),
|
|
2320
|
+
outputToken.decimals
|
|
2321
|
+
);
|
|
2322
|
+
if (opts.quote) {
|
|
2323
|
+
printSellQuote(output, {
|
|
2324
|
+
coinName,
|
|
2325
|
+
coinSymbol,
|
|
2326
|
+
address: coinAddress,
|
|
2327
|
+
soldFormatted,
|
|
2328
|
+
amountIn,
|
|
2329
|
+
coinDecimals,
|
|
2330
|
+
receivedFormatted,
|
|
2331
|
+
quoteAmountOut,
|
|
2332
|
+
outputSymbol: outputToken.symbol,
|
|
2333
|
+
outputDecimals: outputToken.decimals,
|
|
2334
|
+
slippagePct
|
|
2335
|
+
});
|
|
2336
|
+
track("cli_sell", {
|
|
2337
|
+
action: "quote",
|
|
2338
|
+
coin_address: coinAddress,
|
|
2339
|
+
coin_name: coinName,
|
|
2340
|
+
coin_symbol: coinSymbol,
|
|
2341
|
+
amount_mode: amountMode,
|
|
2342
|
+
output_asset: outputAsset,
|
|
2343
|
+
slippage: slippagePct,
|
|
2344
|
+
output_format: output
|
|
2345
|
+
});
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (!opts.yes) {
|
|
2349
|
+
printSellQuote("table", {
|
|
2350
|
+
coinName,
|
|
2351
|
+
coinSymbol,
|
|
2352
|
+
address: coinAddress,
|
|
2353
|
+
soldFormatted,
|
|
2354
|
+
amountIn,
|
|
2355
|
+
coinDecimals,
|
|
2356
|
+
receivedFormatted,
|
|
2357
|
+
quoteAmountOut,
|
|
2358
|
+
outputSymbol: outputToken.symbol,
|
|
2359
|
+
outputDecimals: outputToken.decimals,
|
|
2360
|
+
slippagePct
|
|
2361
|
+
});
|
|
2362
|
+
const ok = await confirm3({
|
|
2363
|
+
message: "Confirm?",
|
|
2364
|
+
default: false
|
|
2365
|
+
});
|
|
2366
|
+
if (!ok) {
|
|
2367
|
+
console.error("Aborted.");
|
|
2368
|
+
process.exit(0);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
let receipt;
|
|
2372
|
+
let txHash;
|
|
2373
|
+
let receivedAmountOut = BigInt(quoteAmountOut);
|
|
2374
|
+
let receivedSource = "quote";
|
|
2375
|
+
try {
|
|
2376
|
+
receipt = await tradeCoin2({
|
|
2377
|
+
tradeParameters,
|
|
2378
|
+
walletClient,
|
|
2379
|
+
publicClient,
|
|
2380
|
+
account
|
|
2381
|
+
});
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
track("cli_sell", {
|
|
2384
|
+
action: "trade",
|
|
2385
|
+
coin_address: coinAddress,
|
|
2386
|
+
coin_name: coinName,
|
|
2387
|
+
coin_symbol: coinSymbol,
|
|
2388
|
+
amount_mode: amountMode,
|
|
2389
|
+
output_asset: outputAsset,
|
|
2390
|
+
slippage: slippagePct,
|
|
2391
|
+
output_format: output,
|
|
2392
|
+
success: false,
|
|
2393
|
+
error_type: err instanceof Error ? err.constructor.name : "unknown"
|
|
2394
|
+
});
|
|
2395
|
+
await shutdownAnalytics();
|
|
2396
|
+
outputErrorAndExit(
|
|
2397
|
+
json,
|
|
2398
|
+
`Transaction failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
txHash = receipt.transactionHash;
|
|
2402
|
+
if (outputToken.trade.type === "erc20") {
|
|
2403
|
+
try {
|
|
2404
|
+
receivedAmountOut = getReceivedAmountFromReceipt({
|
|
2405
|
+
receipt,
|
|
2406
|
+
tokenAddress: outputToken.trade.address,
|
|
2407
|
+
recipient: account.address
|
|
2408
|
+
});
|
|
2409
|
+
receivedSource = "receipt";
|
|
2410
|
+
} catch {
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
printSellResult(output, {
|
|
2414
|
+
coinName,
|
|
2415
|
+
coinSymbol,
|
|
2416
|
+
address: coinAddress,
|
|
2417
|
+
amountIn,
|
|
2418
|
+
coinDecimals,
|
|
2419
|
+
soldFormatted,
|
|
2420
|
+
receivedAmountOut,
|
|
2421
|
+
outputSymbol: outputToken.symbol,
|
|
2422
|
+
outputDecimals: outputToken.decimals,
|
|
2423
|
+
receivedSource,
|
|
2424
|
+
txHash
|
|
2425
|
+
});
|
|
2426
|
+
track("cli_sell", {
|
|
2427
|
+
action: "trade",
|
|
2428
|
+
coin_address: coinAddress,
|
|
2429
|
+
coin_name: coinName,
|
|
2430
|
+
coin_symbol: coinSymbol,
|
|
2431
|
+
amount_mode: amountMode,
|
|
2432
|
+
output_asset: outputAsset,
|
|
2433
|
+
slippage: slippagePct,
|
|
2434
|
+
output_format: output,
|
|
2435
|
+
success: true,
|
|
2436
|
+
tx_hash: txHash
|
|
2437
|
+
});
|
|
2438
|
+
});
|
|
2439
|
+
|
|
2440
|
+
// src/commands/setup.ts
|
|
2441
|
+
import { Command as Command7 } from "commander";
|
|
2442
|
+
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
2443
|
+
|
|
2444
|
+
// src/lib/strings.ts
|
|
2445
|
+
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";
|
|
2446
|
+
var NO_WALLET_CONFIGURED = "No wallet configured.";
|
|
2447
|
+
var NO_WALLET_SUGGESTION = "Run 'zora setup' to create or import one.";
|
|
2448
|
+
var SAVE_ERROR_HINT = "Check that the directory exists and is writable.";
|
|
2449
|
+
var BACKUP_WARNING = "Back up this file \u2014 it's the only copy of your key.";
|
|
2450
|
+
|
|
2451
|
+
// src/commands/setup.ts
|
|
2452
|
+
var isValidPrivateKey = (key) => /^(0x)?[0-9a-fA-F]{64}$/.test(key);
|
|
2453
|
+
var toAccount = (json, key, errorPrefix) => {
|
|
2454
|
+
try {
|
|
2455
|
+
return privateKeyToAccount3(normalizeKey(key));
|
|
2456
|
+
} catch {
|
|
2457
|
+
outputErrorAndExit(
|
|
2458
|
+
json,
|
|
2459
|
+
`\u2717 ${errorPrefix} isn't a valid private key.`
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
var setupCommand = new Command7("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) {
|
|
2464
|
+
const json = getJson(this);
|
|
2465
|
+
const nonInteractive = getYes(this);
|
|
2466
|
+
const envKey = process.env.ZORA_PRIVATE_KEY;
|
|
2467
|
+
if (envKey !== void 0) {
|
|
2468
|
+
if (!isValidPrivateKey(envKey)) {
|
|
2469
|
+
outputErrorAndExit(
|
|
2470
|
+
json,
|
|
2471
|
+
"\u2717 ZORA_PRIVATE_KEY isn't a valid private key.",
|
|
2472
|
+
"Fix it and run zora setup again."
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
const account = toAccount(json, envKey, "ZORA_PRIVATE_KEY");
|
|
2476
|
+
outputData(json, {
|
|
2477
|
+
json: { source: "env", address: account.address },
|
|
2478
|
+
table: () => {
|
|
2479
|
+
console.log(" Using wallet from ZORA_PRIVATE_KEY.\n");
|
|
2480
|
+
console.log(` Address: ${account.address}
|
|
2481
|
+
`);
|
|
2482
|
+
console.log(` ${DEPOSIT_INSTRUCTIONS}`);
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
track("cli_setup", {
|
|
2486
|
+
action: "env_detected",
|
|
2487
|
+
source: "env",
|
|
2488
|
+
output_format: json ? "json" : "text"
|
|
2489
|
+
});
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
let existing;
|
|
2493
|
+
if (!options.force) {
|
|
2494
|
+
try {
|
|
2495
|
+
existing = getPrivateKey();
|
|
2496
|
+
} catch (err) {
|
|
2497
|
+
outputErrorAndExit(
|
|
2498
|
+
json,
|
|
2499
|
+
`\u2717 Could not read wallet: ${err.message}`,
|
|
2500
|
+
"Run 'zora setup --force' to overwrite it."
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
if (existing) {
|
|
2505
|
+
const account = toAccount(json, existing, "Stored private key");
|
|
2506
|
+
const truncated = `${account.address.slice(0, 6)}\u2026${account.address.slice(-4)}`;
|
|
2507
|
+
console.log(` Wallet already configured: ${truncated}
|
|
2508
|
+
`);
|
|
2509
|
+
if (!options.force) {
|
|
2510
|
+
outputErrorAndExit(
|
|
2511
|
+
json,
|
|
2512
|
+
"Wallet already exists.",
|
|
2513
|
+
"Use --force to overwrite."
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
let choice;
|
|
2518
|
+
if (options.create) {
|
|
2519
|
+
choice = "create";
|
|
2520
|
+
} else {
|
|
2521
|
+
choice = await selectOrDefault(
|
|
2522
|
+
{
|
|
2523
|
+
message: "How do you want to set up your wallet?",
|
|
2524
|
+
choices: [
|
|
2525
|
+
{
|
|
2526
|
+
name: "Create a new wallet (recommended)",
|
|
2527
|
+
value: "create"
|
|
2528
|
+
},
|
|
2529
|
+
{ name: "Import a private key", value: "import" }
|
|
2530
|
+
],
|
|
2531
|
+
default: "create"
|
|
2532
|
+
},
|
|
2533
|
+
nonInteractive
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
if (choice === "import") {
|
|
2537
|
+
let importedKey;
|
|
2538
|
+
while (!importedKey) {
|
|
2539
|
+
const input = await passwordOrFail(
|
|
2540
|
+
json,
|
|
2541
|
+
{ message: "Paste your private key:" },
|
|
2542
|
+
nonInteractive
|
|
2543
|
+
);
|
|
2544
|
+
if (isValidPrivateKey(input.trim())) {
|
|
2545
|
+
importedKey = input.trim();
|
|
2546
|
+
} else {
|
|
2547
|
+
console.error(
|
|
2548
|
+
"\u2717 Not a valid private key. Must be 64 hex characters, with or without a 0x prefix.\n"
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
const account = toAccount(json, importedKey, "Imported key");
|
|
2553
|
+
try {
|
|
2554
|
+
savePrivateKey(importedKey);
|
|
2555
|
+
} catch {
|
|
2556
|
+
outputErrorAndExit(
|
|
2557
|
+
json,
|
|
2558
|
+
`\u2717 Couldn't save to ${getWalletPath()}.`,
|
|
2559
|
+
SAVE_ERROR_HINT
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
outputData(json, {
|
|
2563
|
+
json: {
|
|
2564
|
+
action: "imported",
|
|
2565
|
+
address: account.address,
|
|
2566
|
+
path: getWalletPath()
|
|
2567
|
+
},
|
|
2568
|
+
table: () => {
|
|
2569
|
+
console.log("\n\u2713 Wallet imported\n");
|
|
2570
|
+
console.log(` Address: ${account.address}`);
|
|
2571
|
+
console.log(` Private key: saved to ${getWalletPath()}
|
|
2572
|
+
`);
|
|
2573
|
+
console.log(` ${BACKUP_WARNING}
|
|
2574
|
+
`);
|
|
2575
|
+
console.log(` ${DEPOSIT_INSTRUCTIONS}`);
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
track("cli_setup", {
|
|
2579
|
+
action: "imported",
|
|
2580
|
+
source: "file",
|
|
2581
|
+
output_format: json ? "json" : "text"
|
|
2582
|
+
});
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
if (choice === "create") {
|
|
2586
|
+
const privateKey = generatePrivateKey();
|
|
2587
|
+
const account = toAccount(json, privateKey, "Generated key");
|
|
2588
|
+
try {
|
|
2589
|
+
savePrivateKey(privateKey);
|
|
2590
|
+
} catch {
|
|
2591
|
+
outputErrorAndExit(
|
|
2592
|
+
json,
|
|
2593
|
+
`\u2717 Couldn't save to ${getWalletPath()}.`,
|
|
2594
|
+
SAVE_ERROR_HINT
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
outputData(json, {
|
|
2598
|
+
json: {
|
|
2599
|
+
action: "created",
|
|
2600
|
+
address: account.address,
|
|
2601
|
+
path: getWalletPath()
|
|
2602
|
+
},
|
|
2603
|
+
table: () => {
|
|
2604
|
+
console.log("\n\u2713 Wallet created\n");
|
|
2605
|
+
console.log(` Address: ${account.address}`);
|
|
2606
|
+
console.log(` Private key: saved to ${getWalletPath()}
|
|
2607
|
+
`);
|
|
2608
|
+
console.log(` ${BACKUP_WARNING}
|
|
2609
|
+
`);
|
|
2610
|
+
console.log(` ${DEPOSIT_INSTRUCTIONS}`);
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
track("cli_setup", {
|
|
2614
|
+
action: "created",
|
|
2615
|
+
source: "file",
|
|
2616
|
+
output_format: json ? "json" : "text"
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
// src/commands/wallet.ts
|
|
2622
|
+
import { Command as Command8 } from "commander";
|
|
2623
|
+
import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
|
|
2624
|
+
var resolvePrivateKey = () => {
|
|
2625
|
+
const envKey = process.env.ZORA_PRIVATE_KEY;
|
|
2626
|
+
if (envKey) {
|
|
2627
|
+
return { key: envKey, source: "env" };
|
|
2628
|
+
}
|
|
2629
|
+
const fileKey = getPrivateKey();
|
|
2630
|
+
if (fileKey !== void 0) {
|
|
2631
|
+
return { key: fileKey, source: "file" };
|
|
2632
|
+
}
|
|
2633
|
+
return void 0;
|
|
2634
|
+
};
|
|
2635
|
+
var walletCommand = new Command8("wallet").description(
|
|
2636
|
+
"Manage your Zora wallet"
|
|
2637
|
+
);
|
|
2638
|
+
walletCommand.command("info").description("Show wallet address and storage location").action(function() {
|
|
2639
|
+
const json = getJson(this);
|
|
2640
|
+
const resolved = resolvePrivateKey();
|
|
2641
|
+
if (!resolved) {
|
|
2642
|
+
outputErrorAndExit(json, NO_WALLET_CONFIGURED, NO_WALLET_SUGGESTION);
|
|
2643
|
+
}
|
|
2644
|
+
let account;
|
|
2645
|
+
try {
|
|
2646
|
+
account = privateKeyToAccount4(normalizeKey(resolved.key));
|
|
2647
|
+
} catch {
|
|
2648
|
+
const msg = resolved.source === "env" ? "ZORA_PRIVATE_KEY is not a valid private key." : "Stored private key is invalid.";
|
|
2649
|
+
const suggestion = resolved.source === "env" ? void 0 : "Run 'zora setup --force' to replace it.";
|
|
2650
|
+
outputErrorAndExit(json, `\u2717 ${msg}`, suggestion);
|
|
2651
|
+
}
|
|
2652
|
+
const source = resolved.source === "env" ? "env (ZORA_PRIVATE_KEY)" : getWalletPath();
|
|
2653
|
+
outputData(json, {
|
|
2654
|
+
json: { address: account.address, source },
|
|
2655
|
+
table: () => {
|
|
2656
|
+
console.log(` Address: ${account.address}`);
|
|
2657
|
+
console.log(` Source: ${source}`);
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
track("cli_wallet_info", {
|
|
2661
|
+
source: resolved.source,
|
|
2662
|
+
output_format: json ? "json" : "text"
|
|
2663
|
+
});
|
|
2664
|
+
});
|
|
2665
|
+
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) {
|
|
2666
|
+
const json = getJson(this);
|
|
2667
|
+
const nonInteractive = getYes(this);
|
|
2668
|
+
const resolved = resolvePrivateKey();
|
|
2669
|
+
if (!resolved) {
|
|
2670
|
+
outputErrorAndExit(json, NO_WALLET_CONFIGURED, NO_WALLET_SUGGESTION);
|
|
2671
|
+
}
|
|
2672
|
+
if (!options.force) {
|
|
2673
|
+
console.log(
|
|
2674
|
+
" \u26A0 Your private key grants full access to your wallet."
|
|
2675
|
+
);
|
|
2676
|
+
console.log(
|
|
2677
|
+
" Anyone who sees it can steal your funds. Never share it.\n"
|
|
2678
|
+
);
|
|
2679
|
+
const ok = await confirmOrDefault(
|
|
2680
|
+
{ message: "Export private key?", default: false },
|
|
2681
|
+
nonInteractive
|
|
2682
|
+
);
|
|
2683
|
+
if (!ok) {
|
|
2684
|
+
console.error("Aborted.");
|
|
2685
|
+
process.exit(0);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
console.log(resolved.key);
|
|
2689
|
+
track("cli_wallet_export", {
|
|
2690
|
+
output_format: json ? "json" : "text"
|
|
2691
|
+
});
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
// src/components/Zorb.tsx
|
|
2695
|
+
import { Text as Text4, Box as Box4 } from "ink";
|
|
2696
|
+
|
|
2697
|
+
// src/lib/zorb-pixels.ts
|
|
2698
|
+
function supportsTruecolor() {
|
|
2699
|
+
if (!process.stdout.isTTY) return false;
|
|
2700
|
+
const ct = process.env.COLORTERM;
|
|
2701
|
+
if (ct === "truecolor" || ct === "24bit") return true;
|
|
2702
|
+
if (typeof process.stdout.getColorDepth === "function") {
|
|
2703
|
+
return process.stdout.getColorDepth() >= 24;
|
|
2704
|
+
}
|
|
2705
|
+
return false;
|
|
2706
|
+
}
|
|
2707
|
+
function hexToRgb(hex) {
|
|
2708
|
+
const n = parseInt(hex.replace("#", ""), 16);
|
|
2709
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
2710
|
+
}
|
|
2711
|
+
function lerp(a, b, t) {
|
|
2712
|
+
return a + (b - a) * t;
|
|
2713
|
+
}
|
|
2714
|
+
function clamp(v, min, max) {
|
|
2715
|
+
return v < min ? min : v > max ? max : v;
|
|
2716
|
+
}
|
|
2717
|
+
function alphaOver(bg, fg, a) {
|
|
2718
|
+
return [lerp(bg[0], fg[0], a), lerp(bg[1], fg[1], a), lerp(bg[2], fg[2], a)];
|
|
2719
|
+
}
|
|
2720
|
+
function gaussian(dist, sigma) {
|
|
2721
|
+
if (sigma <= 0) return dist <= 0 ? 1 : 0;
|
|
2722
|
+
return Math.exp(-(dist * dist) / (2 * sigma * sigma));
|
|
2723
|
+
}
|
|
2724
|
+
var BASE_COLOR = hexToRgb("#A1723A");
|
|
2725
|
+
var LAYERS = [
|
|
2726
|
+
// 1: Dark maroon shadow
|
|
2727
|
+
{
|
|
2728
|
+
cx: 0.54,
|
|
2729
|
+
cy: 0.45,
|
|
2730
|
+
radius: 0.53,
|
|
2731
|
+
color: hexToRgb("#531002"),
|
|
2732
|
+
blur: 0.062,
|
|
2733
|
+
opacity: 1
|
|
2734
|
+
},
|
|
2735
|
+
// 2: Blue body
|
|
2736
|
+
{
|
|
2737
|
+
cx: 0.6,
|
|
2738
|
+
cy: 0.38,
|
|
2739
|
+
radius: 0.43,
|
|
2740
|
+
color: hexToRgb("#2B5DF0"),
|
|
2741
|
+
blur: 0.124,
|
|
2742
|
+
opacity: 1
|
|
2743
|
+
},
|
|
2744
|
+
// 3: Blue accent (gradient from center color to transparent)
|
|
2745
|
+
{
|
|
2746
|
+
cx: 0.59,
|
|
2747
|
+
cy: 0.38,
|
|
2748
|
+
radius: 0.45,
|
|
2749
|
+
color: hexToRgb("#387AFA"),
|
|
2750
|
+
blur: 0.046,
|
|
2751
|
+
opacity: 1,
|
|
2752
|
+
gradient: { gcx: 0.66, gcy: 0.26 }
|
|
2753
|
+
},
|
|
2754
|
+
// 4: Pink glow
|
|
2755
|
+
{
|
|
2756
|
+
cx: 0.66,
|
|
2757
|
+
cy: 0.27,
|
|
2758
|
+
radius: 0.23,
|
|
2759
|
+
color: hexToRgb("#FCB8D4"),
|
|
2760
|
+
blur: 0.093,
|
|
2761
|
+
opacity: 1
|
|
2762
|
+
},
|
|
2763
|
+
// 5: White specular
|
|
2764
|
+
{
|
|
2765
|
+
cx: 0.66,
|
|
2766
|
+
cy: 0.27,
|
|
2767
|
+
radius: 0.09,
|
|
2768
|
+
color: hexToRgb("#FFFFFF"),
|
|
2769
|
+
blur: 0.062,
|
|
2770
|
+
opacity: 1
|
|
2771
|
+
},
|
|
2772
|
+
// 6: Dark ring (transparent → black → transparent, opacity 0.9)
|
|
2773
|
+
{
|
|
2774
|
+
cx: 0.6,
|
|
2775
|
+
cy: 0.36,
|
|
2776
|
+
radius: 0.82,
|
|
2777
|
+
color: [0, 0, 0],
|
|
2778
|
+
blur: 0.046,
|
|
2779
|
+
opacity: 0.9,
|
|
2780
|
+
ring: true
|
|
2781
|
+
}
|
|
2782
|
+
];
|
|
2783
|
+
function computeLayerAlpha(nx, ny, layer) {
|
|
2784
|
+
const dx = nx - layer.cx;
|
|
2785
|
+
const dy = ny - layer.cy;
|
|
2786
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
2787
|
+
const radialFalloff = gaussian(Math.max(0, dist - layer.radius), layer.blur);
|
|
2788
|
+
if (layer.ring) {
|
|
2789
|
+
const normalizedDist = dist / layer.radius;
|
|
2790
|
+
const ringProfile = gaussian(normalizedDist - 0.7, 0.15) * radialFalloff;
|
|
2791
|
+
return { alpha: ringProfile * layer.opacity, color: layer.color };
|
|
2792
|
+
}
|
|
2793
|
+
if (layer.gradient) {
|
|
2794
|
+
const gdx = nx - layer.gradient.gcx;
|
|
2795
|
+
const gdy = ny - layer.gradient.gcy;
|
|
2796
|
+
const gDist = Math.sqrt(gdx * gdx + gdy * gdy);
|
|
2797
|
+
const gradientT = clamp(gDist / (layer.radius * 1.2), 0, 1);
|
|
2798
|
+
const alpha = radialFalloff * (1 - gradientT);
|
|
2799
|
+
return { alpha: alpha * layer.opacity, color: layer.color };
|
|
2800
|
+
}
|
|
2801
|
+
return { alpha: radialFalloff * layer.opacity, color: layer.color };
|
|
2802
|
+
}
|
|
2803
|
+
function circleAlpha(px, py, size) {
|
|
2804
|
+
const cx = (size - 1) / 2;
|
|
2805
|
+
const cy = (size - 1) / 2;
|
|
2806
|
+
const r = size / 2;
|
|
2807
|
+
const dx = px - cx;
|
|
2808
|
+
const dy = py - cy;
|
|
2809
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
2810
|
+
return clamp(r - dist + 0.5, 0, 1);
|
|
2811
|
+
}
|
|
2812
|
+
function generateZorbPixels(size) {
|
|
2813
|
+
const grid = [];
|
|
2814
|
+
for (let y = 0; y < size; y++) {
|
|
2815
|
+
const row = [];
|
|
2816
|
+
for (let x = 0; x < size; x++) {
|
|
2817
|
+
const nx = x / (size - 1);
|
|
2818
|
+
const ny = y / (size - 1);
|
|
2819
|
+
let pixel = [...BASE_COLOR];
|
|
2820
|
+
for (const layer of LAYERS) {
|
|
2821
|
+
const { alpha, color } = computeLayerAlpha(nx, ny, layer);
|
|
2822
|
+
if (alpha > 1e-3) {
|
|
2823
|
+
pixel = alphaOver(pixel, color, alpha);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
const ca = circleAlpha(x, y, size);
|
|
2827
|
+
pixel = [
|
|
2828
|
+
Math.round(pixel[0] * ca),
|
|
2829
|
+
Math.round(pixel[1] * ca),
|
|
2830
|
+
Math.round(pixel[2] * ca)
|
|
2831
|
+
];
|
|
2832
|
+
row.push(pixel);
|
|
2833
|
+
}
|
|
2834
|
+
grid.push(row);
|
|
2835
|
+
}
|
|
2836
|
+
return grid;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// src/components/Zorb.tsx
|
|
2840
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2841
|
+
var LOWER_HALF_BLOCK = "\u2584";
|
|
2842
|
+
var UPPER_HALF_BLOCK = "\u2580";
|
|
2843
|
+
function rgbString([r, g, b]) {
|
|
2844
|
+
return `rgb(${r},${g},${b})`;
|
|
2845
|
+
}
|
|
2846
|
+
function isBlack([r, g, b]) {
|
|
2847
|
+
return r === 0 && g === 0 && b === 0;
|
|
2848
|
+
}
|
|
2849
|
+
function Zorb({ size = 20 }) {
|
|
2850
|
+
if (!supportsTruecolor()) return null;
|
|
2851
|
+
const grid = generateZorbPixels(size);
|
|
2852
|
+
const rows = [];
|
|
2853
|
+
for (let y = 0; y < size; y += 2) {
|
|
2854
|
+
const topRow = grid[y];
|
|
2855
|
+
const bottomRow = y + 1 < size ? grid[y + 1] : void 0;
|
|
2856
|
+
const cells = [];
|
|
2857
|
+
for (let x = 0; x < size; x++) {
|
|
2858
|
+
const top = topRow[x];
|
|
2859
|
+
const bottom = bottomRow ? bottomRow[x] : [0, 0, 0];
|
|
2860
|
+
const topIsBlack = isBlack(top);
|
|
2861
|
+
const bottomIsBlack = isBlack(bottom);
|
|
2862
|
+
if (topIsBlack && bottomIsBlack) {
|
|
2863
|
+
cells.push(/* @__PURE__ */ jsx5(Text4, { children: " " }, x));
|
|
2864
|
+
} else if (topIsBlack) {
|
|
2865
|
+
cells.push(
|
|
2866
|
+
/* @__PURE__ */ jsx5(Text4, { color: rgbString(bottom), children: LOWER_HALF_BLOCK }, x)
|
|
2867
|
+
);
|
|
2868
|
+
} else if (bottomIsBlack) {
|
|
2869
|
+
cells.push(
|
|
2870
|
+
/* @__PURE__ */ jsx5(Text4, { color: rgbString(top), children: UPPER_HALF_BLOCK }, x)
|
|
2871
|
+
);
|
|
2872
|
+
} else {
|
|
2873
|
+
cells.push(
|
|
2874
|
+
/* @__PURE__ */ jsx5(
|
|
2875
|
+
Text4,
|
|
2876
|
+
{
|
|
2877
|
+
backgroundColor: rgbString(top),
|
|
2878
|
+
color: rgbString(bottom),
|
|
2879
|
+
children: LOWER_HALF_BLOCK
|
|
2880
|
+
},
|
|
2881
|
+
x
|
|
2882
|
+
)
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
rows.push(/* @__PURE__ */ jsx5(Text4, { children: cells }, y));
|
|
2887
|
+
}
|
|
2888
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
2889
|
+
/* @__PURE__ */ jsx5(Text4, { children: " " }),
|
|
2890
|
+
rows,
|
|
2891
|
+
/* @__PURE__ */ jsx5(Text4, { children: " " })
|
|
2892
|
+
] });
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
// src/index.tsx
|
|
2896
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
2897
|
+
if (process.env.ZORA_API_TARGET) {
|
|
2898
|
+
setApiBaseUrl(process.env.ZORA_API_TARGET);
|
|
2899
|
+
}
|
|
2900
|
+
var version = true ? "0.2.3" : JSON.parse(
|
|
2901
|
+
readFileSync2(new URL("../package.json", import.meta.url), "utf-8")
|
|
2902
|
+
).version;
|
|
2903
|
+
var buildProgram = () => {
|
|
2904
|
+
const program2 = new Command9().name("zora").description("Zora CLI").version(version).option("--json", "Output as JSON (for scripts and automation)", false);
|
|
2905
|
+
program2.addCommand(authCommand);
|
|
2906
|
+
program2.addCommand(balanceCommand);
|
|
2907
|
+
program2.addCommand(buyCommand);
|
|
2908
|
+
program2.addCommand(exploreCommand);
|
|
2909
|
+
program2.addCommand(getCommand);
|
|
2910
|
+
program2.addCommand(setupCommand);
|
|
2911
|
+
program2.addCommand(walletCommand);
|
|
2912
|
+
program2.addCommand(sellCommand);
|
|
2913
|
+
return program2;
|
|
2914
|
+
};
|
|
2915
|
+
var program = buildProgram();
|
|
2916
|
+
if (!process.env.VITEST) {
|
|
2917
|
+
const showingHelp = process.argv.length <= 2 || process.argv.includes("--help") || process.argv.includes("-h");
|
|
2918
|
+
if (showingHelp && !process.argv.includes("--json") && supportsTruecolor()) {
|
|
2919
|
+
renderOnce(/* @__PURE__ */ jsx6(Zorb, { size: 20 }));
|
|
2920
|
+
}
|
|
2921
|
+
identify();
|
|
2922
|
+
try {
|
|
2923
|
+
await program.parseAsync();
|
|
2924
|
+
} catch (err) {
|
|
2925
|
+
if (err instanceof ExitPromptError) {
|
|
2926
|
+
console.log("\nAborted.");
|
|
2927
|
+
process.exit(0);
|
|
2928
|
+
}
|
|
2929
|
+
throw err;
|
|
2930
|
+
} finally {
|
|
2931
|
+
await shutdownAnalytics();
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
export {
|
|
2935
|
+
buildProgram
|
|
2936
|
+
};
|