@net-protocol/cli 0.1.0

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,1291 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { Command } from 'commander';
4
+ import chalk2 from 'chalk';
5
+ import { readFileSync } from 'fs';
6
+ import { detectFileTypeFromBase64, base64ToDataUri, StorageClient, shouldSuggestXmlStorage, getStorageKeyBytes, encodeStorageKeyForUrl, STORAGE_CONTRACT, CHUNKED_STORAGE_CONTRACT } from '@net-protocol/storage';
7
+ import { stringToHex, createWalletClient, http, hexToString, defineChain } from 'viem';
8
+ import { privateKeyToAccount } from 'viem/accounts';
9
+ import { getPublicClient, getChainRpcUrls } from '@net-protocol/core';
10
+ import { createRelayX402Client, createRelaySession, checkBackendWalletBalance, fundBackendWallet, batchTransactions, submitTransactionsViaRelay, waitForConfirmations, retryFailedTransactions as retryFailedTransactions$1 } from '@net-protocol/relay';
11
+
12
+ function parseCommonOptions(options) {
13
+ const privateKey = options.privateKey || process.env.NET_PRIVATE_KEY || process.env.PRIVATE_KEY;
14
+ if (!privateKey) {
15
+ console.error(
16
+ chalk2.red(
17
+ "Error: Private key is required. Provide via --private-key flag or NET_PRIVATE_KEY/PRIVATE_KEY environment variable"
18
+ )
19
+ );
20
+ process.exit(1);
21
+ }
22
+ if (!privateKey.startsWith("0x") || privateKey.length !== 66) {
23
+ console.error(
24
+ chalk2.red(
25
+ "Error: Invalid private key format (must be 0x-prefixed, 66 characters)"
26
+ )
27
+ );
28
+ process.exit(1);
29
+ }
30
+ if (options.privateKey) {
31
+ console.warn(
32
+ chalk2.yellow(
33
+ "\u26A0\uFE0F Warning: Private key provided via command line. Consider using NET_PRIVATE_KEY environment variable instead."
34
+ )
35
+ );
36
+ }
37
+ const chainId = options.chainId || (process.env.NET_CHAIN_ID ? parseInt(process.env.NET_CHAIN_ID, 10) : void 0);
38
+ if (!chainId) {
39
+ console.error(
40
+ chalk2.red(
41
+ "Error: Chain ID is required. Provide via --chain-id flag or NET_CHAIN_ID environment variable"
42
+ )
43
+ );
44
+ process.exit(1);
45
+ }
46
+ const rpcUrl = options.rpcUrl || process.env.NET_RPC_URL;
47
+ return {
48
+ privateKey,
49
+ chainId,
50
+ rpcUrl
51
+ };
52
+ }
53
+ async function checkNormalStorageExists(params) {
54
+ const { storageClient, storageKey, operatorAddress, expectedContent } = params;
55
+ const existing = await storageClient.get({
56
+ key: storageKey,
57
+ operator: operatorAddress
58
+ });
59
+ if (!existing) {
60
+ return { exists: false };
61
+ }
62
+ const storedContent = hexToString(existing.value);
63
+ const matches = storedContent === expectedContent;
64
+ return { exists: true, matches };
65
+ }
66
+ async function checkChunkedStorageExists(params) {
67
+ const { storageClient, chunkedHash, operatorAddress } = params;
68
+ const meta = await storageClient.getChunkedMetadata({
69
+ key: chunkedHash,
70
+ operator: operatorAddress
71
+ });
72
+ return meta !== null && meta.chunkCount > 0;
73
+ }
74
+ async function checkXmlChunksExist(params) {
75
+ const { storageClient, chunkedHashes, operatorAddress } = params;
76
+ const existing = /* @__PURE__ */ new Set();
77
+ await Promise.all(
78
+ chunkedHashes.map(async (hash) => {
79
+ const exists = await checkChunkedStorageExists({
80
+ storageClient,
81
+ chunkedHash: hash,
82
+ operatorAddress
83
+ });
84
+ if (exists) {
85
+ existing.add(hash);
86
+ }
87
+ })
88
+ );
89
+ return existing;
90
+ }
91
+ async function checkXmlMetadataExists(params) {
92
+ const { storageClient, storageKey, operatorAddress, expectedMetadata } = params;
93
+ const existing = await storageClient.get({
94
+ key: storageKey,
95
+ operator: operatorAddress
96
+ });
97
+ if (!existing) {
98
+ return { exists: false };
99
+ }
100
+ const storedMetadata = hexToString(existing.value);
101
+ const matches = storedMetadata === expectedMetadata;
102
+ return { exists: true, matches };
103
+ }
104
+
105
+ // src/commands/storage/utils.ts
106
+ function typedArgsToArray(args) {
107
+ if (args.type === "normal" || args.type === "metadata") {
108
+ return [args.args.key, args.args.text, args.args.value];
109
+ } else {
110
+ return [args.args.hash, args.args.text, args.args.chunks];
111
+ }
112
+ }
113
+ function extractTypedArgsFromTransaction(tx, type) {
114
+ if (type === "normal" || type === "metadata") {
115
+ return {
116
+ type,
117
+ args: {
118
+ key: tx.args[0],
119
+ text: tx.args[1],
120
+ value: tx.args[2]
121
+ }
122
+ };
123
+ } else {
124
+ return {
125
+ type: "chunked",
126
+ args: {
127
+ hash: tx.args[0],
128
+ text: tx.args[1],
129
+ chunks: tx.args[2]
130
+ }
131
+ };
132
+ }
133
+ }
134
+ function generateStorageUrl(operatorAddress, chainId, storageKey) {
135
+ if (!operatorAddress) return void 0;
136
+ return `https://storedon.net/net/${chainId}/storage/load/${operatorAddress}/${encodeStorageKeyForUrl(storageKey)}`;
137
+ }
138
+ async function checkTransactionExists(params) {
139
+ const { storageClient, tx, operatorAddress } = params;
140
+ if (tx.type === "normal") {
141
+ if (tx.typedArgs.type === "normal") {
142
+ const expectedContent = hexToString(tx.typedArgs.args.value);
143
+ const check = await checkNormalStorageExists({
144
+ storageClient,
145
+ storageKey: tx.id,
146
+ operatorAddress,
147
+ expectedContent
148
+ });
149
+ return check.exists && check.matches === true;
150
+ }
151
+ } else if (tx.type === "chunked") {
152
+ return await checkChunkedStorageExists({
153
+ storageClient,
154
+ chunkedHash: tx.id,
155
+ operatorAddress
156
+ });
157
+ } else if (tx.type === "metadata") {
158
+ if (tx.typedArgs.type === "metadata") {
159
+ const expectedMetadata = hexToString(tx.typedArgs.args.value);
160
+ const check = await checkXmlMetadataExists({
161
+ storageClient,
162
+ storageKey: tx.id,
163
+ operatorAddress,
164
+ expectedMetadata
165
+ });
166
+ return check.exists && check.matches === true;
167
+ }
168
+ }
169
+ return false;
170
+ }
171
+
172
+ // src/commands/storage/transactions/prep.ts
173
+ function prepareNormalStorageTransaction(storageClient, args, originalStorageKey) {
174
+ const content = hexToString(args.value);
175
+ const transaction = storageClient.preparePut({
176
+ key: originalStorageKey,
177
+ text: args.text,
178
+ value: content
179
+ });
180
+ const typedArgs = {
181
+ type: "normal",
182
+ args
183
+ };
184
+ return {
185
+ id: args.key,
186
+ type: "normal",
187
+ transaction,
188
+ typedArgs
189
+ };
190
+ }
191
+ function prepareXmlStorageTransactions(params) {
192
+ const { storageClient, storageKey, text, content, operatorAddress } = params;
193
+ const storageKeyBytes = getStorageKeyBytes(storageKey);
194
+ const result = storageClient.prepareXmlStorage({
195
+ data: content,
196
+ operatorAddress,
197
+ storageKey,
198
+ // Pass as string, not bytes32
199
+ filename: text,
200
+ useChunkedStorageBackend: true
201
+ // Use ChunkedStorage backend (default)
202
+ });
203
+ const transactions = result.transactionConfigs.map(
204
+ (tx, index) => {
205
+ if (index === 0) {
206
+ const typedArgs = extractTypedArgsFromTransaction(tx, "metadata");
207
+ return {
208
+ id: storageKeyBytes,
209
+ type: "metadata",
210
+ transaction: tx,
211
+ typedArgs
212
+ };
213
+ } else {
214
+ const typedArgs = extractTypedArgsFromTransaction(tx, "chunked");
215
+ if (typedArgs.type === "chunked") {
216
+ const chunkedHash = typedArgs.args.hash;
217
+ return {
218
+ id: chunkedHash,
219
+ type: "chunked",
220
+ transaction: tx,
221
+ typedArgs
222
+ };
223
+ }
224
+ throw new Error("Invalid chunked transaction");
225
+ }
226
+ }
227
+ );
228
+ return transactions;
229
+ }
230
+ async function filterExistingTransactions(params) {
231
+ const { storageClient, transactions, operatorAddress, expectedContent } = params;
232
+ const toSend = [];
233
+ const skipped = [];
234
+ for (const tx of transactions) {
235
+ let exists = false;
236
+ if (tx.type === "normal") {
237
+ if (expectedContent) {
238
+ const check = await checkNormalStorageExists({
239
+ storageClient,
240
+ storageKey: tx.id,
241
+ operatorAddress,
242
+ expectedContent
243
+ });
244
+ exists = check.exists && check.matches === true;
245
+ } else {
246
+ if (tx.typedArgs.type === "normal") {
247
+ const storedContent = hexToString(tx.typedArgs.args.value);
248
+ const check = await checkNormalStorageExists({
249
+ storageClient,
250
+ storageKey: tx.id,
251
+ operatorAddress,
252
+ expectedContent: storedContent
253
+ });
254
+ exists = check.exists && check.matches === true;
255
+ }
256
+ }
257
+ } else if (tx.type === "chunked") {
258
+ exists = await checkChunkedStorageExists({
259
+ storageClient,
260
+ chunkedHash: tx.id,
261
+ operatorAddress
262
+ });
263
+ } else if (tx.type === "metadata") {
264
+ if (tx.typedArgs.type === "metadata") {
265
+ const expectedMetadata = hexToString(tx.typedArgs.args.value);
266
+ const check = await checkXmlMetadataExists({
267
+ storageClient,
268
+ storageKey: tx.id,
269
+ operatorAddress,
270
+ expectedMetadata
271
+ });
272
+ exists = check.exists && check.matches === true;
273
+ }
274
+ }
275
+ if (exists) {
276
+ skipped.push(tx);
277
+ } else {
278
+ toSend.push(tx);
279
+ }
280
+ }
281
+ return { toSend, skipped };
282
+ }
283
+ async function filterXmlStorageTransactions(params) {
284
+ const { storageClient, transactions, operatorAddress } = params;
285
+ const metadataTx = transactions.find(
286
+ (tx) => tx.to.toLowerCase() === STORAGE_CONTRACT.address.toLowerCase()
287
+ );
288
+ const chunkTxs = transactions.filter(
289
+ (tx) => tx.to.toLowerCase() === CHUNKED_STORAGE_CONTRACT.address.toLowerCase()
290
+ );
291
+ const chunkedHashes = [];
292
+ for (const tx of chunkTxs) {
293
+ const typedArgs = extractTypedArgsFromTransaction(tx, "chunked");
294
+ if (typedArgs.type === "chunked") {
295
+ chunkedHashes.push(typedArgs.args.hash);
296
+ }
297
+ }
298
+ const toSend = [];
299
+ const skipped = [];
300
+ const existingChunks = await checkXmlChunksExist({
301
+ storageClient,
302
+ chunkedHashes,
303
+ operatorAddress
304
+ });
305
+ for (const tx of chunkTxs) {
306
+ const typedArgs = extractTypedArgsFromTransaction(tx, "chunked");
307
+ if (typedArgs.type === "chunked") {
308
+ const hash = typedArgs.args.hash;
309
+ if (existingChunks.has(hash)) {
310
+ skipped.push(tx);
311
+ } else {
312
+ toSend.push(tx);
313
+ }
314
+ }
315
+ }
316
+ if (metadataTx) {
317
+ const allChunksExist = chunkedHashes.length > 0 && chunkedHashes.every(
318
+ (hash) => existingChunks.has(hash)
319
+ );
320
+ if (allChunksExist) {
321
+ try {
322
+ const typedArgs = extractTypedArgsFromTransaction(metadataTx, "metadata");
323
+ if (typedArgs.type === "metadata") {
324
+ const expectedMetadata = hexToString(typedArgs.args.value);
325
+ const check = await checkXmlMetadataExists({
326
+ storageClient,
327
+ storageKey: typedArgs.args.key,
328
+ operatorAddress,
329
+ expectedMetadata
330
+ });
331
+ if (check.exists && check.matches) {
332
+ skipped.push(metadataTx);
333
+ } else {
334
+ toSend.unshift(metadataTx);
335
+ }
336
+ }
337
+ } catch {
338
+ toSend.unshift(metadataTx);
339
+ }
340
+ } else {
341
+ toSend.unshift(metadataTx);
342
+ }
343
+ }
344
+ return { toSend, skipped };
345
+ }
346
+ function createWalletClientFromPrivateKey(params) {
347
+ const { privateKey, chainId, rpcUrl } = params;
348
+ const account = privateKeyToAccount(privateKey);
349
+ const publicClient = getPublicClient({ chainId, rpcUrl });
350
+ const rpcUrls = getChainRpcUrls({ chainId, rpcUrl });
351
+ const chain = publicClient.chain ? defineChain({
352
+ id: chainId,
353
+ name: publicClient.chain.name,
354
+ nativeCurrency: publicClient.chain.nativeCurrency,
355
+ rpcUrls: {
356
+ default: { http: rpcUrls },
357
+ public: { http: rpcUrls }
358
+ },
359
+ blockExplorers: publicClient.chain.blockExplorers
360
+ }) : defineChain({
361
+ id: chainId,
362
+ name: `Chain ${chainId}`,
363
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
364
+ rpcUrls: {
365
+ default: { http: rpcUrls },
366
+ public: { http: rpcUrls }
367
+ }
368
+ });
369
+ const walletClient = createWalletClient({
370
+ account,
371
+ chain,
372
+ transport: http()
373
+ });
374
+ return {
375
+ walletClient,
376
+ publicClient,
377
+ operatorAddress: account.address
378
+ };
379
+ }
380
+ async function sendTransactionsWithIdempotency(params) {
381
+ const { storageClient, walletClient, publicClient, transactions, operatorAddress } = params;
382
+ let sent = 0;
383
+ let skipped = 0;
384
+ let failed = 0;
385
+ let finalHash;
386
+ const errorMessages = [];
387
+ for (let i = 0; i < transactions.length; i++) {
388
+ const tx = transactions[i];
389
+ try {
390
+ const exists = await checkTransactionExists({
391
+ storageClient,
392
+ tx,
393
+ operatorAddress
394
+ });
395
+ if (exists) {
396
+ console.log(
397
+ `\u23ED\uFE0F Transaction ${i + 1}/${transactions.length} skipped (already stored): ${tx.id}`
398
+ );
399
+ skipped++;
400
+ continue;
401
+ }
402
+ console.log(
403
+ `\u{1F4E4} Sending transaction ${i + 1}/${transactions.length}: ${tx.id}`
404
+ );
405
+ const args = typedArgsToArray(tx.typedArgs);
406
+ if (!walletClient.account) {
407
+ throw new Error("Wallet client must have an account");
408
+ }
409
+ const hash = await walletClient.writeContract({
410
+ account: walletClient.account,
411
+ address: tx.transaction.to,
412
+ abi: tx.transaction.abi,
413
+ functionName: tx.transaction.functionName,
414
+ args,
415
+ value: tx.transaction.value,
416
+ chain: null
417
+ });
418
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
419
+ console.log(
420
+ `\u2713 Transaction ${i + 1} confirmed in block ${receipt.blockNumber} (hash: ${hash})`
421
+ );
422
+ sent++;
423
+ finalHash = hash;
424
+ } catch (error) {
425
+ const errorMsg = error instanceof Error ? error.message : String(error);
426
+ console.error(`\u2717 Transaction ${i + 1} failed: ${errorMsg}`);
427
+ errorMessages.push(errorMsg);
428
+ failed++;
429
+ }
430
+ }
431
+ return {
432
+ success: failed === 0,
433
+ skipped: skipped > 0,
434
+ transactionsSent: sent,
435
+ transactionsSkipped: skipped,
436
+ transactionsFailed: failed,
437
+ finalHash,
438
+ error: errorMessages.length > 0 ? errorMessages.join("; ") : void 0
439
+ };
440
+ }
441
+
442
+ // src/commands/storage/core/upload.ts
443
+ async function uploadFile(options) {
444
+ const fileBuffer = readFileSync(options.filePath);
445
+ const isBinary = fileBuffer.some(
446
+ (byte) => byte === 0 || byte < 32 && byte !== 9 && byte !== 10 && byte !== 13
447
+ );
448
+ let fileContent;
449
+ if (isBinary) {
450
+ const base64String = fileBuffer.toString("base64");
451
+ const detectedType = detectFileTypeFromBase64(base64String);
452
+ if (detectedType) {
453
+ fileContent = base64ToDataUri(base64String);
454
+ } else {
455
+ fileContent = base64String;
456
+ }
457
+ } else {
458
+ fileContent = fileBuffer.toString("utf-8");
459
+ }
460
+ const storageClient = new StorageClient({
461
+ chainId: options.chainId,
462
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
463
+ });
464
+ const { walletClient, publicClient, operatorAddress } = createWalletClientFromPrivateKey({
465
+ privateKey: options.privateKey,
466
+ chainId: options.chainId,
467
+ rpcUrl: options.rpcUrl
468
+ });
469
+ const useXmlStorage = shouldSuggestXmlStorage(fileContent);
470
+ const storageType = useXmlStorage ? "xml" : "normal";
471
+ let transactions;
472
+ if (useXmlStorage) {
473
+ transactions = prepareXmlStorageTransactions({
474
+ storageClient,
475
+ storageKey: options.storageKey,
476
+ text: options.text,
477
+ content: fileContent,
478
+ operatorAddress
479
+ });
480
+ } else {
481
+ const storageKeyBytes = getStorageKeyBytes(
482
+ options.storageKey
483
+ );
484
+ const typedArgs = {
485
+ key: storageKeyBytes,
486
+ text: options.text,
487
+ value: stringToHex(fileContent)
488
+ };
489
+ transactions = [
490
+ prepareNormalStorageTransaction(
491
+ storageClient,
492
+ typedArgs,
493
+ options.storageKey
494
+ // Pass original string key for preparePut
495
+ )
496
+ ];
497
+ }
498
+ let transactionsToSend;
499
+ let skippedCount = 0;
500
+ if (useXmlStorage) {
501
+ const chunkTransactions = transactions.filter((tx) => tx.type === "chunked").map((tx) => tx.transaction);
502
+ const txConfigToTxWithId = new Map(
503
+ transactions.filter((tx) => tx.type === "chunked").map((tx) => [tx.transaction, tx])
504
+ );
505
+ const filtered = await filterXmlStorageTransactions({
506
+ storageClient,
507
+ transactions: chunkTransactions,
508
+ // Only chunk transactions
509
+ operatorAddress
510
+ });
511
+ const filteredToSend = filtered.toSend.map((txConfig) => txConfigToTxWithId.get(txConfig)).filter((tx) => tx !== void 0);
512
+ const filteredSkipped = filtered.skipped.map((txConfig) => txConfigToTxWithId.get(txConfig)).filter((tx) => tx !== void 0);
513
+ const metadataTx = transactions.find((tx) => tx.type === "metadata");
514
+ if (metadataTx) {
515
+ filteredToSend.unshift(metadataTx);
516
+ }
517
+ transactionsToSend = filteredToSend;
518
+ skippedCount = filteredSkipped.length;
519
+ } else {
520
+ const filtered = await filterExistingTransactions({
521
+ storageClient,
522
+ transactions,
523
+ operatorAddress,
524
+ expectedContent: fileContent
525
+ });
526
+ transactionsToSend = filtered.toSend;
527
+ skippedCount = filtered.skipped.length;
528
+ }
529
+ if (transactionsToSend.length === 0) {
530
+ return {
531
+ success: true,
532
+ skipped: true,
533
+ transactionsSent: 0,
534
+ transactionsSkipped: skippedCount,
535
+ transactionsFailed: 0,
536
+ operatorAddress,
537
+ storageType
538
+ };
539
+ }
540
+ const result = await sendTransactionsWithIdempotency({
541
+ storageClient,
542
+ walletClient,
543
+ publicClient,
544
+ transactions: transactionsToSend,
545
+ operatorAddress
546
+ });
547
+ result.transactionsSkipped += skippedCount;
548
+ result.operatorAddress = operatorAddress;
549
+ result.storageType = storageType;
550
+ return result;
551
+ }
552
+
553
+ // src/commands/storage/relay/recheckStorage.ts
554
+ async function recheckFailedTransactionsStorage(failedIndexes, transactions, storageClient, backendWalletAddress) {
555
+ if (failedIndexes.length === 0) {
556
+ return [];
557
+ }
558
+ const failedTransactions = failedIndexes.map((idx) => transactions[idx]);
559
+ const chunkedHashes = [];
560
+ for (const tx of failedTransactions) {
561
+ try {
562
+ const typedArgs = extractTypedArgsFromTransaction(tx, "chunked");
563
+ if (typedArgs.type === "chunked") {
564
+ chunkedHashes.push(typedArgs.args.hash);
565
+ }
566
+ } catch {
567
+ }
568
+ }
569
+ if (chunkedHashes.length === 0) {
570
+ return failedIndexes;
571
+ }
572
+ const existingChunks = await checkXmlChunksExist({
573
+ storageClient,
574
+ chunkedHashes,
575
+ operatorAddress: backendWalletAddress
576
+ });
577
+ const stillFailed = [];
578
+ for (const failedIdx of failedIndexes) {
579
+ const tx = transactions[failedIdx];
580
+ try {
581
+ const typedArgs = extractTypedArgsFromTransaction(tx, "chunked");
582
+ if (typedArgs.type === "chunked") {
583
+ const hash = typedArgs.args.hash;
584
+ if (!existingChunks.has(hash)) {
585
+ stillFailed.push(failedIdx);
586
+ }
587
+ } else {
588
+ stillFailed.push(failedIdx);
589
+ }
590
+ } catch {
591
+ stillFailed.push(failedIdx);
592
+ }
593
+ }
594
+ return stillFailed;
595
+ }
596
+
597
+ // src/commands/storage/relay/retry.ts
598
+ async function retryFailedTransactions(params) {
599
+ const {
600
+ storageClient,
601
+ backendWalletAddress,
602
+ apiUrl,
603
+ chainId,
604
+ operatorAddress,
605
+ secretKey,
606
+ failedIndexes,
607
+ originalTransactions,
608
+ config,
609
+ sessionToken
610
+ } = params;
611
+ return retryFailedTransactions$1({
612
+ apiUrl,
613
+ chainId,
614
+ operatorAddress,
615
+ secretKey,
616
+ failedIndexes,
617
+ originalTransactions,
618
+ backendWalletAddress,
619
+ config,
620
+ sessionToken,
621
+ recheckFunction: async (failedIndexes2, transactions, backendWalletAddress2) => {
622
+ return recheckFailedTransactionsStorage(
623
+ failedIndexes2,
624
+ transactions,
625
+ storageClient,
626
+ backendWalletAddress2
627
+ );
628
+ }
629
+ });
630
+ }
631
+
632
+ // src/commands/storage/core/uploadRelay.ts
633
+ async function uploadFileWithRelay(options) {
634
+ const errors = [];
635
+ const fileBuffer = readFileSync(options.filePath);
636
+ const isBinary = fileBuffer.some(
637
+ (byte) => byte === 0 || byte < 32 && byte !== 9 && byte !== 10 && byte !== 13
638
+ );
639
+ let fileContent;
640
+ if (isBinary) {
641
+ const base64String = fileBuffer.toString("base64");
642
+ const detectedType = detectFileTypeFromBase64(base64String);
643
+ if (detectedType) {
644
+ fileContent = base64ToDataUri(base64String);
645
+ } else {
646
+ fileContent = base64String;
647
+ }
648
+ } else {
649
+ fileContent = fileBuffer.toString("utf-8");
650
+ }
651
+ const storageClient = new StorageClient({
652
+ chainId: options.chainId,
653
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
654
+ });
655
+ const userAccount = privateKeyToAccount(options.privateKey);
656
+ const userAddress = userAccount.address;
657
+ const publicClient = getPublicClient({
658
+ chainId: options.chainId,
659
+ rpcUrl: options.rpcUrl
660
+ });
661
+ const rpcUrls = getChainRpcUrls({
662
+ chainId: options.chainId,
663
+ rpcUrl: options.rpcUrl
664
+ });
665
+ const userWalletClient = createWalletClient({
666
+ account: userAccount,
667
+ chain: publicClient.chain,
668
+ transport: http(rpcUrls[0])
669
+ // Use first RPC URL
670
+ });
671
+ const { fetchWithPayment, httpClient } = createRelayX402Client(
672
+ userAccount,
673
+ options.chainId
674
+ );
675
+ const sessionResult = await createRelaySession({
676
+ apiUrl: options.apiUrl,
677
+ chainId: options.chainId,
678
+ operatorAddress: userAddress,
679
+ secretKey: options.secretKey,
680
+ account: userAccount,
681
+ expiresIn: 3600
682
+ // 1 hour - should be enough for most uploads
683
+ });
684
+ const sessionToken = sessionResult.sessionToken;
685
+ console.log("\u2713 Session token created (valid for 1 hour)");
686
+ let backendWalletAddress;
687
+ let shouldFund = true;
688
+ try {
689
+ const balanceResult = await checkBackendWalletBalance({
690
+ apiUrl: options.apiUrl,
691
+ chainId: options.chainId,
692
+ operatorAddress: userAddress,
693
+ secretKey: options.secretKey
694
+ });
695
+ backendWalletAddress = balanceResult.backendWalletAddress;
696
+ shouldFund = !balanceResult.sufficientBalance;
697
+ } catch (error) {
698
+ shouldFund = true;
699
+ }
700
+ if (shouldFund) {
701
+ const fundResult = await fundBackendWallet({
702
+ apiUrl: options.apiUrl,
703
+ chainId: options.chainId,
704
+ operatorAddress: userAddress,
705
+ secretKey: options.secretKey,
706
+ fetchWithPayment,
707
+ httpClient
708
+ });
709
+ backendWalletAddress = fundResult.backendWalletAddress;
710
+ }
711
+ if (!backendWalletAddress) {
712
+ throw new Error("Failed to determine backend wallet address");
713
+ }
714
+ const chunkPrepareResult = storageClient.prepareXmlStorage({
715
+ data: fileContent,
716
+ operatorAddress: backendWalletAddress,
717
+ storageKey: options.storageKey,
718
+ filename: options.text,
719
+ useChunkedStorageBackend: true
720
+ });
721
+ const chunkTxs = chunkPrepareResult.transactionConfigs.slice(1);
722
+ const topLevelHash = chunkPrepareResult.topLevelHash;
723
+ const chunkMetadata = chunkPrepareResult.metadata;
724
+ const metadataTx = storageClient.preparePut({
725
+ key: topLevelHash,
726
+ text: options.text,
727
+ value: chunkMetadata
728
+ // Use the XML metadata from chunk preparation
729
+ });
730
+ const filteredChunks = await filterXmlStorageTransactions({
731
+ storageClient,
732
+ transactions: chunkTxs,
733
+ operatorAddress: backendWalletAddress
734
+ });
735
+ const chunksToSend = filteredChunks.toSend;
736
+ const chunksSkipped = filteredChunks.skipped.length;
737
+ const metadataStorageKey = metadataTx.args[0];
738
+ const expectedMetadata = hexToString(metadataTx.args[2]);
739
+ let metadataNeedsSubmission = true;
740
+ const metadataCheck = await checkXmlMetadataExists({
741
+ storageClient,
742
+ storageKey: metadataStorageKey,
743
+ operatorAddress: userAddress,
744
+ // User is operator for metadata
745
+ expectedMetadata
746
+ });
747
+ if (metadataCheck.exists && metadataCheck.matches) {
748
+ metadataNeedsSubmission = false;
749
+ }
750
+ let chunkTransactionHashes = [];
751
+ let chunksSent = 0;
752
+ if (chunksToSend.length > 0) {
753
+ try {
754
+ const batches = batchTransactions(chunksToSend);
755
+ const totalBatches = batches.length;
756
+ if (totalBatches > 1) {
757
+ console.log(
758
+ `\u{1F4E6} Splitting ${chunksToSend.length} chunks into ${totalBatches} batch(es)`
759
+ );
760
+ }
761
+ for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
762
+ const batch = batches[batchIndex];
763
+ if (totalBatches > 1) {
764
+ console.log(
765
+ `\u{1F4E4} Sending batch ${batchIndex + 1}/${totalBatches} (${batch.length} transactions)...`
766
+ );
767
+ }
768
+ const submitResult = await submitTransactionsViaRelay({
769
+ apiUrl: options.apiUrl,
770
+ chainId: options.chainId,
771
+ operatorAddress: userAddress,
772
+ secretKey: options.secretKey,
773
+ transactions: batch,
774
+ sessionToken
775
+ });
776
+ chunkTransactionHashes.push(...submitResult.transactionHashes);
777
+ chunksSent += submitResult.successfulIndexes.length;
778
+ if (submitResult.failedIndexes.length === batch.length) {
779
+ const errorMessage = `Batch ${batchIndex + 1}: All ${batch.length} transactions failed. Likely due to insufficient backend wallet balance or network issues. Stopping batch processing to avoid wasting app fees.`;
780
+ console.error(`\u274C ${errorMessage}`);
781
+ errors.push(new Error(errorMessage));
782
+ break;
783
+ }
784
+ if (submitResult.failedIndexes.length > 0 && submitResult.successfulIndexes.length > 0) {
785
+ const failedTxs = submitResult.failedIndexes.map((idx) => batch[idx]);
786
+ try {
787
+ const retryResult = await retryFailedTransactions({
788
+ apiUrl: options.apiUrl,
789
+ chainId: options.chainId,
790
+ operatorAddress: userAddress,
791
+ secretKey: options.secretKey,
792
+ failedIndexes: submitResult.failedIndexes,
793
+ originalTransactions: failedTxs,
794
+ storageClient,
795
+ backendWalletAddress,
796
+ sessionToken
797
+ });
798
+ chunkTransactionHashes.push(...retryResult.transactionHashes);
799
+ chunksSent += retryResult.successfulIndexes.length;
800
+ if (retryResult.failedIndexes.length > 0) {
801
+ errors.push(
802
+ new Error(
803
+ `Batch ${batchIndex + 1}: ${retryResult.failedIndexes.length} transactions failed after retries`
804
+ )
805
+ );
806
+ }
807
+ } catch (retryError) {
808
+ errors.push(
809
+ retryError instanceof Error ? retryError : new Error(
810
+ `Batch ${batchIndex + 1} retry failed: ${String(
811
+ retryError
812
+ )}`
813
+ )
814
+ );
815
+ }
816
+ }
817
+ if (batchIndex < batches.length - 1 && chunkTransactionHashes.length > 0) {
818
+ const batchHashes = chunkTransactionHashes.slice(
819
+ -submitResult.successfulIndexes.length
820
+ );
821
+ if (batchHashes.length > 0) {
822
+ try {
823
+ await waitForConfirmations({
824
+ publicClient,
825
+ transactionHashes: batchHashes,
826
+ confirmations: 1,
827
+ // Just 1 confirmation between batches
828
+ timeout: 3e4
829
+ // 30 second timeout per batch
830
+ });
831
+ } catch (confirmationError) {
832
+ console.warn(
833
+ `\u26A0\uFE0F Batch ${batchIndex + 1} confirmation timeout (continuing...)`
834
+ );
835
+ }
836
+ }
837
+ }
838
+ }
839
+ } catch (submitError) {
840
+ errors.push(
841
+ submitError instanceof Error ? submitError : new Error(`Chunk submission failed: ${String(submitError)}`)
842
+ );
843
+ }
844
+ }
845
+ if (chunkTransactionHashes.length > 0) {
846
+ try {
847
+ await waitForConfirmations({
848
+ publicClient,
849
+ transactionHashes: chunkTransactionHashes,
850
+ confirmations: 1,
851
+ timeout: 6e4
852
+ });
853
+ } catch (confirmationError) {
854
+ errors.push(
855
+ confirmationError instanceof Error ? confirmationError : new Error(`Chunk confirmation failed: ${String(confirmationError)}`)
856
+ );
857
+ }
858
+ }
859
+ let metadataTransactionHash;
860
+ if (metadataNeedsSubmission) {
861
+ try {
862
+ metadataTransactionHash = await userWalletClient.writeContract({
863
+ address: metadataTx.to,
864
+ abi: metadataTx.abi,
865
+ functionName: metadataTx.functionName,
866
+ args: metadataTx.args,
867
+ value: metadataTx.value !== void 0 && metadataTx.value > BigInt(0) ? metadataTx.value : void 0
868
+ });
869
+ await waitForConfirmations({
870
+ publicClient,
871
+ transactionHashes: [metadataTransactionHash],
872
+ confirmations: 1,
873
+ timeout: 6e4
874
+ });
875
+ } catch (metadataError) {
876
+ errors.push(
877
+ metadataError instanceof Error ? metadataError : new Error(`Metadata submission failed: ${String(metadataError)}`)
878
+ );
879
+ }
880
+ }
881
+ return {
882
+ success: errors.length === 0,
883
+ topLevelHash,
884
+ chunksSent,
885
+ chunksSkipped,
886
+ metadataSubmitted: metadataNeedsSubmission && metadataTransactionHash !== void 0,
887
+ chunkTransactionHashes,
888
+ metadataTransactionHash,
889
+ backendWalletAddress,
890
+ errors: errors.length > 0 ? errors : void 0
891
+ };
892
+ }
893
+ async function previewFile(options) {
894
+ const fileBuffer = readFileSync(options.filePath);
895
+ const isBinary = fileBuffer.some(
896
+ (byte) => byte === 0 || byte < 32 && byte !== 9 && byte !== 10 && byte !== 13
897
+ );
898
+ let fileContent;
899
+ if (isBinary) {
900
+ const base64String = fileBuffer.toString("base64");
901
+ const detectedType = detectFileTypeFromBase64(base64String);
902
+ if (detectedType) {
903
+ fileContent = base64ToDataUri(base64String);
904
+ } else {
905
+ fileContent = base64String;
906
+ }
907
+ } else {
908
+ fileContent = fileBuffer.toString("utf-8");
909
+ }
910
+ const storageClient = new StorageClient({
911
+ chainId: options.chainId,
912
+ overrides: options.rpcUrl ? { rpcUrls: [options.rpcUrl] } : void 0
913
+ });
914
+ const { operatorAddress } = createWalletClientFromPrivateKey({
915
+ privateKey: options.privateKey,
916
+ chainId: options.chainId,
917
+ rpcUrl: options.rpcUrl
918
+ });
919
+ const useXmlStorage = shouldSuggestXmlStorage(fileContent);
920
+ let transactions;
921
+ if (useXmlStorage) {
922
+ transactions = prepareXmlStorageTransactions({
923
+ storageClient,
924
+ storageKey: options.storageKey,
925
+ text: options.text,
926
+ content: fileContent,
927
+ operatorAddress
928
+ });
929
+ } else {
930
+ const storageKeyBytes = getStorageKeyBytes(
931
+ options.storageKey
932
+ );
933
+ const typedArgs = {
934
+ key: storageKeyBytes,
935
+ text: options.text,
936
+ value: stringToHex(fileContent)
937
+ };
938
+ transactions = [
939
+ prepareNormalStorageTransaction(
940
+ storageClient,
941
+ typedArgs,
942
+ options.storageKey
943
+ // Pass original string key for preparePut
944
+ )
945
+ ];
946
+ }
947
+ let transactionsToSend;
948
+ let transactionsSkipped;
949
+ if (useXmlStorage) {
950
+ const chunkTransactions = transactions.filter((tx) => tx.type === "chunked").map((tx) => tx.transaction);
951
+ const txConfigToTxWithId = new Map(
952
+ transactions.filter((tx) => tx.type === "chunked").map((tx) => [tx.transaction, tx])
953
+ );
954
+ const filtered = await filterXmlStorageTransactions({
955
+ storageClient,
956
+ transactions: chunkTransactions,
957
+ // Only chunk transactions
958
+ operatorAddress
959
+ });
960
+ const filteredToSend = filtered.toSend.map((txConfig) => txConfigToTxWithId.get(txConfig)).filter((tx) => tx !== void 0);
961
+ const filteredSkipped = filtered.skipped.map((txConfig) => txConfigToTxWithId.get(txConfig)).filter((tx) => tx !== void 0);
962
+ const metadataTx = transactions.find((tx) => tx.type === "metadata");
963
+ if (metadataTx) {
964
+ filteredToSend.unshift(metadataTx);
965
+ }
966
+ transactionsToSend = filteredToSend;
967
+ transactionsSkipped = filteredSkipped;
968
+ } else {
969
+ const filtered = await filterExistingTransactions({
970
+ storageClient,
971
+ transactions,
972
+ operatorAddress,
973
+ expectedContent: fileContent
974
+ });
975
+ transactionsToSend = filtered.toSend;
976
+ transactionsSkipped = filtered.skipped;
977
+ }
978
+ if (useXmlStorage) {
979
+ const chunkTransactions = transactions.filter(
980
+ (tx) => tx.type === "chunked"
981
+ );
982
+ const metadataTransaction = transactions.find(
983
+ (tx) => tx.type === "metadata"
984
+ );
985
+ const totalChunks = chunkTransactions.length;
986
+ const alreadyStoredChunks = transactionsSkipped.filter(
987
+ (tx) => tx.type === "chunked"
988
+ ).length;
989
+ const needToStoreChunks = transactionsToSend.filter(
990
+ (tx) => tx.type === "chunked"
991
+ ).length;
992
+ const metadataNeedsStorage = metadataTransaction ? transactionsToSend.some((tx) => tx.type === "metadata") : false;
993
+ return {
994
+ storageType: "xml",
995
+ totalChunks,
996
+ alreadyStoredChunks,
997
+ needToStoreChunks,
998
+ metadataNeedsStorage,
999
+ operatorAddress,
1000
+ storageKey: options.storageKey,
1001
+ totalTransactions: transactions.length,
1002
+ transactionsToSend: transactionsToSend.length,
1003
+ transactionsSkipped: transactionsSkipped.length
1004
+ };
1005
+ } else {
1006
+ const totalChunks = 1;
1007
+ const alreadyStoredChunks = transactionsSkipped.length;
1008
+ const needToStoreChunks = transactionsToSend.length;
1009
+ return {
1010
+ storageType: "normal",
1011
+ totalChunks,
1012
+ alreadyStoredChunks,
1013
+ needToStoreChunks,
1014
+ operatorAddress,
1015
+ storageKey: options.storageKey,
1016
+ totalTransactions: transactions.length,
1017
+ transactionsToSend: transactionsToSend.length,
1018
+ transactionsSkipped: transactionsSkipped.length
1019
+ };
1020
+ }
1021
+ }
1022
+
1023
+ // src/commands/storage/index.ts
1024
+ function registerStorageCommand(program2) {
1025
+ const storageCommand = program2.command("storage").description("Storage operations");
1026
+ const uploadCommand = new Command("upload").description("Upload files to Net Storage").requiredOption("--file <path>", "Path to file to upload").requiredOption("--key <key>", "Storage key (filename/identifier)").requiredOption("--text <text>", "Text description/filename").option(
1027
+ "--private-key <key>",
1028
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
1029
+ ).option(
1030
+ "--chain-id <id>",
1031
+ "Chain ID (8453 for Base, 1 for Ethereum, etc.). Can also be set via NET_CHAIN_ID env var",
1032
+ (value) => parseInt(value, 10)
1033
+ ).option(
1034
+ "--rpc-url <url>",
1035
+ "Custom RPC URL (can also be set via NET_RPC_URL env var)"
1036
+ ).action(async (options) => {
1037
+ const commonOptions = parseCommonOptions({
1038
+ privateKey: options.privateKey,
1039
+ chainId: options.chainId,
1040
+ rpcUrl: options.rpcUrl
1041
+ });
1042
+ const uploadOptions = {
1043
+ filePath: options.file,
1044
+ storageKey: options.key,
1045
+ text: options.text,
1046
+ privateKey: commonOptions.privateKey,
1047
+ chainId: commonOptions.chainId,
1048
+ rpcUrl: commonOptions.rpcUrl
1049
+ };
1050
+ try {
1051
+ console.log(chalk2.blue(`\u{1F4C1} Reading file: ${options.file}`));
1052
+ const result = await uploadFile(uploadOptions);
1053
+ const storageUrl = generateStorageUrl(
1054
+ result.operatorAddress,
1055
+ commonOptions.chainId,
1056
+ options.key
1057
+ );
1058
+ if (result.skipped && result.transactionsSent === 0) {
1059
+ console.log(
1060
+ chalk2.green(
1061
+ `\u2713 All data already stored - skipping upload
1062
+ Storage Key: ${options.key}
1063
+ Skipped: ${result.transactionsSkipped} transaction(s)${storageUrl ? `
1064
+ Storage URL: ${chalk2.cyan(storageUrl)}` : ""}`
1065
+ )
1066
+ );
1067
+ process.exit(0);
1068
+ }
1069
+ if (result.success) {
1070
+ console.log(
1071
+ chalk2.green(
1072
+ `\u2713 File uploaded successfully!
1073
+ Storage Key: ${options.key}
1074
+ Storage Type: ${result.storageType === "xml" ? "XML" : "Normal"}
1075
+ Transactions Sent: ${result.transactionsSent}
1076
+ Transactions Skipped: ${result.transactionsSkipped}
1077
+ Final Transaction Hash: ${result.finalHash || "N/A"}${storageUrl ? `
1078
+ Storage URL: ${chalk2.cyan(storageUrl)}` : ""}`
1079
+ )
1080
+ );
1081
+ process.exit(0);
1082
+ } else {
1083
+ console.error(
1084
+ chalk2.red(
1085
+ `\u2717 Upload completed with errors
1086
+ Transactions Sent: ${result.transactionsSent}
1087
+ Transactions Skipped: ${result.transactionsSkipped}
1088
+ Transactions Failed: ${result.transactionsFailed}
1089
+ Error: ${result.error || "Unknown error"}`
1090
+ )
1091
+ );
1092
+ process.exit(1);
1093
+ }
1094
+ } catch (error) {
1095
+ console.error(
1096
+ chalk2.red(
1097
+ `Upload failed: ${error instanceof Error ? error.message : String(error)}`
1098
+ )
1099
+ );
1100
+ process.exit(1);
1101
+ }
1102
+ });
1103
+ const previewCommand = new Command("preview").description("Preview storage upload without submitting transactions").requiredOption("--file <path>", "Path to file to preview").requiredOption("--key <key>", "Storage key (filename/identifier)").requiredOption("--text <text>", "Text description/filename").option(
1104
+ "--private-key <key>",
1105
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
1106
+ ).option(
1107
+ "--chain-id <id>",
1108
+ "Chain ID (8453 for Base, 1 for Ethereum, etc.). Can also be set via NET_CHAIN_ID env var",
1109
+ (value) => parseInt(value, 10)
1110
+ ).option(
1111
+ "--rpc-url <url>",
1112
+ "Custom RPC URL (can also be set via NET_RPC_URL env var)"
1113
+ ).action(async (options) => {
1114
+ const commonOptions = parseCommonOptions({
1115
+ privateKey: options.privateKey,
1116
+ chainId: options.chainId,
1117
+ rpcUrl: options.rpcUrl
1118
+ });
1119
+ const previewOptions = {
1120
+ filePath: options.file,
1121
+ storageKey: options.key,
1122
+ text: options.text,
1123
+ privateKey: commonOptions.privateKey,
1124
+ chainId: commonOptions.chainId,
1125
+ rpcUrl: commonOptions.rpcUrl
1126
+ };
1127
+ try {
1128
+ console.log(chalk2.blue(`\u{1F4C1} Reading file: ${options.file}`));
1129
+ const result = await previewFile(previewOptions);
1130
+ const storageUrl = generateStorageUrl(
1131
+ result.operatorAddress,
1132
+ commonOptions.chainId,
1133
+ options.key
1134
+ );
1135
+ console.log(chalk2.cyan("\n\u{1F4CA} Storage Preview:"));
1136
+ console.log(` Storage Key: ${chalk2.white(result.storageKey)}`);
1137
+ console.log(
1138
+ ` Storage Type: ${chalk2.white(
1139
+ result.storageType === "xml" ? "XML" : "Normal"
1140
+ )}`
1141
+ );
1142
+ console.log(` Total Chunks: ${chalk2.white(result.totalChunks)}`);
1143
+ console.log(
1144
+ ` Already Stored: ${chalk2.green(result.alreadyStoredChunks)}`
1145
+ );
1146
+ console.log(
1147
+ ` Need to Store: ${chalk2.yellow(result.needToStoreChunks)}`
1148
+ );
1149
+ if (result.storageType === "xml" && result.metadataNeedsStorage) {
1150
+ console.log(` Metadata: ${chalk2.yellow("Needs Storage")}`);
1151
+ } else if (result.storageType === "xml") {
1152
+ console.log(` Metadata: ${chalk2.green("Already Stored")}`);
1153
+ }
1154
+ console.log(
1155
+ ` Total Transactions: ${chalk2.white(result.totalTransactions)}`
1156
+ );
1157
+ console.log(
1158
+ ` Transactions to Send: ${chalk2.yellow(result.transactionsToSend)}`
1159
+ );
1160
+ console.log(
1161
+ ` Transactions Skipped: ${chalk2.green(result.transactionsSkipped)}`
1162
+ );
1163
+ console.log(
1164
+ ` Operator Address: ${chalk2.gray(result.operatorAddress)}`
1165
+ );
1166
+ if (storageUrl) {
1167
+ console.log(` Storage URL: ${chalk2.cyan(storageUrl)}`);
1168
+ }
1169
+ if (result.needToStoreChunks === 0 && !result.metadataNeedsStorage) {
1170
+ console.log(
1171
+ chalk2.green("\n\u2713 All data is already stored - no upload needed")
1172
+ );
1173
+ } else {
1174
+ console.log(
1175
+ chalk2.yellow(
1176
+ `
1177
+ \u26A0 ${result.transactionsToSend} transaction(s) would be sent`
1178
+ )
1179
+ );
1180
+ }
1181
+ process.exit(0);
1182
+ } catch (error) {
1183
+ console.error(
1184
+ chalk2.red(
1185
+ `Preview failed: ${error instanceof Error ? error.message : String(error)}`
1186
+ )
1187
+ );
1188
+ process.exit(1);
1189
+ }
1190
+ });
1191
+ const uploadRelayCommand = new Command("upload-relay").description("Upload files to Net Storage via x402 relay (backend pays gas for chunks)").requiredOption("--file <path>", "Path to file to upload").requiredOption("--key <key>", "Storage key (filename/identifier)").requiredOption("--text <text>", "Text description/filename").requiredOption(
1192
+ "--api-url <url>",
1193
+ "Backend API URL (e.g., http://localhost:3000)"
1194
+ ).requiredOption(
1195
+ "--secret-key <key>",
1196
+ "Secret key for backend wallet derivation. Can also be set via X402_SECRET_KEY env var"
1197
+ ).option(
1198
+ "--private-key <key>",
1199
+ "Private key (0x-prefixed hex, 66 characters). Can also be set via NET_PRIVATE_KEY env var"
1200
+ ).option(
1201
+ "--chain-id <id>",
1202
+ "Chain ID (8453 for Base, 1 for Ethereum, etc.). Can also be set via NET_CHAIN_ID env var",
1203
+ (value) => parseInt(value, 10)
1204
+ ).option(
1205
+ "--rpc-url <url>",
1206
+ "Custom RPC URL (can also be set via NET_RPC_URL env var)"
1207
+ ).action(async (options) => {
1208
+ const commonOptions = parseCommonOptions({
1209
+ privateKey: options.privateKey,
1210
+ chainId: options.chainId,
1211
+ rpcUrl: options.rpcUrl
1212
+ });
1213
+ const secretKey = options.secretKey || process.env.X402_SECRET_KEY;
1214
+ if (!secretKey) {
1215
+ console.error(
1216
+ chalk2.red(
1217
+ "Error: --secret-key is required or set X402_SECRET_KEY environment variable"
1218
+ )
1219
+ );
1220
+ process.exit(1);
1221
+ }
1222
+ const uploadRelayOptions = {
1223
+ filePath: options.file,
1224
+ storageKey: options.key,
1225
+ text: options.text,
1226
+ privateKey: commonOptions.privateKey,
1227
+ chainId: commonOptions.chainId,
1228
+ rpcUrl: commonOptions.rpcUrl,
1229
+ apiUrl: options.apiUrl,
1230
+ secretKey
1231
+ };
1232
+ try {
1233
+ console.log(chalk2.blue(`\u{1F4C1} Reading file: ${options.file}`));
1234
+ console.log(chalk2.blue(`\u{1F517} Using relay API: ${options.apiUrl}`));
1235
+ const result = await uploadFileWithRelay(uploadRelayOptions);
1236
+ const { privateKeyToAccount: privateKeyToAccount3 } = await import('viem/accounts');
1237
+ const userAccount = privateKeyToAccount3(commonOptions.privateKey);
1238
+ const storageUrl = generateStorageUrl(
1239
+ userAccount.address,
1240
+ commonOptions.chainId,
1241
+ options.key
1242
+ );
1243
+ if (result.success) {
1244
+ console.log(
1245
+ chalk2.green(
1246
+ `\u2713 File uploaded successfully via relay!
1247
+ Storage Key: ${options.key}
1248
+ Top-Level Hash: ${result.topLevelHash}
1249
+ Chunks Sent: ${result.chunksSent}
1250
+ Chunks Skipped: ${result.chunksSkipped}
1251
+ Metadata Submitted: ${result.metadataSubmitted ? "Yes" : "No (already exists)"}
1252
+ Backend Wallet: ${result.backendWalletAddress}
1253
+ Chunk Transaction Hashes: ${result.chunkTransactionHashes.length > 0 ? result.chunkTransactionHashes.join(", ") : "None"}${result.metadataTransactionHash ? `
1254
+ Metadata Transaction Hash: ${result.metadataTransactionHash}` : ""}${storageUrl ? `
1255
+ Storage URL: ${chalk2.cyan(storageUrl)}` : ""}`
1256
+ )
1257
+ );
1258
+ process.exit(0);
1259
+ } else {
1260
+ console.error(
1261
+ chalk2.red(
1262
+ `\u2717 Upload completed with errors
1263
+ Chunks Sent: ${result.chunksSent}
1264
+ Chunks Skipped: ${result.chunksSkipped}
1265
+ Metadata Submitted: ${result.metadataSubmitted ? "Yes" : "No"}
1266
+ Errors: ${result.errors ? result.errors.map((e) => e.message).join(", ") : "Unknown error"}`
1267
+ )
1268
+ );
1269
+ process.exit(1);
1270
+ }
1271
+ } catch (error) {
1272
+ console.error(
1273
+ chalk2.red(
1274
+ `Upload via relay failed: ${error instanceof Error ? error.message : String(error)}`
1275
+ )
1276
+ );
1277
+ process.exit(1);
1278
+ }
1279
+ });
1280
+ storageCommand.addCommand(uploadCommand);
1281
+ storageCommand.addCommand(previewCommand);
1282
+ storageCommand.addCommand(uploadRelayCommand);
1283
+ }
1284
+
1285
+ // src/cli/index.ts
1286
+ var program = new Command();
1287
+ program.name("netp").description("CLI tool for Net Protocol").version("0.1.0");
1288
+ registerStorageCommand(program);
1289
+ program.parse();
1290
+ //# sourceMappingURL=index.mjs.map
1291
+ //# sourceMappingURL=index.mjs.map