@net-protocol/cli 0.1.6 → 0.1.8

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.
@@ -3,16 +3,17 @@ import 'dotenv/config';
3
3
  import { Command } from 'commander';
4
4
  import { createRequire } from 'module';
5
5
  import chalk4 from 'chalk';
6
- import * as fs from 'fs';
6
+ import * as fs2 from 'fs';
7
7
  import { readFileSync } from 'fs';
8
- import { StorageClient, detectFileTypeFromBase64, base64ToDataUri, shouldSuggestXmlStorage, getStorageKeyBytes, encodeStorageKeyForUrl, STORAGE_CONTRACT as STORAGE_CONTRACT$1, CHUNKED_STORAGE_CONTRACT } from '@net-protocol/storage';
8
+ import { StorageClient, detectFileTypeFromBase64, base64ToDataUri, shouldSuggestXmlStorage, getStorageKeyBytes, encodeStorageKeyForUrl, chunkDataForStorage, CHUNKED_STORAGE_CONTRACT, STORAGE_CONTRACT as STORAGE_CONTRACT$1 } from '@net-protocol/storage';
9
9
  import { stringToHex, createWalletClient, http, hexToString, parseEther, encodeFunctionData, publicActions, defineChain } from 'viem';
10
10
  import { privateKeyToAccount } from 'viem/accounts';
11
- import { getNetContract, getChainName, getPublicClient, getChainRpcUrls, NetClient } from '@net-protocol/core';
11
+ import { getNetContract, getChainName, getPublicClient, getChainRpcUrls, NetClient, toBytes32 } from '@net-protocol/core';
12
12
  import { createRelayX402Client, createRelaySession, checkBackendWalletBalance, fundBackendWallet, batchTransactions, submitTransactionsViaRelay, waitForConfirmations, retryFailedTransactions as retryFailedTransactions$1 } from '@net-protocol/relay';
13
13
  import { isNetrSupportedChain, NetrClient } from '@net-protocol/netr';
14
- import { PROFILE_PICTURE_STORAGE_KEY, PROFILE_METADATA_STORAGE_KEY, parseProfileMetadata, isValidUrl, getProfilePictureStorageArgs, STORAGE_CONTRACT, isValidXUsername, getXUsernameStorageArgs } from '@net-protocol/profiles';
14
+ import { PROFILE_PICTURE_STORAGE_KEY, PROFILE_METADATA_STORAGE_KEY, parseProfileMetadata, PROFILE_CANVAS_STORAGE_KEY, isValidUrl, getProfilePictureStorageArgs, STORAGE_CONTRACT, isValidXUsername, getXUsernameStorageArgs, isValidBio, getProfileMetadataStorageArgs } from '@net-protocol/profiles';
15
15
  import { base } from 'viem/chains';
16
+ import * as path from 'path';
16
17
 
17
18
  function getRequiredChainId(optionValue) {
18
19
  const chainId = optionValue || (process.env.NET_CHAIN_ID ? parseInt(process.env.NET_CHAIN_ID, 10) : void 0);
@@ -29,12 +30,13 @@ function getRequiredChainId(optionValue) {
29
30
  function getRpcUrl(optionValue) {
30
31
  return optionValue || process.env.NET_RPC_URL;
31
32
  }
32
- function parseCommonOptions(options) {
33
+ function parseCommonOptions(options, supportsEncodeOnly = false) {
33
34
  const privateKey = options.privateKey || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY;
34
35
  if (!privateKey) {
36
+ const encodeOnlyHint = supportsEncodeOnly ? ", or use --encode-only to output transaction data without submitting" : "";
35
37
  console.error(
36
38
  chalk4.red(
37
- "Error: Private key is required. Provide via --private-key flag or NET_PRIVATE_KEY/PRIVATE_KEY environment variable"
39
+ `Error: Private key is required. Provide via --private-key flag or NET_PRIVATE_KEY/PRIVATE_KEY environment variable${encodeOnlyHint}`
38
40
  )
39
41
  );
40
42
  process.exit(1);
@@ -1190,7 +1192,7 @@ async function encodeStorageUpload(options) {
1190
1192
  } else {
1191
1193
  operatorAddress = "0x0000000000000000000000000000000000000000";
1192
1194
  }
1193
- const fileContent = fs.readFileSync(options.filePath, "utf-8");
1195
+ const fileContent = fs2.readFileSync(options.filePath, "utf-8");
1194
1196
  const fileSize = Buffer.byteLength(fileContent, "utf-8");
1195
1197
  const client = new StorageClient({
1196
1198
  chainId: readOnlyOptions.chainId
@@ -1271,11 +1273,15 @@ function registerStorageCommand(program2) {
1271
1273
  }
1272
1274
  return;
1273
1275
  }
1274
- const commonOptions = parseCommonOptions({
1275
- privateKey: options.privateKey,
1276
- chainId: options.chainId,
1277
- rpcUrl: options.rpcUrl
1278
- });
1276
+ const commonOptions = parseCommonOptions(
1277
+ {
1278
+ privateKey: options.privateKey,
1279
+ chainId: options.chainId,
1280
+ rpcUrl: options.rpcUrl
1281
+ },
1282
+ true
1283
+ // supports --encode-only
1284
+ );
1279
1285
  const uploadOptions = {
1280
1286
  filePath: options.file,
1281
1287
  storageKey: options.key,
@@ -1470,8 +1476,8 @@ function registerStorageCommand(program2) {
1470
1476
  console.log(chalk4.blue(`\u{1F4C1} Reading file: ${options.file}`));
1471
1477
  console.log(chalk4.blue(`\u{1F517} Using relay API: ${options.apiUrl}`));
1472
1478
  const result = await uploadFileWithRelay(uploadRelayOptions);
1473
- const { privateKeyToAccount: privateKeyToAccount8 } = await import('viem/accounts');
1474
- const userAccount = privateKeyToAccount8(commonOptions.privateKey);
1479
+ const { privateKeyToAccount: privateKeyToAccount10 } = await import('viem/accounts');
1480
+ const userAccount = privateKeyToAccount10(commonOptions.privateKey);
1475
1481
  const storageUrl = generateStorageUrl(
1476
1482
  userAccount.address,
1477
1483
  commonOptions.chainId,
@@ -1561,11 +1567,15 @@ async function executeSend(options) {
1561
1567
  executeEncodeOnly(options);
1562
1568
  return;
1563
1569
  }
1564
- const commonOptions = parseCommonOptions({
1565
- privateKey: options.privateKey,
1566
- chainId: options.chainId,
1567
- rpcUrl: options.rpcUrl
1568
- });
1570
+ const commonOptions = parseCommonOptions(
1571
+ {
1572
+ privateKey: options.privateKey,
1573
+ chainId: options.chainId,
1574
+ rpcUrl: options.rpcUrl
1575
+ },
1576
+ true
1577
+ // supports --encode-only
1578
+ );
1569
1579
  const client = createNetClient(commonOptions);
1570
1580
  const txConfig = prepareMessageConfig(client, options);
1571
1581
  const account = privateKeyToAccount(commonOptions.privateKey);
@@ -1975,11 +1985,15 @@ async function executeTokenDeploy(options) {
1975
1985
  await executeEncodeOnly2(options);
1976
1986
  return;
1977
1987
  }
1978
- const commonOptions = parseCommonOptions({
1979
- privateKey: options.privateKey,
1980
- chainId: options.chainId,
1981
- rpcUrl: options.rpcUrl
1982
- });
1988
+ const commonOptions = parseCommonOptions(
1989
+ {
1990
+ privateKey: options.privateKey,
1991
+ chainId: options.chainId,
1992
+ rpcUrl: options.rpcUrl
1993
+ },
1994
+ true
1995
+ // supports --encode-only
1996
+ );
1983
1997
  if (!isNetrSupportedChain(commonOptions.chainId)) {
1984
1998
  exitWithError(
1985
1999
  `Chain ${commonOptions.chainId} is not supported for token deployment. Supported: Base (8453), Plasma (9745), Monad (143), HyperEVM (999)`
@@ -2244,6 +2258,7 @@ async function executeProfileGet(options) {
2244
2258
  }
2245
2259
  }
2246
2260
  let xUsername;
2261
+ let bio;
2247
2262
  try {
2248
2263
  const metadataResult = await client.readStorageData({
2249
2264
  key: PROFILE_METADATA_STORAGE_KEY,
@@ -2252,6 +2267,7 @@ async function executeProfileGet(options) {
2252
2267
  if (metadataResult.data) {
2253
2268
  const metadata = parseProfileMetadata(metadataResult.data);
2254
2269
  xUsername = metadata?.x_username;
2270
+ bio = metadata?.bio;
2255
2271
  }
2256
2272
  } catch (error) {
2257
2273
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -2259,13 +2275,32 @@ async function executeProfileGet(options) {
2259
2275
  throw error;
2260
2276
  }
2261
2277
  }
2262
- const hasProfile = profilePicture || xUsername;
2278
+ let canvasSize;
2279
+ let canvasIsDataUri = false;
2280
+ try {
2281
+ const canvasResult = await client.readChunkedStorage({
2282
+ key: PROFILE_CANVAS_STORAGE_KEY,
2283
+ operator: options.address
2284
+ });
2285
+ if (canvasResult.data) {
2286
+ canvasSize = canvasResult.data.length;
2287
+ canvasIsDataUri = canvasResult.data.startsWith("data:");
2288
+ }
2289
+ } catch (error) {
2290
+ const errorMessage = error instanceof Error ? error.message : String(error);
2291
+ if (errorMessage !== "ChunkedStorage metadata not found" && !errorMessage.includes("not found")) {
2292
+ throw error;
2293
+ }
2294
+ }
2295
+ const hasProfile = profilePicture || xUsername || bio || canvasSize;
2263
2296
  if (options.json) {
2264
2297
  const output = {
2265
2298
  address: options.address,
2266
2299
  chainId: readOnlyOptions.chainId,
2267
2300
  profilePicture: profilePicture || null,
2268
2301
  xUsername: xUsername || null,
2302
+ bio: bio || null,
2303
+ canvas: canvasSize ? { size: canvasSize, isDataUri: canvasIsDataUri } : null,
2269
2304
  hasProfile
2270
2305
  };
2271
2306
  console.log(JSON.stringify(output, null, 2));
@@ -2280,6 +2315,12 @@ async function executeProfileGet(options) {
2280
2315
  console.log(
2281
2316
  ` ${chalk4.cyan("X Username:")} ${xUsername ? `@${xUsername}` : chalk4.gray("(not set)")}`
2282
2317
  );
2318
+ console.log(
2319
+ ` ${chalk4.cyan("Bio:")} ${bio || chalk4.gray("(not set)")}`
2320
+ );
2321
+ console.log(
2322
+ ` ${chalk4.cyan("Canvas:")} ${canvasSize ? `${canvasSize} bytes${canvasIsDataUri ? " (data URI)" : ""}` : chalk4.gray("(not set)")}`
2323
+ );
2283
2324
  if (!hasProfile) {
2284
2325
  console.log(chalk4.yellow("\n No profile data found for this address."));
2285
2326
  }
@@ -2314,11 +2355,15 @@ async function executeProfileSetPicture(options) {
2314
2355
  console.log(JSON.stringify(encoded, null, 2));
2315
2356
  return;
2316
2357
  }
2317
- const commonOptions = parseCommonOptions({
2318
- privateKey: options.privateKey,
2319
- chainId: options.chainId,
2320
- rpcUrl: options.rpcUrl
2321
- });
2358
+ const commonOptions = parseCommonOptions(
2359
+ {
2360
+ privateKey: options.privateKey,
2361
+ chainId: options.chainId,
2362
+ rpcUrl: options.rpcUrl
2363
+ },
2364
+ true
2365
+ // supports --encode-only
2366
+ );
2322
2367
  try {
2323
2368
  const account = privateKeyToAccount(commonOptions.privateKey);
2324
2369
  const rpcUrls = getChainRpcUrls({
@@ -2385,11 +2430,15 @@ async function executeProfileSetUsername(options) {
2385
2430
  console.log(JSON.stringify(encoded, null, 2));
2386
2431
  return;
2387
2432
  }
2388
- const commonOptions = parseCommonOptions({
2389
- privateKey: options.privateKey,
2390
- chainId: options.chainId,
2391
- rpcUrl: options.rpcUrl
2392
- });
2433
+ const commonOptions = parseCommonOptions(
2434
+ {
2435
+ privateKey: options.privateKey,
2436
+ chainId: options.chainId,
2437
+ rpcUrl: options.rpcUrl
2438
+ },
2439
+ true
2440
+ // supports --encode-only
2441
+ );
2393
2442
  try {
2394
2443
  const account = privateKeyToAccount(commonOptions.privateKey);
2395
2444
  const rpcUrls = getChainRpcUrls({
@@ -2431,6 +2480,325 @@ async function executeProfileSetUsername(options) {
2431
2480
  );
2432
2481
  }
2433
2482
  }
2483
+ async function executeProfileSetBio(options) {
2484
+ if (!isValidBio(options.bio)) {
2485
+ exitWithError(
2486
+ `Invalid bio: "${options.bio}". Bio must be 1-280 characters and cannot contain control characters.`
2487
+ );
2488
+ }
2489
+ const storageArgs = getProfileMetadataStorageArgs({ bio: options.bio });
2490
+ if (options.encodeOnly) {
2491
+ const readOnlyOptions = parseReadOnlyOptions({
2492
+ chainId: options.chainId,
2493
+ rpcUrl: options.rpcUrl
2494
+ });
2495
+ const encoded = encodeTransaction(
2496
+ {
2497
+ to: STORAGE_CONTRACT.address,
2498
+ abi: STORAGE_CONTRACT.abi,
2499
+ functionName: "put",
2500
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
2501
+ },
2502
+ readOnlyOptions.chainId
2503
+ );
2504
+ console.log(JSON.stringify(encoded, null, 2));
2505
+ return;
2506
+ }
2507
+ const commonOptions = parseCommonOptions(
2508
+ {
2509
+ privateKey: options.privateKey,
2510
+ chainId: options.chainId,
2511
+ rpcUrl: options.rpcUrl
2512
+ },
2513
+ true
2514
+ // supports --encode-only
2515
+ );
2516
+ try {
2517
+ const account = privateKeyToAccount(commonOptions.privateKey);
2518
+ const rpcUrls = getChainRpcUrls({
2519
+ chainId: commonOptions.chainId,
2520
+ rpcUrl: commonOptions.rpcUrl
2521
+ });
2522
+ const client = createWalletClient({
2523
+ account,
2524
+ chain: base,
2525
+ // TODO: Support other chains
2526
+ transport: http(rpcUrls[0])
2527
+ }).extend(publicActions);
2528
+ console.log(chalk4.blue(`Setting profile bio...`));
2529
+ console.log(chalk4.gray(` Bio: ${options.bio}`));
2530
+ console.log(chalk4.gray(` Address: ${account.address}`));
2531
+ const hash = await client.writeContract({
2532
+ address: STORAGE_CONTRACT.address,
2533
+ abi: STORAGE_CONTRACT.abi,
2534
+ functionName: "put",
2535
+ args: [storageArgs.bytesKey, storageArgs.topic, storageArgs.bytesValue]
2536
+ });
2537
+ console.log(chalk4.blue(`Waiting for confirmation...`));
2538
+ const receipt = await client.waitForTransactionReceipt({ hash });
2539
+ if (receipt.status === "success") {
2540
+ console.log(
2541
+ chalk4.green(
2542
+ `
2543
+ Bio updated successfully!
2544
+ Transaction: ${hash}
2545
+ Bio: ${options.bio}`
2546
+ )
2547
+ );
2548
+ } else {
2549
+ exitWithError(`Transaction failed: ${hash}`);
2550
+ }
2551
+ } catch (error) {
2552
+ exitWithError(
2553
+ `Failed to set bio: ${error instanceof Error ? error.message : String(error)}`
2554
+ );
2555
+ }
2556
+ }
2557
+ var MAX_CANVAS_SIZE = 60 * 1024;
2558
+ var CANVAS_FILENAME = "profile-compressed.html";
2559
+ function isBinaryContent(buffer) {
2560
+ const sampleSize = Math.min(buffer.length, 8192);
2561
+ for (let i = 0; i < sampleSize; i++) {
2562
+ const byte = buffer[i];
2563
+ if (byte === 0) return true;
2564
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) return true;
2565
+ }
2566
+ return false;
2567
+ }
2568
+ function getMimeType(filePath) {
2569
+ const ext = path.extname(filePath).toLowerCase();
2570
+ const mimeTypes = {
2571
+ ".png": "image/png",
2572
+ ".jpg": "image/jpeg",
2573
+ ".jpeg": "image/jpeg",
2574
+ ".gif": "image/gif",
2575
+ ".webp": "image/webp",
2576
+ ".svg": "image/svg+xml",
2577
+ ".pdf": "application/pdf",
2578
+ ".html": "text/html",
2579
+ ".htm": "text/html",
2580
+ ".css": "text/css",
2581
+ ".js": "application/javascript",
2582
+ ".json": "application/json",
2583
+ ".txt": "text/plain"
2584
+ };
2585
+ return mimeTypes[ext] || "application/octet-stream";
2586
+ }
2587
+ function bufferToDataUri(buffer, mimeType) {
2588
+ const base64 = buffer.toString("base64");
2589
+ return `data:${mimeType};base64,${base64}`;
2590
+ }
2591
+ async function executeProfileSetCanvas(options) {
2592
+ if (!options.file && !options.content) {
2593
+ exitWithError(
2594
+ "Must provide either --file or --content to set canvas content."
2595
+ );
2596
+ }
2597
+ if (options.file && options.content) {
2598
+ exitWithError("Cannot provide both --file and --content. Choose one.");
2599
+ }
2600
+ let canvasContent;
2601
+ if (options.file) {
2602
+ const filePath = path.resolve(options.file);
2603
+ if (!fs2.existsSync(filePath)) {
2604
+ exitWithError(`File not found: ${filePath}`);
2605
+ }
2606
+ const buffer = fs2.readFileSync(filePath);
2607
+ if (buffer.length > MAX_CANVAS_SIZE) {
2608
+ exitWithError(
2609
+ `File too large: ${buffer.length} bytes exceeds maximum of ${MAX_CANVAS_SIZE} bytes (60KB).`
2610
+ );
2611
+ }
2612
+ if (isBinaryContent(buffer)) {
2613
+ const mimeType = getMimeType(filePath);
2614
+ canvasContent = bufferToDataUri(buffer, mimeType);
2615
+ } else {
2616
+ canvasContent = buffer.toString("utf-8");
2617
+ }
2618
+ } else {
2619
+ canvasContent = options.content;
2620
+ const contentSize = Buffer.byteLength(canvasContent, "utf-8");
2621
+ if (contentSize > MAX_CANVAS_SIZE) {
2622
+ exitWithError(
2623
+ `Content too large: ${contentSize} bytes exceeds maximum of ${MAX_CANVAS_SIZE} bytes (60KB).`
2624
+ );
2625
+ }
2626
+ }
2627
+ const bytesKey = toBytes32(PROFILE_CANVAS_STORAGE_KEY);
2628
+ const chunks = chunkDataForStorage(canvasContent);
2629
+ if (options.encodeOnly) {
2630
+ const readOnlyOptions = parseReadOnlyOptions({
2631
+ chainId: options.chainId,
2632
+ rpcUrl: options.rpcUrl
2633
+ });
2634
+ const encoded = encodeTransaction(
2635
+ {
2636
+ to: CHUNKED_STORAGE_CONTRACT.address,
2637
+ abi: CHUNKED_STORAGE_CONTRACT.abi,
2638
+ functionName: "put",
2639
+ args: [bytesKey, CANVAS_FILENAME, chunks]
2640
+ },
2641
+ readOnlyOptions.chainId
2642
+ );
2643
+ console.log(JSON.stringify(encoded, null, 2));
2644
+ return;
2645
+ }
2646
+ const commonOptions = parseCommonOptions(
2647
+ {
2648
+ privateKey: options.privateKey,
2649
+ chainId: options.chainId,
2650
+ rpcUrl: options.rpcUrl
2651
+ },
2652
+ true
2653
+ // supports --encode-only
2654
+ );
2655
+ try {
2656
+ const account = privateKeyToAccount(commonOptions.privateKey);
2657
+ const rpcUrls = getChainRpcUrls({
2658
+ chainId: commonOptions.chainId,
2659
+ rpcUrl: commonOptions.rpcUrl
2660
+ });
2661
+ const client = createWalletClient({
2662
+ account,
2663
+ chain: base,
2664
+ // TODO: Support other chains
2665
+ transport: http(rpcUrls[0])
2666
+ }).extend(publicActions);
2667
+ console.log(chalk4.blue(`Setting profile canvas...`));
2668
+ console.log(
2669
+ chalk4.gray(` Content size: ${Buffer.byteLength(canvasContent)} bytes`)
2670
+ );
2671
+ console.log(chalk4.gray(` Chunks: ${chunks.length}`));
2672
+ console.log(chalk4.gray(` Address: ${account.address}`));
2673
+ const hash = await client.writeContract({
2674
+ address: CHUNKED_STORAGE_CONTRACT.address,
2675
+ abi: CHUNKED_STORAGE_CONTRACT.abi,
2676
+ functionName: "put",
2677
+ args: [bytesKey, CANVAS_FILENAME, chunks]
2678
+ });
2679
+ console.log(chalk4.blue(`Waiting for confirmation...`));
2680
+ const receipt = await client.waitForTransactionReceipt({ hash });
2681
+ if (receipt.status === "success") {
2682
+ console.log(
2683
+ chalk4.green(
2684
+ `
2685
+ Canvas updated successfully!
2686
+ Transaction: ${hash}
2687
+ Content size: ${Buffer.byteLength(canvasContent)} bytes
2688
+ Chunks: ${chunks.length}`
2689
+ )
2690
+ );
2691
+ } else {
2692
+ exitWithError(`Transaction failed: ${hash}`);
2693
+ }
2694
+ } catch (error) {
2695
+ exitWithError(
2696
+ `Failed to set canvas: ${error instanceof Error ? error.message : String(error)}`
2697
+ );
2698
+ }
2699
+ }
2700
+ function isDataUri(content) {
2701
+ return content.startsWith("data:");
2702
+ }
2703
+ function parseDataUri(dataUri) {
2704
+ const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
2705
+ if (!match) {
2706
+ throw new Error("Invalid data URI format");
2707
+ }
2708
+ const mimeType = match[1];
2709
+ const base64Data = match[2];
2710
+ const buffer = Buffer.from(base64Data, "base64");
2711
+ return { buffer, mimeType };
2712
+ }
2713
+ function getExtensionFromMimeType(mimeType) {
2714
+ const extensions = {
2715
+ "image/png": ".png",
2716
+ "image/jpeg": ".jpg",
2717
+ "image/gif": ".gif",
2718
+ "image/webp": ".webp",
2719
+ "image/svg+xml": ".svg",
2720
+ "application/pdf": ".pdf",
2721
+ "text/html": ".html",
2722
+ "text/css": ".css",
2723
+ "application/javascript": ".js",
2724
+ "application/json": ".json",
2725
+ "text/plain": ".txt",
2726
+ "application/octet-stream": ".bin"
2727
+ };
2728
+ return extensions[mimeType] || ".bin";
2729
+ }
2730
+ async function executeProfileGetCanvas(options) {
2731
+ const readOnlyOptions = parseReadOnlyOptions({
2732
+ chainId: options.chainId,
2733
+ rpcUrl: options.rpcUrl
2734
+ });
2735
+ const client = new StorageClient({
2736
+ chainId: readOnlyOptions.chainId,
2737
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
2738
+ });
2739
+ try {
2740
+ let canvasContent;
2741
+ let canvasText;
2742
+ try {
2743
+ const result = await client.readChunkedStorage({
2744
+ key: PROFILE_CANVAS_STORAGE_KEY,
2745
+ operator: options.address
2746
+ });
2747
+ if (result.data) {
2748
+ canvasContent = result.data;
2749
+ canvasText = result.text;
2750
+ }
2751
+ } catch (error) {
2752
+ const errorMessage = error instanceof Error ? error.message : String(error);
2753
+ if (errorMessage !== "ChunkedStorage metadata not found" && !errorMessage.includes("not found")) {
2754
+ throw error;
2755
+ }
2756
+ }
2757
+ if (options.json) {
2758
+ const output = {
2759
+ address: options.address,
2760
+ chainId: readOnlyOptions.chainId,
2761
+ canvas: canvasContent || null,
2762
+ filename: canvasText || null,
2763
+ hasCanvas: !!canvasContent,
2764
+ isDataUri: canvasContent ? isDataUri(canvasContent) : false,
2765
+ contentLength: canvasContent ? canvasContent.length : 0
2766
+ };
2767
+ console.log(JSON.stringify(output, null, 2));
2768
+ return;
2769
+ }
2770
+ if (!canvasContent) {
2771
+ exitWithError(`No canvas found for address: ${options.address}`);
2772
+ }
2773
+ if (options.output) {
2774
+ const outputPath = path.resolve(options.output);
2775
+ if (isDataUri(canvasContent)) {
2776
+ const { buffer, mimeType } = parseDataUri(canvasContent);
2777
+ let finalPath = outputPath;
2778
+ if (!path.extname(outputPath)) {
2779
+ finalPath = outputPath + getExtensionFromMimeType(mimeType);
2780
+ }
2781
+ fs2.writeFileSync(finalPath, buffer);
2782
+ console.log(
2783
+ chalk4.green(`Canvas written to: ${finalPath} (${buffer.length} bytes)`)
2784
+ );
2785
+ } else {
2786
+ fs2.writeFileSync(outputPath, canvasContent, "utf-8");
2787
+ console.log(
2788
+ chalk4.green(
2789
+ `Canvas written to: ${outputPath} (${canvasContent.length} bytes)`
2790
+ )
2791
+ );
2792
+ }
2793
+ return;
2794
+ }
2795
+ console.log(canvasContent);
2796
+ } catch (error) {
2797
+ exitWithError(
2798
+ `Failed to read canvas: ${error instanceof Error ? error.message : String(error)}`
2799
+ );
2800
+ }
2801
+ }
2434
2802
 
2435
2803
  // src/commands/profile/index.ts
2436
2804
  function registerProfileCommand(program2) {
@@ -2497,9 +2865,73 @@ function registerProfileCommand(program2) {
2497
2865
  encodeOnly: options.encodeOnly
2498
2866
  });
2499
2867
  });
2868
+ const setBioCommand = new Command("set-bio").description("Set your profile bio").requiredOption("--bio <bio>", "Your profile bio (max 280 characters)").option(
2869
+ "--private-key <key>",
2870
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
2871
+ ).option(
2872
+ "--chain-id <id>",
2873
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
2874
+ (value) => parseInt(value, 10)
2875
+ ).option(
2876
+ "--rpc-url <url>",
2877
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
2878
+ ).option(
2879
+ "--encode-only",
2880
+ "Output transaction data as JSON instead of executing"
2881
+ ).action(async (options) => {
2882
+ await executeProfileSetBio({
2883
+ bio: options.bio,
2884
+ privateKey: options.privateKey,
2885
+ chainId: options.chainId,
2886
+ rpcUrl: options.rpcUrl,
2887
+ encodeOnly: options.encodeOnly
2888
+ });
2889
+ });
2890
+ 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(
2891
+ "--private-key <key>",
2892
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
2893
+ ).option(
2894
+ "--chain-id <id>",
2895
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
2896
+ (value) => parseInt(value, 10)
2897
+ ).option(
2898
+ "--rpc-url <url>",
2899
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
2900
+ ).option(
2901
+ "--encode-only",
2902
+ "Output transaction data as JSON instead of executing"
2903
+ ).action(async (options) => {
2904
+ await executeProfileSetCanvas({
2905
+ file: options.file,
2906
+ content: options.content,
2907
+ privateKey: options.privateKey,
2908
+ chainId: options.chainId,
2909
+ rpcUrl: options.rpcUrl,
2910
+ encodeOnly: options.encodeOnly
2911
+ });
2912
+ });
2913
+ 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(
2914
+ "--chain-id <id>",
2915
+ "Chain ID. Can also be set via NET_CHAIN_ID env var",
2916
+ (value) => parseInt(value, 10)
2917
+ ).option(
2918
+ "--rpc-url <url>",
2919
+ "Custom RPC URL. Can also be set via NET_RPC_URL env var"
2920
+ ).option("--json", "Output in JSON format").action(async (options) => {
2921
+ await executeProfileGetCanvas({
2922
+ address: options.address,
2923
+ output: options.output,
2924
+ chainId: options.chainId,
2925
+ rpcUrl: options.rpcUrl,
2926
+ json: options.json
2927
+ });
2928
+ });
2500
2929
  profileCommand.addCommand(getCommand);
2501
2930
  profileCommand.addCommand(setPictureCommand);
2502
2931
  profileCommand.addCommand(setUsernameCommand);
2932
+ profileCommand.addCommand(setBioCommand);
2933
+ profileCommand.addCommand(setCanvasCommand);
2934
+ profileCommand.addCommand(getCanvasCommand);
2503
2935
  }
2504
2936
 
2505
2937
  // src/cli/index.ts