@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.
- package/README.md +339 -0
- package/dist/cli/index.mjs +1291 -0
- package/dist/cli/index.mjs.map +1 -0
- package/package.json +65 -0
|
@@ -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
|