@net-protocol/cli 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,970 @@
1
+ import { Command } from 'commander';
2
+ import chalk3 from 'chalk';
3
+ import { StorageClient, chunkDataForStorage, CHUNKED_STORAGE_CONTRACT } from '@net-protocol/storage';
4
+ import { PROFILE_PICTURE_STORAGE_KEY, PROFILE_METADATA_STORAGE_KEY, parseProfileMetadata, PROFILE_CANVAS_STORAGE_KEY, isValidUrl, getProfilePictureStorageArgs, STORAGE_CONTRACT, isValidXUsername, getProfileMetadataStorageArgs, isValidBio, isValidTokenAddress } from '@net-protocol/profiles';
5
+ import { createWalletClient, http, publicActions, encodeFunctionData } from 'viem';
6
+ import { privateKeyToAccount } from 'viem/accounts';
7
+ import { base } from 'viem/chains';
8
+ import { getChainRpcUrls, toBytes32 } from '@net-protocol/core';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+
12
+ // src/commands/profile/index.ts
13
+ function getRequiredChainId(optionValue) {
14
+ const chainId = optionValue || (process.env.NET_CHAIN_ID ? parseInt(process.env.NET_CHAIN_ID, 10) : void 0);
15
+ if (!chainId) {
16
+ console.error(
17
+ chalk3.red(
18
+ "Error: Chain ID is required. Provide via --chain-id flag or NET_CHAIN_ID environment variable"
19
+ )
20
+ );
21
+ process.exit(1);
22
+ }
23
+ return chainId;
24
+ }
25
+ function getRpcUrl(optionValue) {
26
+ return optionValue || process.env.NET_RPC_URL;
27
+ }
28
+ function parseCommonOptions(options, supportsEncodeOnly = false) {
29
+ const privateKey = options.privateKey || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY;
30
+ if (!privateKey) {
31
+ const encodeOnlyHint = supportsEncodeOnly ? ", or use --encode-only to output transaction data without submitting" : "";
32
+ console.error(
33
+ chalk3.red(
34
+ `Error: Private key is required. Provide via --private-key flag or NET_PRIVATE_KEY/PRIVATE_KEY environment variable${encodeOnlyHint}`
35
+ )
36
+ );
37
+ process.exit(1);
38
+ }
39
+ if (!privateKey.startsWith("0x") || privateKey.length !== 66) {
40
+ console.error(
41
+ chalk3.red(
42
+ "Error: Invalid private key format (must be 0x-prefixed, 66 characters)"
43
+ )
44
+ );
45
+ process.exit(1);
46
+ }
47
+ if (options.privateKey) {
48
+ console.warn(
49
+ chalk3.yellow(
50
+ "Warning: Private key provided via command line. Consider using NET_PRIVATE_KEY environment variable instead."
51
+ )
52
+ );
53
+ }
54
+ return {
55
+ privateKey,
56
+ chainId: getRequiredChainId(options.chainId),
57
+ rpcUrl: getRpcUrl(options.rpcUrl)
58
+ };
59
+ }
60
+ function parseReadOnlyOptions(options) {
61
+ return {
62
+ chainId: getRequiredChainId(options.chainId),
63
+ rpcUrl: getRpcUrl(options.rpcUrl)
64
+ };
65
+ }
66
+ function exitWithError(message) {
67
+ console.error(chalk3.red(`Error: ${message}`));
68
+ process.exit(1);
69
+ }
70
+
71
+ // src/commands/profile/get.ts
72
+ async function executeProfileGet(options) {
73
+ const readOnlyOptions = parseReadOnlyOptions({
74
+ chainId: options.chainId,
75
+ rpcUrl: options.rpcUrl
76
+ });
77
+ const client = new StorageClient({
78
+ chainId: readOnlyOptions.chainId,
79
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
80
+ });
81
+ try {
82
+ let profilePicture;
83
+ try {
84
+ const pictureResult = await client.readStorageData({
85
+ key: PROFILE_PICTURE_STORAGE_KEY,
86
+ operator: options.address
87
+ });
88
+ if (pictureResult.data) {
89
+ profilePicture = pictureResult.data;
90
+ }
91
+ } catch (error) {
92
+ const errorMessage = error instanceof Error ? error.message : String(error);
93
+ if (errorMessage !== "StoredDataNotFound") {
94
+ throw error;
95
+ }
96
+ }
97
+ let xUsername;
98
+ let bio;
99
+ let tokenAddress;
100
+ try {
101
+ const metadataResult = await client.readStorageData({
102
+ key: PROFILE_METADATA_STORAGE_KEY,
103
+ operator: options.address
104
+ });
105
+ if (metadataResult.data) {
106
+ const metadata = parseProfileMetadata(metadataResult.data);
107
+ xUsername = metadata?.x_username;
108
+ bio = metadata?.bio;
109
+ tokenAddress = metadata?.token_address;
110
+ }
111
+ } catch (error) {
112
+ const errorMessage = error instanceof Error ? error.message : String(error);
113
+ if (errorMessage !== "StoredDataNotFound") {
114
+ throw error;
115
+ }
116
+ }
117
+ let canvasSize;
118
+ let canvasIsDataUri = false;
119
+ try {
120
+ const canvasResult = await client.readChunkedStorage({
121
+ key: PROFILE_CANVAS_STORAGE_KEY,
122
+ operator: options.address
123
+ });
124
+ if (canvasResult.data) {
125
+ canvasSize = canvasResult.data.length;
126
+ canvasIsDataUri = canvasResult.data.startsWith("data:");
127
+ }
128
+ } catch (error) {
129
+ const errorMessage = error instanceof Error ? error.message : String(error);
130
+ if (errorMessage !== "ChunkedStorage metadata not found" && !errorMessage.includes("not found")) {
131
+ throw error;
132
+ }
133
+ }
134
+ const hasProfile = profilePicture || xUsername || bio || tokenAddress || canvasSize;
135
+ if (options.json) {
136
+ const output = {
137
+ address: options.address,
138
+ chainId: readOnlyOptions.chainId,
139
+ profilePicture: profilePicture || null,
140
+ xUsername: xUsername || null,
141
+ bio: bio || null,
142
+ tokenAddress: tokenAddress || null,
143
+ canvas: canvasSize ? { size: canvasSize, isDataUri: canvasIsDataUri } : null,
144
+ hasProfile
145
+ };
146
+ console.log(JSON.stringify(output, null, 2));
147
+ return;
148
+ }
149
+ console.log(chalk3.white.bold("\nProfile:\n"));
150
+ console.log(` ${chalk3.cyan("Address:")} ${options.address}`);
151
+ console.log(` ${chalk3.cyan("Chain ID:")} ${readOnlyOptions.chainId}`);
152
+ console.log(
153
+ ` ${chalk3.cyan("Profile Picture:")} ${profilePicture || chalk3.gray("(not set)")}`
154
+ );
155
+ console.log(
156
+ ` ${chalk3.cyan("X Username:")} ${xUsername ? `@${xUsername}` : chalk3.gray("(not set)")}`
157
+ );
158
+ console.log(
159
+ ` ${chalk3.cyan("Bio:")} ${bio || chalk3.gray("(not set)")}`
160
+ );
161
+ console.log(
162
+ ` ${chalk3.cyan("Token Address:")} ${tokenAddress || chalk3.gray("(not set)")}`
163
+ );
164
+ console.log(
165
+ ` ${chalk3.cyan("Canvas:")} ${canvasSize ? `${canvasSize} bytes${canvasIsDataUri ? " (data URI)" : ""}` : chalk3.gray("(not set)")}`
166
+ );
167
+ if (!hasProfile) {
168
+ console.log(chalk3.yellow("\n No profile data found for this address."));
169
+ }
170
+ console.log();
171
+ } catch (error) {
172
+ exitWithError(
173
+ `Failed to read profile: ${error instanceof Error ? error.message : String(error)}`
174
+ );
175
+ }
176
+ }
177
+ function encodeTransaction(config, chainId) {
178
+ const calldata = encodeFunctionData({
179
+ abi: config.abi,
180
+ functionName: config.functionName,
181
+ args: config.args
182
+ });
183
+ return {
184
+ to: config.to,
185
+ data: calldata,
186
+ chainId,
187
+ value: config.value?.toString() ?? "0"
188
+ };
189
+ }
190
+
191
+ // src/commands/profile/set-picture.ts
192
+ async function executeProfileSetPicture(options) {
193
+ if (!isValidUrl(options.url)) {
194
+ exitWithError(
195
+ `Invalid URL: "${options.url}". Please provide a valid URL (e.g., https://example.com/image.jpg)`
196
+ );
197
+ }
198
+ const storageArgs = getProfilePictureStorageArgs(options.url);
199
+ if (options.encodeOnly) {
200
+ const readOnlyOptions = parseReadOnlyOptions({
201
+ chainId: options.chainId,
202
+ rpcUrl: options.rpcUrl
203
+ });
204
+ const encoded = encodeTransaction(
205
+ {
206
+ to: STORAGE_CONTRACT.address,
207
+ abi: STORAGE_CONTRACT.abi,
208
+ functionName: "put",
209
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
210
+ },
211
+ readOnlyOptions.chainId
212
+ );
213
+ console.log(JSON.stringify(encoded, null, 2));
214
+ return;
215
+ }
216
+ const commonOptions = parseCommonOptions(
217
+ {
218
+ privateKey: options.privateKey,
219
+ chainId: options.chainId,
220
+ rpcUrl: options.rpcUrl
221
+ },
222
+ true
223
+ // supports --encode-only
224
+ );
225
+ try {
226
+ const account = privateKeyToAccount(commonOptions.privateKey);
227
+ const rpcUrls = getChainRpcUrls({
228
+ chainId: commonOptions.chainId,
229
+ rpcUrl: commonOptions.rpcUrl
230
+ });
231
+ const client = createWalletClient({
232
+ account,
233
+ chain: base,
234
+ // TODO: Support other chains
235
+ transport: http(rpcUrls[0])
236
+ }).extend(publicActions);
237
+ console.log(chalk3.blue(`\u{1F4F7} Setting profile picture...`));
238
+ console.log(chalk3.gray(` URL: ${options.url}`));
239
+ console.log(chalk3.gray(` Address: ${account.address}`));
240
+ const hash = await client.writeContract({
241
+ address: STORAGE_CONTRACT.address,
242
+ abi: STORAGE_CONTRACT.abi,
243
+ functionName: "put",
244
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
245
+ });
246
+ console.log(chalk3.blue(`\u23F3 Waiting for confirmation...`));
247
+ const receipt = await client.waitForTransactionReceipt({ hash });
248
+ if (receipt.status === "success") {
249
+ console.log(
250
+ chalk3.green(
251
+ `
252
+ \u2713 Profile picture updated successfully!
253
+ Transaction: ${hash}
254
+ URL: ${options.url}`
255
+ )
256
+ );
257
+ } else {
258
+ exitWithError(`Transaction failed: ${hash}`);
259
+ }
260
+ } catch (error) {
261
+ exitWithError(
262
+ `Failed to set profile picture: ${error instanceof Error ? error.message : String(error)}`
263
+ );
264
+ }
265
+ }
266
+ async function readExistingMetadata(address, client) {
267
+ try {
268
+ const metadataResult = await client.readStorageData({
269
+ key: PROFILE_METADATA_STORAGE_KEY,
270
+ operator: address
271
+ });
272
+ if (metadataResult.data) {
273
+ const metadata = parseProfileMetadata(metadataResult.data);
274
+ return {
275
+ x_username: metadata?.x_username,
276
+ bio: metadata?.bio,
277
+ display_name: metadata?.display_name,
278
+ token_address: metadata?.token_address
279
+ };
280
+ }
281
+ } catch (error) {
282
+ const errorMessage = error instanceof Error ? error.message : String(error);
283
+ if (errorMessage !== "StoredDataNotFound") {
284
+ throw error;
285
+ }
286
+ }
287
+ return {};
288
+ }
289
+
290
+ // src/commands/profile/set-username.ts
291
+ async function executeProfileSetUsername(options) {
292
+ if (!isValidXUsername(options.username)) {
293
+ exitWithError(
294
+ `Invalid X username: "${options.username}". Usernames must be 1-15 characters, alphanumeric and underscores only.`
295
+ );
296
+ }
297
+ const usernameForStorage = options.username.startsWith("@") ? options.username.slice(1) : options.username;
298
+ const displayUsername = `@${usernameForStorage}`;
299
+ if (options.encodeOnly) {
300
+ const readOnlyOptions = parseReadOnlyOptions({
301
+ chainId: options.chainId,
302
+ rpcUrl: options.rpcUrl
303
+ });
304
+ const storageArgs = getProfileMetadataStorageArgs({
305
+ x_username: usernameForStorage
306
+ });
307
+ const encoded = encodeTransaction(
308
+ {
309
+ to: STORAGE_CONTRACT.address,
310
+ abi: STORAGE_CONTRACT.abi,
311
+ functionName: "put",
312
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
313
+ },
314
+ readOnlyOptions.chainId
315
+ );
316
+ console.log(JSON.stringify(encoded, null, 2));
317
+ return;
318
+ }
319
+ const commonOptions = parseCommonOptions(
320
+ {
321
+ privateKey: options.privateKey,
322
+ chainId: options.chainId,
323
+ rpcUrl: options.rpcUrl
324
+ },
325
+ true
326
+ // supports --encode-only
327
+ );
328
+ try {
329
+ const account = privateKeyToAccount(commonOptions.privateKey);
330
+ const rpcUrls = getChainRpcUrls({
331
+ chainId: commonOptions.chainId,
332
+ rpcUrl: commonOptions.rpcUrl
333
+ });
334
+ const client = createWalletClient({
335
+ account,
336
+ chain: base,
337
+ // TODO: Support other chains
338
+ transport: http(rpcUrls[0])
339
+ }).extend(publicActions);
340
+ console.log(chalk3.blue(`Setting X username...`));
341
+ console.log(chalk3.gray(` Username: ${displayUsername}`));
342
+ console.log(chalk3.gray(` Address: ${account.address}`));
343
+ const storageClient = new StorageClient({
344
+ chainId: commonOptions.chainId,
345
+ overrides: commonOptions.rpcUrl ? { rpcUrls: [commonOptions.rpcUrl] } : void 0
346
+ });
347
+ const existing = await readExistingMetadata(
348
+ account.address,
349
+ storageClient
350
+ );
351
+ const storageArgs = getProfileMetadataStorageArgs({
352
+ x_username: usernameForStorage,
353
+ bio: existing.bio,
354
+ display_name: existing.display_name,
355
+ token_address: existing.token_address
356
+ });
357
+ const hash = await client.writeContract({
358
+ address: STORAGE_CONTRACT.address,
359
+ abi: STORAGE_CONTRACT.abi,
360
+ functionName: "put",
361
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
362
+ });
363
+ console.log(chalk3.blue(`Waiting for confirmation...`));
364
+ const receipt = await client.waitForTransactionReceipt({ hash });
365
+ if (receipt.status === "success") {
366
+ console.log(
367
+ chalk3.green(
368
+ `
369
+ X username updated successfully!
370
+ Transaction: ${hash}
371
+ Username: ${displayUsername}`
372
+ )
373
+ );
374
+ } else {
375
+ exitWithError(`Transaction failed: ${hash}`);
376
+ }
377
+ } catch (error) {
378
+ exitWithError(
379
+ `Failed to set X username: ${error instanceof Error ? error.message : String(error)}`
380
+ );
381
+ }
382
+ }
383
+ async function executeProfileSetBio(options) {
384
+ if (!isValidBio(options.bio)) {
385
+ exitWithError(
386
+ `Invalid bio: "${options.bio}". Bio must be 1-280 characters and cannot contain control characters.`
387
+ );
388
+ }
389
+ if (options.encodeOnly) {
390
+ const readOnlyOptions = parseReadOnlyOptions({
391
+ chainId: options.chainId,
392
+ rpcUrl: options.rpcUrl
393
+ });
394
+ const storageArgs = getProfileMetadataStorageArgs({ bio: options.bio });
395
+ const encoded = encodeTransaction(
396
+ {
397
+ to: STORAGE_CONTRACT.address,
398
+ abi: STORAGE_CONTRACT.abi,
399
+ functionName: "put",
400
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
401
+ },
402
+ readOnlyOptions.chainId
403
+ );
404
+ console.log(JSON.stringify(encoded, null, 2));
405
+ return;
406
+ }
407
+ const commonOptions = parseCommonOptions(
408
+ {
409
+ privateKey: options.privateKey,
410
+ chainId: options.chainId,
411
+ rpcUrl: options.rpcUrl
412
+ },
413
+ true
414
+ // supports --encode-only
415
+ );
416
+ try {
417
+ const account = privateKeyToAccount(commonOptions.privateKey);
418
+ const rpcUrls = getChainRpcUrls({
419
+ chainId: commonOptions.chainId,
420
+ rpcUrl: commonOptions.rpcUrl
421
+ });
422
+ const client = createWalletClient({
423
+ account,
424
+ chain: base,
425
+ // TODO: Support other chains
426
+ transport: http(rpcUrls[0])
427
+ }).extend(publicActions);
428
+ console.log(chalk3.blue(`Setting profile bio...`));
429
+ console.log(chalk3.gray(` Bio: ${options.bio}`));
430
+ console.log(chalk3.gray(` Address: ${account.address}`));
431
+ const storageClient = new StorageClient({
432
+ chainId: commonOptions.chainId,
433
+ overrides: commonOptions.rpcUrl ? { rpcUrls: [commonOptions.rpcUrl] } : void 0
434
+ });
435
+ const existing = await readExistingMetadata(
436
+ account.address,
437
+ storageClient
438
+ );
439
+ const storageArgs = getProfileMetadataStorageArgs({
440
+ bio: options.bio,
441
+ x_username: existing.x_username,
442
+ display_name: existing.display_name,
443
+ token_address: existing.token_address
444
+ });
445
+ const hash = await client.writeContract({
446
+ address: STORAGE_CONTRACT.address,
447
+ abi: STORAGE_CONTRACT.abi,
448
+ functionName: "put",
449
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
450
+ });
451
+ console.log(chalk3.blue(`Waiting for confirmation...`));
452
+ const receipt = await client.waitForTransactionReceipt({ hash });
453
+ if (receipt.status === "success") {
454
+ console.log(
455
+ chalk3.green(
456
+ `
457
+ Bio updated successfully!
458
+ Transaction: ${hash}
459
+ Bio: ${options.bio}`
460
+ )
461
+ );
462
+ } else {
463
+ exitWithError(`Transaction failed: ${hash}`);
464
+ }
465
+ } catch (error) {
466
+ exitWithError(
467
+ `Failed to set bio: ${error instanceof Error ? error.message : String(error)}`
468
+ );
469
+ }
470
+ }
471
+ async function executeProfileSetTokenAddress(options) {
472
+ if (!isValidTokenAddress(options.tokenAddress)) {
473
+ exitWithError(
474
+ `Invalid token address: "${options.tokenAddress}". Must be a valid EVM address (0x-prefixed, 40 hex characters).`
475
+ );
476
+ }
477
+ const normalizedAddress = options.tokenAddress.toLowerCase();
478
+ if (options.encodeOnly) {
479
+ const readOnlyOptions = parseReadOnlyOptions({
480
+ chainId: options.chainId,
481
+ rpcUrl: options.rpcUrl
482
+ });
483
+ const storageArgs = getProfileMetadataStorageArgs({
484
+ token_address: normalizedAddress
485
+ });
486
+ const encoded = encodeTransaction(
487
+ {
488
+ to: STORAGE_CONTRACT.address,
489
+ abi: STORAGE_CONTRACT.abi,
490
+ functionName: "put",
491
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
492
+ },
493
+ readOnlyOptions.chainId
494
+ );
495
+ console.log(JSON.stringify(encoded, null, 2));
496
+ return;
497
+ }
498
+ const commonOptions = parseCommonOptions(
499
+ {
500
+ privateKey: options.privateKey,
501
+ chainId: options.chainId,
502
+ rpcUrl: options.rpcUrl
503
+ },
504
+ true
505
+ // supports --encode-only
506
+ );
507
+ try {
508
+ const account = privateKeyToAccount(commonOptions.privateKey);
509
+ const rpcUrls = getChainRpcUrls({
510
+ chainId: commonOptions.chainId,
511
+ rpcUrl: commonOptions.rpcUrl
512
+ });
513
+ const client = createWalletClient({
514
+ account,
515
+ chain: base,
516
+ // TODO: Support other chains
517
+ transport: http(rpcUrls[0])
518
+ }).extend(publicActions);
519
+ console.log(chalk3.blue(`Setting profile token address...`));
520
+ console.log(chalk3.gray(` Token Address: ${normalizedAddress}`));
521
+ console.log(chalk3.gray(` Address: ${account.address}`));
522
+ const storageClient = new StorageClient({
523
+ chainId: commonOptions.chainId,
524
+ overrides: commonOptions.rpcUrl ? { rpcUrls: [commonOptions.rpcUrl] } : void 0
525
+ });
526
+ const existing = await readExistingMetadata(
527
+ account.address,
528
+ storageClient
529
+ );
530
+ const storageArgs = getProfileMetadataStorageArgs({
531
+ token_address: normalizedAddress,
532
+ x_username: existing.x_username,
533
+ bio: existing.bio,
534
+ display_name: existing.display_name
535
+ });
536
+ const hash = await client.writeContract({
537
+ address: STORAGE_CONTRACT.address,
538
+ abi: STORAGE_CONTRACT.abi,
539
+ functionName: "put",
540
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
541
+ });
542
+ console.log(chalk3.blue(`Waiting for confirmation...`));
543
+ const receipt = await client.waitForTransactionReceipt({ hash });
544
+ if (receipt.status === "success") {
545
+ console.log(
546
+ chalk3.green(
547
+ `
548
+ Token address updated successfully!
549
+ Transaction: ${hash}
550
+ Token Address: ${normalizedAddress}`
551
+ )
552
+ );
553
+ } else {
554
+ exitWithError(`Transaction failed: ${hash}`);
555
+ }
556
+ } catch (error) {
557
+ exitWithError(
558
+ `Failed to set token address: ${error instanceof Error ? error.message : String(error)}`
559
+ );
560
+ }
561
+ }
562
+ var MAX_CANVAS_SIZE = 60 * 1024;
563
+ var CANVAS_FILENAME = "profile-compressed.html";
564
+ function isBinaryContent(buffer) {
565
+ const sampleSize = Math.min(buffer.length, 8192);
566
+ for (let i = 0; i < sampleSize; i++) {
567
+ const byte = buffer[i];
568
+ if (byte === 0) return true;
569
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) return true;
570
+ }
571
+ return false;
572
+ }
573
+ function getMimeType(filePath) {
574
+ const ext = path.extname(filePath).toLowerCase();
575
+ const mimeTypes = {
576
+ ".png": "image/png",
577
+ ".jpg": "image/jpeg",
578
+ ".jpeg": "image/jpeg",
579
+ ".gif": "image/gif",
580
+ ".webp": "image/webp",
581
+ ".svg": "image/svg+xml",
582
+ ".pdf": "application/pdf",
583
+ ".html": "text/html",
584
+ ".htm": "text/html",
585
+ ".css": "text/css",
586
+ ".js": "application/javascript",
587
+ ".json": "application/json",
588
+ ".txt": "text/plain"
589
+ };
590
+ return mimeTypes[ext] || "application/octet-stream";
591
+ }
592
+ function bufferToDataUri(buffer, mimeType) {
593
+ const base64 = buffer.toString("base64");
594
+ return `data:${mimeType};base64,${base64}`;
595
+ }
596
+ async function executeProfileSetCanvas(options) {
597
+ if (!options.file && !options.content) {
598
+ exitWithError(
599
+ "Must provide either --file or --content to set canvas content."
600
+ );
601
+ }
602
+ if (options.file && options.content) {
603
+ exitWithError("Cannot provide both --file and --content. Choose one.");
604
+ }
605
+ let canvasContent;
606
+ if (options.file) {
607
+ const filePath = path.resolve(options.file);
608
+ if (!fs.existsSync(filePath)) {
609
+ exitWithError(`File not found: ${filePath}`);
610
+ }
611
+ const buffer = fs.readFileSync(filePath);
612
+ if (buffer.length > MAX_CANVAS_SIZE) {
613
+ exitWithError(
614
+ `File too large: ${buffer.length} bytes exceeds maximum of ${MAX_CANVAS_SIZE} bytes (60KB).`
615
+ );
616
+ }
617
+ if (isBinaryContent(buffer)) {
618
+ const mimeType = getMimeType(filePath);
619
+ canvasContent = bufferToDataUri(buffer, mimeType);
620
+ } else {
621
+ canvasContent = buffer.toString("utf-8");
622
+ }
623
+ } else {
624
+ canvasContent = options.content;
625
+ const contentSize = Buffer.byteLength(canvasContent, "utf-8");
626
+ if (contentSize > MAX_CANVAS_SIZE) {
627
+ exitWithError(
628
+ `Content too large: ${contentSize} bytes exceeds maximum of ${MAX_CANVAS_SIZE} bytes (60KB).`
629
+ );
630
+ }
631
+ }
632
+ const bytesKey = toBytes32(PROFILE_CANVAS_STORAGE_KEY);
633
+ const chunks = chunkDataForStorage(canvasContent);
634
+ if (options.encodeOnly) {
635
+ const readOnlyOptions = parseReadOnlyOptions({
636
+ chainId: options.chainId,
637
+ rpcUrl: options.rpcUrl
638
+ });
639
+ const encoded = encodeTransaction(
640
+ {
641
+ to: CHUNKED_STORAGE_CONTRACT.address,
642
+ abi: CHUNKED_STORAGE_CONTRACT.abi,
643
+ functionName: "put",
644
+ args: [bytesKey, CANVAS_FILENAME, chunks]
645
+ },
646
+ readOnlyOptions.chainId
647
+ );
648
+ console.log(JSON.stringify(encoded, null, 2));
649
+ return;
650
+ }
651
+ const commonOptions = parseCommonOptions(
652
+ {
653
+ privateKey: options.privateKey,
654
+ chainId: options.chainId,
655
+ rpcUrl: options.rpcUrl
656
+ },
657
+ true
658
+ // supports --encode-only
659
+ );
660
+ try {
661
+ const account = privateKeyToAccount(commonOptions.privateKey);
662
+ const rpcUrls = getChainRpcUrls({
663
+ chainId: commonOptions.chainId,
664
+ rpcUrl: commonOptions.rpcUrl
665
+ });
666
+ const client = createWalletClient({
667
+ account,
668
+ chain: base,
669
+ // TODO: Support other chains
670
+ transport: http(rpcUrls[0])
671
+ }).extend(publicActions);
672
+ console.log(chalk3.blue(`Setting profile canvas...`));
673
+ console.log(
674
+ chalk3.gray(` Content size: ${Buffer.byteLength(canvasContent)} bytes`)
675
+ );
676
+ console.log(chalk3.gray(` Chunks: ${chunks.length}`));
677
+ console.log(chalk3.gray(` Address: ${account.address}`));
678
+ const hash = await client.writeContract({
679
+ address: CHUNKED_STORAGE_CONTRACT.address,
680
+ abi: CHUNKED_STORAGE_CONTRACT.abi,
681
+ functionName: "put",
682
+ args: [bytesKey, CANVAS_FILENAME, chunks]
683
+ });
684
+ console.log(chalk3.blue(`Waiting for confirmation...`));
685
+ const receipt = await client.waitForTransactionReceipt({ hash });
686
+ if (receipt.status === "success") {
687
+ console.log(
688
+ chalk3.green(
689
+ `
690
+ Canvas updated successfully!
691
+ Transaction: ${hash}
692
+ Content size: ${Buffer.byteLength(canvasContent)} bytes
693
+ Chunks: ${chunks.length}`
694
+ )
695
+ );
696
+ } else {
697
+ exitWithError(`Transaction failed: ${hash}`);
698
+ }
699
+ } catch (error) {
700
+ exitWithError(
701
+ `Failed to set canvas: ${error instanceof Error ? error.message : String(error)}`
702
+ );
703
+ }
704
+ }
705
+ function isDataUri(content) {
706
+ return content.startsWith("data:");
707
+ }
708
+ function parseDataUri(dataUri) {
709
+ const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
710
+ if (!match) {
711
+ throw new Error("Invalid data URI format");
712
+ }
713
+ const mimeType = match[1];
714
+ const base64Data = match[2];
715
+ const buffer = Buffer.from(base64Data, "base64");
716
+ return { buffer, mimeType };
717
+ }
718
+ function getExtensionFromMimeType(mimeType) {
719
+ const extensions = {
720
+ "image/png": ".png",
721
+ "image/jpeg": ".jpg",
722
+ "image/gif": ".gif",
723
+ "image/webp": ".webp",
724
+ "image/svg+xml": ".svg",
725
+ "application/pdf": ".pdf",
726
+ "text/html": ".html",
727
+ "text/css": ".css",
728
+ "application/javascript": ".js",
729
+ "application/json": ".json",
730
+ "text/plain": ".txt",
731
+ "application/octet-stream": ".bin"
732
+ };
733
+ return extensions[mimeType] || ".bin";
734
+ }
735
+ async function executeProfileGetCanvas(options) {
736
+ const readOnlyOptions = parseReadOnlyOptions({
737
+ chainId: options.chainId,
738
+ rpcUrl: options.rpcUrl
739
+ });
740
+ const client = new StorageClient({
741
+ chainId: readOnlyOptions.chainId,
742
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
743
+ });
744
+ try {
745
+ let canvasContent;
746
+ let canvasText;
747
+ try {
748
+ const result = await client.readChunkedStorage({
749
+ key: PROFILE_CANVAS_STORAGE_KEY,
750
+ operator: options.address
751
+ });
752
+ if (result.data) {
753
+ canvasContent = result.data;
754
+ canvasText = result.text;
755
+ }
756
+ } catch (error) {
757
+ const errorMessage = error instanceof Error ? error.message : String(error);
758
+ if (errorMessage !== "ChunkedStorage metadata not found" && !errorMessage.includes("not found")) {
759
+ throw error;
760
+ }
761
+ }
762
+ if (options.json) {
763
+ const output = {
764
+ address: options.address,
765
+ chainId: readOnlyOptions.chainId,
766
+ canvas: canvasContent || null,
767
+ filename: canvasText || null,
768
+ hasCanvas: !!canvasContent,
769
+ isDataUri: canvasContent ? isDataUri(canvasContent) : false,
770
+ contentLength: canvasContent ? canvasContent.length : 0
771
+ };
772
+ console.log(JSON.stringify(output, null, 2));
773
+ return;
774
+ }
775
+ if (!canvasContent) {
776
+ exitWithError(`No canvas found for address: ${options.address}`);
777
+ }
778
+ if (options.output) {
779
+ const outputPath = path.resolve(options.output);
780
+ if (isDataUri(canvasContent)) {
781
+ const { buffer, mimeType } = parseDataUri(canvasContent);
782
+ let finalPath = outputPath;
783
+ if (!path.extname(outputPath)) {
784
+ finalPath = outputPath + getExtensionFromMimeType(mimeType);
785
+ }
786
+ fs.writeFileSync(finalPath, buffer);
787
+ console.log(
788
+ chalk3.green(`Canvas written to: ${finalPath} (${buffer.length} bytes)`)
789
+ );
790
+ } else {
791
+ fs.writeFileSync(outputPath, canvasContent, "utf-8");
792
+ console.log(
793
+ chalk3.green(
794
+ `Canvas written to: ${outputPath} (${canvasContent.length} bytes)`
795
+ )
796
+ );
797
+ }
798
+ return;
799
+ }
800
+ console.log(canvasContent);
801
+ } catch (error) {
802
+ exitWithError(
803
+ `Failed to read canvas: ${error instanceof Error ? error.message : String(error)}`
804
+ );
805
+ }
806
+ }
807
+
808
+ // src/commands/profile/index.ts
809
+ function registerProfileCommand(program) {
810
+ const profileCommand = program.command("profile").description("User profile operations");
811
+ const getCommand = new Command("get").description("Get profile data for an address").requiredOption("--address <address>", "Wallet address to get profile for").option(
812
+ "--chain-id <id>",
813
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
814
+ (value) => parseInt(value, 10)
815
+ ).option(
816
+ "--rpc-url <url>",
817
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
818
+ ).option("--json", "Output in JSON format").action(async (options) => {
819
+ await executeProfileGet({
820
+ address: options.address,
821
+ chainId: options.chainId,
822
+ rpcUrl: options.rpcUrl,
823
+ json: options.json
824
+ });
825
+ });
826
+ const setPictureCommand = new Command("set-picture").description("Set your profile picture URL").requiredOption("--url <url>", "Image URL for profile picture").option(
827
+ "--private-key <key>",
828
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
829
+ ).option(
830
+ "--chain-id <id>",
831
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
832
+ (value) => parseInt(value, 10)
833
+ ).option(
834
+ "--rpc-url <url>",
835
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
836
+ ).option(
837
+ "--encode-only",
838
+ "Output transaction data as JSON instead of executing"
839
+ ).action(async (options) => {
840
+ await executeProfileSetPicture({
841
+ url: options.url,
842
+ privateKey: options.privateKey,
843
+ chainId: options.chainId,
844
+ rpcUrl: options.rpcUrl,
845
+ encodeOnly: options.encodeOnly
846
+ });
847
+ });
848
+ const setUsernameCommand = new Command("set-x-username").description("Set your X (Twitter) username for your profile").requiredOption(
849
+ "--username <username>",
850
+ "Your X (Twitter) username (with or without @)"
851
+ ).option(
852
+ "--private-key <key>",
853
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
854
+ ).option(
855
+ "--chain-id <id>",
856
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
857
+ (value) => parseInt(value, 10)
858
+ ).option(
859
+ "--rpc-url <url>",
860
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
861
+ ).option(
862
+ "--encode-only",
863
+ "Output transaction data as JSON instead of executing"
864
+ ).action(async (options) => {
865
+ await executeProfileSetUsername({
866
+ username: options.username,
867
+ privateKey: options.privateKey,
868
+ chainId: options.chainId,
869
+ rpcUrl: options.rpcUrl,
870
+ encodeOnly: options.encodeOnly
871
+ });
872
+ });
873
+ const setBioCommand = new Command("set-bio").description("Set your profile bio").requiredOption("--bio <bio>", "Your profile bio (max 280 characters)").option(
874
+ "--private-key <key>",
875
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
876
+ ).option(
877
+ "--chain-id <id>",
878
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
879
+ (value) => parseInt(value, 10)
880
+ ).option(
881
+ "--rpc-url <url>",
882
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
883
+ ).option(
884
+ "--encode-only",
885
+ "Output transaction data as JSON instead of executing"
886
+ ).action(async (options) => {
887
+ await executeProfileSetBio({
888
+ bio: options.bio,
889
+ privateKey: options.privateKey,
890
+ chainId: options.chainId,
891
+ rpcUrl: options.rpcUrl,
892
+ encodeOnly: options.encodeOnly
893
+ });
894
+ });
895
+ const setTokenAddressCommand = new Command("set-token-address").description("Set your profile token address (ERC-20 token that represents you)").requiredOption(
896
+ "--token-address <address>",
897
+ "ERC-20 token contract address (0x-prefixed)"
898
+ ).option(
899
+ "--private-key <key>",
900
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
901
+ ).option(
902
+ "--chain-id <id>",
903
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
904
+ (value) => parseInt(value, 10)
905
+ ).option(
906
+ "--rpc-url <url>",
907
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
908
+ ).option(
909
+ "--encode-only",
910
+ "Output transaction data as JSON instead of executing"
911
+ ).action(async (options) => {
912
+ await executeProfileSetTokenAddress({
913
+ tokenAddress: options.tokenAddress,
914
+ privateKey: options.privateKey,
915
+ chainId: options.chainId,
916
+ rpcUrl: options.rpcUrl,
917
+ encodeOnly: options.encodeOnly
918
+ });
919
+ });
920
+ const setCanvasCommand = new Command("set-canvas").description("Set your profile canvas (HTML content)").option("--file <path>", "Path to file containing canvas content").option("--content <html>", "HTML content for canvas (inline)").option(
921
+ "--private-key <key>",
922
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
923
+ ).option(
924
+ "--chain-id <id>",
925
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
926
+ (value) => parseInt(value, 10)
927
+ ).option(
928
+ "--rpc-url <url>",
929
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
930
+ ).option(
931
+ "--encode-only",
932
+ "Output transaction data as JSON instead of executing"
933
+ ).action(async (options) => {
934
+ await executeProfileSetCanvas({
935
+ file: options.file,
936
+ content: options.content,
937
+ privateKey: options.privateKey,
938
+ chainId: options.chainId,
939
+ rpcUrl: options.rpcUrl,
940
+ encodeOnly: options.encodeOnly
941
+ });
942
+ });
943
+ const getCanvasCommand = new Command("get-canvas").description("Get profile canvas for an address").requiredOption("--address <address>", "Wallet address to get canvas for").option("--output <path>", "Write canvas content to file instead of stdout").option(
944
+ "--chain-id <id>",
945
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
946
+ (value) => parseInt(value, 10)
947
+ ).option(
948
+ "--rpc-url <url>",
949
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
950
+ ).option("--json", "Output in JSON format").action(async (options) => {
951
+ await executeProfileGetCanvas({
952
+ address: options.address,
953
+ output: options.output,
954
+ chainId: options.chainId,
955
+ rpcUrl: options.rpcUrl,
956
+ json: options.json
957
+ });
958
+ });
959
+ profileCommand.addCommand(getCommand);
960
+ profileCommand.addCommand(setPictureCommand);
961
+ profileCommand.addCommand(setUsernameCommand);
962
+ profileCommand.addCommand(setBioCommand);
963
+ profileCommand.addCommand(setTokenAddressCommand);
964
+ profileCommand.addCommand(setCanvasCommand);
965
+ profileCommand.addCommand(getCanvasCommand);
966
+ }
967
+
968
+ export { registerProfileCommand };
969
+ //# sourceMappingURL=index.mjs.map
970
+ //# sourceMappingURL=index.mjs.map