@net-protocol/cli 0.1.7 → 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.
package/README.md CHANGED
@@ -364,6 +364,8 @@ Profile operations for managing your Net Protocol profile.
364
364
  - `profile set-picture` - Set your profile picture URL
365
365
  - `profile set-x-username` - Set your X (Twitter) username
366
366
  - `profile set-bio` - Set your profile bio
367
+ - `profile set-canvas` - Set your profile canvas (HTML content)
368
+ - `profile get-canvas` - Get profile canvas for an address
367
369
 
368
370
  ##### Profile Get
369
371
 
@@ -513,6 +515,99 @@ netp profile set-bio \
513
515
  --encode-only
514
516
  ```
515
517
 
518
+ ##### Profile Set Canvas
519
+
520
+ Set your profile canvas (HTML content, max 60KB). Canvas content is stored using ChunkedStorage with gzip compression.
521
+
522
+ ```bash
523
+ netp profile set-canvas \
524
+ --file <path> | --content <html> \
525
+ [--private-key <0x...>] \
526
+ [--chain-id <8453|1|...>] \
527
+ [--rpc-url <custom-rpc>] \
528
+ [--encode-only]
529
+ ```
530
+
531
+ **Profile Set Canvas Arguments:**
532
+
533
+ - `--file` (optional): Path to file containing canvas content (HTML, images, etc.)
534
+ - `--content` (optional): HTML content for canvas (inline). Must provide either `--file` or `--content`, but not both.
535
+ - `--private-key` (optional): Private key. Can also be set via `NET_PRIVATE_KEY` environment variable
536
+ - `--chain-id` (optional): Chain ID. Can also be set via `NET_CHAIN_ID` environment variable
537
+ - `--rpc-url` (optional): Custom RPC URL. Can also be set via `NET_RPC_URL` environment variable
538
+ - `--encode-only` (optional): Output transaction data as JSON instead of executing
539
+
540
+ **Notes:**
541
+ - Maximum canvas size is 60KB
542
+ - Binary files (images) are automatically converted to data URIs
543
+ - Content is compressed using gzip before storage
544
+
545
+ **Example:**
546
+
547
+ ```bash
548
+ # Set canvas from file
549
+ netp profile set-canvas \
550
+ --file ./my-canvas.html \
551
+ --chain-id 8453
552
+
553
+ # Set canvas from inline content
554
+ netp profile set-canvas \
555
+ --content "<html><body><h1>My Profile</h1></body></html>" \
556
+ --chain-id 8453
557
+
558
+ # Encode-only
559
+ netp profile set-canvas \
560
+ --file ./my-canvas.html \
561
+ --chain-id 8453 \
562
+ --encode-only
563
+ ```
564
+
565
+ ##### Profile Get Canvas
566
+
567
+ Get profile canvas for an address.
568
+
569
+ ```bash
570
+ netp profile get-canvas \
571
+ --address <wallet-address> \
572
+ [--output <path>] \
573
+ [--chain-id <8453|1|...>] \
574
+ [--rpc-url <custom-rpc>] \
575
+ [--json]
576
+ ```
577
+
578
+ **Profile Get Canvas Arguments:**
579
+
580
+ - `--address` (required): Wallet address to get canvas for
581
+ - `--output` (optional): Write canvas content to file instead of stdout
582
+ - `--chain-id` (optional): Chain ID. Can also be set via `NET_CHAIN_ID` environment variable
583
+ - `--rpc-url` (optional): Custom RPC URL. Can also be set via `NET_RPC_URL` environment variable
584
+ - `--json` (optional): Output in JSON format (includes metadata like size and type)
585
+
586
+ **Notes:**
587
+ - Binary content (data URIs) is automatically converted to binary files when using `--output`
588
+ - JSON output includes: canvas content, filename, size, and whether it's a data URI
589
+
590
+ **Example:**
591
+
592
+ ```bash
593
+ # Output to stdout
594
+ netp profile get-canvas \
595
+ --address 0x1234567890abcdef1234567890abcdef12345678 \
596
+ --chain-id 8453
597
+
598
+ # Save to file
599
+ netp profile get-canvas \
600
+ --address 0x1234567890abcdef1234567890abcdef12345678 \
601
+ --output ./canvas.html \
602
+ --chain-id 8453
603
+
604
+ # JSON output
605
+ netp profile get-canvas \
606
+ --address 0x1234567890abcdef1234567890abcdef12345678 \
607
+ --chain-id 8453 \
608
+ --json
609
+ ```
610
+
516
611
  #### Info Command
517
612
 
518
613
  Show contract info and stats.
@@ -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, isValidBio, getProfileMetadataStorageArgs } 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);
@@ -1191,7 +1192,7 @@ async function encodeStorageUpload(options) {
1191
1192
  } else {
1192
1193
  operatorAddress = "0x0000000000000000000000000000000000000000";
1193
1194
  }
1194
- const fileContent = fs.readFileSync(options.filePath, "utf-8");
1195
+ const fileContent = fs2.readFileSync(options.filePath, "utf-8");
1195
1196
  const fileSize = Buffer.byteLength(fileContent, "utf-8");
1196
1197
  const client = new StorageClient({
1197
1198
  chainId: readOnlyOptions.chainId
@@ -1475,8 +1476,8 @@ function registerStorageCommand(program2) {
1475
1476
  console.log(chalk4.blue(`\u{1F4C1} Reading file: ${options.file}`));
1476
1477
  console.log(chalk4.blue(`\u{1F517} Using relay API: ${options.apiUrl}`));
1477
1478
  const result = await uploadFileWithRelay(uploadRelayOptions);
1478
- const { privateKeyToAccount: privateKeyToAccount9 } = await import('viem/accounts');
1479
- const userAccount = privateKeyToAccount9(commonOptions.privateKey);
1479
+ const { privateKeyToAccount: privateKeyToAccount10 } = await import('viem/accounts');
1480
+ const userAccount = privateKeyToAccount10(commonOptions.privateKey);
1480
1481
  const storageUrl = generateStorageUrl(
1481
1482
  userAccount.address,
1482
1483
  commonOptions.chainId,
@@ -2274,7 +2275,24 @@ async function executeProfileGet(options) {
2274
2275
  throw error;
2275
2276
  }
2276
2277
  }
2277
- const hasProfile = profilePicture || xUsername || bio;
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;
2278
2296
  if (options.json) {
2279
2297
  const output = {
2280
2298
  address: options.address,
@@ -2282,6 +2300,7 @@ async function executeProfileGet(options) {
2282
2300
  profilePicture: profilePicture || null,
2283
2301
  xUsername: xUsername || null,
2284
2302
  bio: bio || null,
2303
+ canvas: canvasSize ? { size: canvasSize, isDataUri: canvasIsDataUri } : null,
2285
2304
  hasProfile
2286
2305
  };
2287
2306
  console.log(JSON.stringify(output, null, 2));
@@ -2299,6 +2318,9 @@ async function executeProfileGet(options) {
2299
2318
  console.log(
2300
2319
  ` ${chalk4.cyan("Bio:")} ${bio || chalk4.gray("(not set)")}`
2301
2320
  );
2321
+ console.log(
2322
+ ` ${chalk4.cyan("Canvas:")} ${canvasSize ? `${canvasSize} bytes${canvasIsDataUri ? " (data URI)" : ""}` : chalk4.gray("(not set)")}`
2323
+ );
2302
2324
  if (!hasProfile) {
2303
2325
  console.log(chalk4.yellow("\n No profile data found for this address."));
2304
2326
  }
@@ -2532,6 +2554,251 @@ Bio updated successfully!
2532
2554
  );
2533
2555
  }
2534
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
+ }
2535
2802
 
2536
2803
  // src/commands/profile/index.ts
2537
2804
  function registerProfileCommand(program2) {
@@ -2620,10 +2887,51 @@ function registerProfileCommand(program2) {
2620
2887
  encodeOnly: options.encodeOnly
2621
2888
  });
2622
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
+ });
2623
2929
  profileCommand.addCommand(getCommand);
2624
2930
  profileCommand.addCommand(setPictureCommand);
2625
2931
  profileCommand.addCommand(setUsernameCommand);
2626
2932
  profileCommand.addCommand(setBioCommand);
2933
+ profileCommand.addCommand(setCanvasCommand);
2934
+ profileCommand.addCommand(getCanvasCommand);
2627
2935
  }
2628
2936
 
2629
2937
  // src/cli/index.ts