@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.
- package/README.md +132 -0
- package/dist/cli/index.mjs +467 -35
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/cli/index.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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:
|
|
1474
|
-
const userAccount =
|
|
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
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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
|