hippius 0.1.0__py3-none-any.whl
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.
- hippius-0.1.0.dist-info/METADATA +475 -0
- hippius-0.1.0.dist-info/RECORD +9 -0
- hippius-0.1.0.dist-info/WHEEL +4 -0
- hippius-0.1.0.dist-info/entry_points.txt +4 -0
- hippius_sdk/__init__.py +11 -0
- hippius_sdk/cli.py +658 -0
- hippius_sdk/client.py +246 -0
- hippius_sdk/ipfs.py +985 -0
- hippius_sdk/substrate.py +688 -0
hippius_sdk/substrate.py
ADDED
@@ -0,0 +1,688 @@
|
|
1
|
+
"""
|
2
|
+
Substrate operations for the Hippius SDK.
|
3
|
+
|
4
|
+
Note: This functionality is coming soon and not implemented yet.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import json
|
9
|
+
from typing import Dict, Any, Optional, List, Union
|
10
|
+
from substrateinterface import SubstrateInterface, Keypair
|
11
|
+
from dotenv import load_dotenv
|
12
|
+
|
13
|
+
# Load environment variables
|
14
|
+
load_dotenv()
|
15
|
+
|
16
|
+
|
17
|
+
class FileInput:
|
18
|
+
"""File input for storage requests"""
|
19
|
+
|
20
|
+
def __init__(self, file_hash: str, file_name: str):
|
21
|
+
"""
|
22
|
+
Initialize a file input
|
23
|
+
|
24
|
+
Args:
|
25
|
+
file_hash: IPFS hash (CID) of the file
|
26
|
+
file_name: Name of the file
|
27
|
+
"""
|
28
|
+
self.file_hash = file_hash
|
29
|
+
self.file_name = file_name
|
30
|
+
|
31
|
+
def to_dict(self) -> Dict[str, str]:
|
32
|
+
"""Convert to dictionary representation"""
|
33
|
+
return {"fileHash": self.file_hash, "fileName": self.file_name}
|
34
|
+
|
35
|
+
|
36
|
+
class SubstrateClient:
|
37
|
+
"""
|
38
|
+
Client for interacting with the Hippius Substrate blockchain.
|
39
|
+
|
40
|
+
Note: This functionality is not fully implemented yet and is under active development.
|
41
|
+
"""
|
42
|
+
|
43
|
+
def __init__(self, url: str = None, seed_phrase: Optional[str] = None):
|
44
|
+
"""
|
45
|
+
Initialize the Substrate client.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
url: WebSocket URL of the Hippius substrate node
|
49
|
+
If not provided, uses SUBSTRATE_URL from environment
|
50
|
+
seed_phrase: Seed phrase for the account (mnemonic)
|
51
|
+
If not provided, uses SUBSTRATE_SEED_PHRASE from environment
|
52
|
+
"""
|
53
|
+
if not url:
|
54
|
+
url = os.getenv("SUBSTRATE_URL", "wss://rpc.hippius.network")
|
55
|
+
|
56
|
+
# Store URL and initialize variables
|
57
|
+
self.url = url
|
58
|
+
self._substrate = None
|
59
|
+
self._keypair = None
|
60
|
+
|
61
|
+
# Set seed phrase if provided or available in environment
|
62
|
+
if seed_phrase:
|
63
|
+
self.set_seed_phrase(seed_phrase)
|
64
|
+
elif os.getenv("SUBSTRATE_SEED_PHRASE"):
|
65
|
+
self.set_seed_phrase(os.getenv("SUBSTRATE_SEED_PHRASE"))
|
66
|
+
|
67
|
+
# Don't connect immediately to avoid exceptions during initialization
|
68
|
+
# Connection will happen lazily when needed
|
69
|
+
|
70
|
+
def connect(self) -> None:
|
71
|
+
"""
|
72
|
+
Connect to the Substrate node.
|
73
|
+
|
74
|
+
Initializes the connection to the Substrate node and creates a keypair from the seed phrase.
|
75
|
+
"""
|
76
|
+
try:
|
77
|
+
print(f"Connecting to Substrate node at {self.url}...")
|
78
|
+
self._substrate = SubstrateInterface(
|
79
|
+
url=self.url,
|
80
|
+
ss58_format=42, # Substrate default
|
81
|
+
type_registry_preset="substrate-node-template",
|
82
|
+
)
|
83
|
+
|
84
|
+
# Only create keypair if seed phrase is available
|
85
|
+
if hasattr(self, "_seed_phrase") and self._seed_phrase:
|
86
|
+
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
87
|
+
print(
|
88
|
+
f"Connected successfully. Account address: {self._keypair.ss58_address}"
|
89
|
+
)
|
90
|
+
else:
|
91
|
+
print("Connected successfully (read-only mode, no keypair)")
|
92
|
+
|
93
|
+
return True
|
94
|
+
|
95
|
+
except Exception as e:
|
96
|
+
print(f"Failed to connect to Substrate node: {e}")
|
97
|
+
raise ConnectionError(
|
98
|
+
f"Could not connect to Substrate node at {self.url}: {e}"
|
99
|
+
)
|
100
|
+
|
101
|
+
return False
|
102
|
+
|
103
|
+
def set_seed_phrase(self, seed_phrase: str) -> None:
|
104
|
+
"""
|
105
|
+
Set or update the seed phrase used for signing transactions.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
seed_phrase: Mnemonic seed phrase for the account
|
109
|
+
"""
|
110
|
+
if not seed_phrase or not seed_phrase.strip():
|
111
|
+
raise ValueError("Seed phrase cannot be empty")
|
112
|
+
|
113
|
+
# Store the seed phrase
|
114
|
+
self._seed_phrase = seed_phrase.strip()
|
115
|
+
|
116
|
+
# Try to create the keypair if possible
|
117
|
+
try:
|
118
|
+
if hasattr(self, "_substrate") and self._substrate:
|
119
|
+
# If we already have a connection, create the keypair
|
120
|
+
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
121
|
+
print(f"Keypair created for account: {self._keypair.ss58_address}")
|
122
|
+
else:
|
123
|
+
print(f"Seed phrase set (keypair will be created when connecting)")
|
124
|
+
except Exception as e:
|
125
|
+
print(f"Warning: Could not create keypair from seed phrase: {e}")
|
126
|
+
print(f"Keypair will be created when needed")
|
127
|
+
|
128
|
+
def storage_request(
|
129
|
+
self, files: List[Union[FileInput, Dict[str, str]]], miner_ids: List[str] = None
|
130
|
+
) -> str:
|
131
|
+
"""
|
132
|
+
Submit a storage request for IPFS files to the marketplace.
|
133
|
+
|
134
|
+
This method batches all files into a single transaction to efficiently store
|
135
|
+
multiple files at once.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
files: List of FileInput objects or dictionaries with fileHash and fileName
|
139
|
+
miner_ids: List of miner IDs to store the files (optional)
|
140
|
+
|
141
|
+
Returns:
|
142
|
+
str: Transaction hash
|
143
|
+
|
144
|
+
Example:
|
145
|
+
>>> client.storage_request([
|
146
|
+
... FileInput("QmHash1", "file1.txt"),
|
147
|
+
... FileInput("QmHash2", "file2.jpg")
|
148
|
+
... ])
|
149
|
+
"""
|
150
|
+
# Convert any dict inputs to FileInput objects
|
151
|
+
file_inputs = []
|
152
|
+
for file in files:
|
153
|
+
if isinstance(file, dict):
|
154
|
+
file_inputs.append(
|
155
|
+
FileInput(
|
156
|
+
file_hash=file.get("fileHash") or file.get("cid"),
|
157
|
+
file_name=file.get("fileName")
|
158
|
+
or file.get("filename")
|
159
|
+
or "unknown",
|
160
|
+
)
|
161
|
+
)
|
162
|
+
else:
|
163
|
+
file_inputs.append(file)
|
164
|
+
|
165
|
+
# Print what is being submitted
|
166
|
+
print(f"Submitting storage request for {len(file_inputs)} files as a batch:")
|
167
|
+
for file in file_inputs:
|
168
|
+
print(f" - {file.file_name}: {file.file_hash}")
|
169
|
+
|
170
|
+
if miner_ids:
|
171
|
+
print(f"Targeted miners: {', '.join(miner_ids)}")
|
172
|
+
else:
|
173
|
+
print("No specific miners targeted (using default selection)")
|
174
|
+
|
175
|
+
try:
|
176
|
+
# Initialize Substrate connection
|
177
|
+
if not hasattr(self, "_substrate") or self._substrate is None:
|
178
|
+
print("Initializing Substrate connection...")
|
179
|
+
self._substrate = SubstrateInterface(
|
180
|
+
url=self.url,
|
181
|
+
ss58_format=42, # Substrate default
|
182
|
+
type_registry_preset="substrate-node-template",
|
183
|
+
)
|
184
|
+
print(f"Connected to Substrate node at {self.url}")
|
185
|
+
|
186
|
+
# Create keypair from seed phrase if not already created
|
187
|
+
if not hasattr(self, "_keypair") or self._keypair is None:
|
188
|
+
if not hasattr(self, "_seed_phrase") or not self._seed_phrase:
|
189
|
+
raise ValueError(
|
190
|
+
"Seed phrase must be set before making transactions"
|
191
|
+
)
|
192
|
+
|
193
|
+
print("Creating keypair from seed phrase...")
|
194
|
+
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
195
|
+
print(f"Keypair created for address: {self._keypair.ss58_address}")
|
196
|
+
|
197
|
+
# Prepare storage request call
|
198
|
+
print("Preparing marketplace.storageRequest batch call...")
|
199
|
+
|
200
|
+
# Format files for the batch call - all files are included in a single array
|
201
|
+
formatted_files = []
|
202
|
+
for file_input in file_inputs:
|
203
|
+
formatted_files.append(
|
204
|
+
{
|
205
|
+
"file_hash": file_input.file_hash,
|
206
|
+
"file_name": file_input.file_name,
|
207
|
+
}
|
208
|
+
)
|
209
|
+
|
210
|
+
# Create call parameters with all files in a single batch
|
211
|
+
call_params = {
|
212
|
+
"files_input": formatted_files,
|
213
|
+
"miner_ids": miner_ids
|
214
|
+
if miner_ids
|
215
|
+
else [], # Always include miner_ids, empty array if not specified
|
216
|
+
}
|
217
|
+
|
218
|
+
# Create the call to the marketplace
|
219
|
+
print(f"Call parameters: {json.dumps(call_params, indent=2)}")
|
220
|
+
call = self._substrate.compose_call(
|
221
|
+
call_module="Marketplace",
|
222
|
+
call_function="storage_request",
|
223
|
+
call_params=call_params,
|
224
|
+
)
|
225
|
+
|
226
|
+
# Get payment info to estimate the fee
|
227
|
+
payment_info = self._substrate.get_payment_info(
|
228
|
+
call=call, keypair=self._keypair
|
229
|
+
)
|
230
|
+
|
231
|
+
estimated_fee = payment_info.get("partialFee", 0)
|
232
|
+
print(f"Estimated transaction fee: {estimated_fee}")
|
233
|
+
|
234
|
+
# Create a signed extrinsic
|
235
|
+
extrinsic = self._substrate.create_signed_extrinsic(
|
236
|
+
call=call, keypair=self._keypair
|
237
|
+
)
|
238
|
+
|
239
|
+
print(f"Submitting batch transaction for {len(formatted_files)} files...")
|
240
|
+
|
241
|
+
# Submit the transaction
|
242
|
+
response = self._substrate.submit_extrinsic(
|
243
|
+
extrinsic=extrinsic, wait_for_inclusion=True
|
244
|
+
)
|
245
|
+
|
246
|
+
# Get the transaction hash
|
247
|
+
tx_hash = response.extrinsic_hash
|
248
|
+
|
249
|
+
print(f"Batch transaction submitted successfully!")
|
250
|
+
print(f"Transaction hash: {tx_hash}")
|
251
|
+
print(
|
252
|
+
f"All {len(formatted_files)} files have been stored in a single transaction"
|
253
|
+
)
|
254
|
+
|
255
|
+
return tx_hash
|
256
|
+
|
257
|
+
except ValueError as e:
|
258
|
+
# Handle configuration errors
|
259
|
+
print(f"Error: {e}")
|
260
|
+
raise
|
261
|
+
except Exception as e:
|
262
|
+
print(f"Error interacting with Substrate: {e}")
|
263
|
+
raise
|
264
|
+
|
265
|
+
return "simulated-tx-hash"
|
266
|
+
|
267
|
+
def store_cid(
|
268
|
+
self, cid: str, filename: str = None, metadata: Optional[Dict[str, Any]] = None
|
269
|
+
) -> str:
|
270
|
+
"""
|
271
|
+
Store a CID on the blockchain.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
cid: Content Identifier (CID) to store
|
275
|
+
filename: Original filename (optional)
|
276
|
+
metadata: Additional metadata to store with the CID
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
str: Transaction hash
|
280
|
+
"""
|
281
|
+
file_input = FileInput(file_hash=cid, file_name=filename or "unnamed_file")
|
282
|
+
return self.storage_request([file_input])
|
283
|
+
|
284
|
+
def get_cid_metadata(self, cid: str) -> Dict[str, Any]:
|
285
|
+
"""
|
286
|
+
Retrieve metadata for a CID from the blockchain.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
cid: Content Identifier (CID) to query
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
Dict[str, Any]: Metadata associated with the CID
|
293
|
+
"""
|
294
|
+
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
295
|
+
|
296
|
+
def get_account_cids(self, account_address: str) -> List[str]:
|
297
|
+
"""
|
298
|
+
Get all CIDs associated with an account.
|
299
|
+
|
300
|
+
Args:
|
301
|
+
account_address: Substrate account address
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
List[str]: List of CIDs owned by the account
|
305
|
+
"""
|
306
|
+
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
307
|
+
|
308
|
+
def delete_cid(self, cid: str) -> str:
|
309
|
+
"""
|
310
|
+
Delete a CID from the blockchain (mark as removed).
|
311
|
+
|
312
|
+
Args:
|
313
|
+
cid: Content Identifier (CID) to delete
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
str: Transaction hash
|
317
|
+
"""
|
318
|
+
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
319
|
+
|
320
|
+
def get_storage_fee(self, file_size_mb: float) -> float:
|
321
|
+
"""
|
322
|
+
Get the estimated storage fee for a file of given size.
|
323
|
+
|
324
|
+
Args:
|
325
|
+
file_size_mb: File size in megabytes
|
326
|
+
|
327
|
+
Returns:
|
328
|
+
float: Estimated fee in native tokens
|
329
|
+
"""
|
330
|
+
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
331
|
+
|
332
|
+
def get_account_balance(
|
333
|
+
self, account_address: Optional[str] = None
|
334
|
+
) -> Dict[str, float]:
|
335
|
+
"""
|
336
|
+
Get the balance of an account.
|
337
|
+
|
338
|
+
Args:
|
339
|
+
account_address: Substrate account address (uses keypair address if not specified)
|
340
|
+
|
341
|
+
Returns:
|
342
|
+
Dict[str, float]: Account balances (free, reserved, total)
|
343
|
+
"""
|
344
|
+
raise NotImplementedError("Substrate functionality is not implemented yet.")
|
345
|
+
|
346
|
+
def get_free_credits(self, account_address: Optional[str] = None) -> float:
|
347
|
+
"""
|
348
|
+
Get the free credits available for an account in the marketplace.
|
349
|
+
|
350
|
+
Args:
|
351
|
+
account_address: Substrate account address (uses keypair address if not specified)
|
352
|
+
Format: 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
|
353
|
+
|
354
|
+
Returns:
|
355
|
+
float: Free credits amount (with 18 decimal places)
|
356
|
+
|
357
|
+
Raises:
|
358
|
+
ConnectionError: If connection to Substrate fails
|
359
|
+
ValueError: If account has no credits
|
360
|
+
"""
|
361
|
+
try:
|
362
|
+
# Initialize Substrate connection if not already connected
|
363
|
+
if not hasattr(self, "_substrate") or self._substrate is None:
|
364
|
+
print("Initializing Substrate connection...")
|
365
|
+
self._substrate = SubstrateInterface(
|
366
|
+
url=self.url,
|
367
|
+
ss58_format=42, # Substrate default
|
368
|
+
type_registry_preset="substrate-node-template",
|
369
|
+
)
|
370
|
+
print(f"Connected to Substrate node at {self.url}")
|
371
|
+
|
372
|
+
# Use provided account address or default to keypair address
|
373
|
+
if not account_address:
|
374
|
+
if not hasattr(self, "_keypair") or self._keypair is None:
|
375
|
+
if not hasattr(self, "_seed_phrase") or not self._seed_phrase:
|
376
|
+
raise ValueError(
|
377
|
+
"No account address provided and no seed phrase is set"
|
378
|
+
)
|
379
|
+
|
380
|
+
print("Creating keypair from seed phrase to get account address...")
|
381
|
+
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
382
|
+
|
383
|
+
account_address = self._keypair.ss58_address
|
384
|
+
print(f"Using keypair address: {account_address}")
|
385
|
+
|
386
|
+
# Query the blockchain for free credits
|
387
|
+
print(f"Querying free credits for account: {account_address}")
|
388
|
+
result = self._substrate.query(
|
389
|
+
module="Credits",
|
390
|
+
storage_function="FreeCredits",
|
391
|
+
params=[account_address],
|
392
|
+
)
|
393
|
+
|
394
|
+
# If credits exist, convert to a float with 18 decimal places
|
395
|
+
if result.value is not None:
|
396
|
+
# Convert from blockchain u128 to float (divide by 10^18)
|
397
|
+
credits_raw = int(result.value)
|
398
|
+
credits_float = (
|
399
|
+
credits_raw / 1_000_000_000_000_000_000
|
400
|
+
) # 18 zeros for decimals
|
401
|
+
print(f"Free credits: {credits_float} ({credits_raw} raw value)")
|
402
|
+
return credits_float
|
403
|
+
else:
|
404
|
+
print(f"No credits found for account: {account_address}")
|
405
|
+
raise ValueError(f"No credits found for account: {account_address}")
|
406
|
+
|
407
|
+
except Exception as e:
|
408
|
+
error_msg = f"Error querying free credits: {str(e)}"
|
409
|
+
print(error_msg)
|
410
|
+
raise ValueError(error_msg)
|
411
|
+
|
412
|
+
def get_user_file_hashes(self, account_address: Optional[str] = None) -> List[str]:
|
413
|
+
"""
|
414
|
+
Get all file hashes (CIDs) stored by a user in the marketplace.
|
415
|
+
|
416
|
+
Args:
|
417
|
+
account_address: Substrate account address (uses keypair address if not specified)
|
418
|
+
Format: 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
|
419
|
+
|
420
|
+
Returns:
|
421
|
+
List[str]: List of CIDs stored by the user
|
422
|
+
|
423
|
+
Raises:
|
424
|
+
ConnectionError: If connection to Substrate fails
|
425
|
+
ValueError: If query fails or no files found
|
426
|
+
"""
|
427
|
+
try:
|
428
|
+
# Initialize Substrate connection if not already connected
|
429
|
+
if not hasattr(self, "_substrate") or self._substrate is None:
|
430
|
+
print("Initializing Substrate connection...")
|
431
|
+
self._substrate = SubstrateInterface(
|
432
|
+
url=self.url,
|
433
|
+
ss58_format=42, # Substrate default
|
434
|
+
type_registry_preset="substrate-node-template",
|
435
|
+
)
|
436
|
+
print(f"Connected to Substrate node at {self.url}")
|
437
|
+
|
438
|
+
# Use provided account address or default to keypair address
|
439
|
+
if not account_address:
|
440
|
+
if not hasattr(self, "_keypair") or self._keypair is None:
|
441
|
+
if not hasattr(self, "_seed_phrase") or not self._seed_phrase:
|
442
|
+
raise ValueError(
|
443
|
+
"No account address provided and no seed phrase is set"
|
444
|
+
)
|
445
|
+
|
446
|
+
print("Creating keypair from seed phrase to get account address...")
|
447
|
+
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
448
|
+
|
449
|
+
account_address = self._keypair.ss58_address
|
450
|
+
print(f"Using keypair address: {account_address}")
|
451
|
+
|
452
|
+
# Query the blockchain for user file hashes
|
453
|
+
print(f"Querying file hashes for account: {account_address}")
|
454
|
+
result = self._substrate.query(
|
455
|
+
module="Marketplace",
|
456
|
+
storage_function="UserFileHashes",
|
457
|
+
params=[account_address],
|
458
|
+
)
|
459
|
+
|
460
|
+
# If files exist, convert to a list of CIDs
|
461
|
+
if result.value:
|
462
|
+
# The result is already a list of bytes, convert each to string
|
463
|
+
file_hashes = [cid.hex() for cid in result.value]
|
464
|
+
print(f"Found {len(file_hashes)} files stored by this account")
|
465
|
+
return file_hashes
|
466
|
+
else:
|
467
|
+
print(f"No files found for account: {account_address}")
|
468
|
+
return []
|
469
|
+
|
470
|
+
except Exception as e:
|
471
|
+
error_msg = f"Error querying user file hashes: {str(e)}"
|
472
|
+
print(error_msg)
|
473
|
+
raise ValueError(error_msg)
|
474
|
+
|
475
|
+
def get_user_files(
|
476
|
+
self,
|
477
|
+
account_address: Optional[str] = None,
|
478
|
+
truncate_miners: bool = True,
|
479
|
+
max_miners: int = 3,
|
480
|
+
) -> List[Dict[str, Any]]:
|
481
|
+
"""
|
482
|
+
Get detailed information about all files stored by a user in the marketplace.
|
483
|
+
|
484
|
+
This method uses a custom JSON-RPC endpoint to get comprehensive file information.
|
485
|
+
|
486
|
+
Args:
|
487
|
+
account_address: Substrate account address (uses keypair address if not specified)
|
488
|
+
Format: 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
|
489
|
+
truncate_miners: Whether to truncate long miner IDs for display (default: True)
|
490
|
+
max_miners: Maximum number of miners to include in the response (default: 3, 0 for all)
|
491
|
+
|
492
|
+
Returns:
|
493
|
+
List[Dict[str, Any]]: List of file objects with the following structure:
|
494
|
+
{
|
495
|
+
"file_hash": str, # The IPFS CID of the file
|
496
|
+
"file_name": str, # The name of the file
|
497
|
+
"miner_ids": List[str], # List of miner IDs that have pinned the file
|
498
|
+
"miner_ids_full": List[str], # Complete list of miner IDs (if truncated)
|
499
|
+
"miner_count": int, # Total number of miners
|
500
|
+
"file_size": int, # Size of the file in bytes
|
501
|
+
"size_formatted": str # Human-readable file size
|
502
|
+
}
|
503
|
+
|
504
|
+
Raises:
|
505
|
+
ConnectionError: If connection to Substrate fails
|
506
|
+
ValueError: If query fails
|
507
|
+
"""
|
508
|
+
try:
|
509
|
+
# Initialize Substrate connection if not already connected
|
510
|
+
if not hasattr(self, "_substrate") or self._substrate is None:
|
511
|
+
print("Initializing Substrate connection...")
|
512
|
+
self._substrate = SubstrateInterface(
|
513
|
+
url=self.url,
|
514
|
+
ss58_format=42, # Substrate default
|
515
|
+
type_registry_preset="substrate-node-template",
|
516
|
+
)
|
517
|
+
print(f"Connected to Substrate node at {self.url}")
|
518
|
+
|
519
|
+
# Use provided account address or default to keypair address
|
520
|
+
if not account_address:
|
521
|
+
if not hasattr(self, "_keypair") or self._keypair is None:
|
522
|
+
if not hasattr(self, "_seed_phrase") or not self._seed_phrase:
|
523
|
+
raise ValueError(
|
524
|
+
"No account address provided and no seed phrase is set"
|
525
|
+
)
|
526
|
+
|
527
|
+
print("Creating keypair from seed phrase to get account address...")
|
528
|
+
self._keypair = Keypair.create_from_mnemonic(self._seed_phrase)
|
529
|
+
|
530
|
+
account_address = self._keypair.ss58_address
|
531
|
+
print(f"Using keypair address: {account_address}")
|
532
|
+
|
533
|
+
# Prepare the JSON-RPC request
|
534
|
+
request = {
|
535
|
+
"jsonrpc": "2.0",
|
536
|
+
"method": "get_user_files",
|
537
|
+
"params": [account_address],
|
538
|
+
"id": 1,
|
539
|
+
}
|
540
|
+
|
541
|
+
print(f"Querying detailed file information for account: {account_address}")
|
542
|
+
|
543
|
+
# Make the JSON-RPC call
|
544
|
+
response = self._substrate.rpc_request(
|
545
|
+
method="get_user_files", params=[account_address]
|
546
|
+
)
|
547
|
+
|
548
|
+
# Check for errors in the response
|
549
|
+
if "error" in response:
|
550
|
+
error_msg = (
|
551
|
+
f"RPC error: {response['error'].get('message', 'Unknown error')}"
|
552
|
+
)
|
553
|
+
print(error_msg)
|
554
|
+
raise ValueError(error_msg)
|
555
|
+
|
556
|
+
# Extract the result
|
557
|
+
files = response.get("result", [])
|
558
|
+
print(f"Found {len(files)} files stored by this account")
|
559
|
+
|
560
|
+
# Helper function to convert ASCII code arrays to strings
|
561
|
+
def ascii_to_string(value):
|
562
|
+
if isinstance(value, list) and all(isinstance(x, int) for x in value):
|
563
|
+
return "".join(chr(code) for code in value)
|
564
|
+
return str(value)
|
565
|
+
|
566
|
+
# Helper function to properly format CIDs
|
567
|
+
def format_cid(cid_str):
|
568
|
+
# If it already looks like a proper CID, return it as is
|
569
|
+
if cid_str.startswith(("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")):
|
570
|
+
return cid_str
|
571
|
+
|
572
|
+
# Check if it's a hex string
|
573
|
+
if all(c in "0123456789abcdefABCDEF" for c in cid_str):
|
574
|
+
# First try the special case where the hex string is actually ASCII encoded
|
575
|
+
try:
|
576
|
+
# Try to decode the hex as ASCII characters
|
577
|
+
# (This is the case with some substrate responses where the CID is hex-encoded ASCII)
|
578
|
+
hex_bytes = bytes.fromhex(cid_str)
|
579
|
+
ascii_str = hex_bytes.decode("ascii")
|
580
|
+
|
581
|
+
# If the decoded string starts with a valid CID prefix, return it
|
582
|
+
if ascii_str.startswith(
|
583
|
+
("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")
|
584
|
+
):
|
585
|
+
return ascii_str
|
586
|
+
except Exception:
|
587
|
+
pass
|
588
|
+
|
589
|
+
# If the above doesn't work, try the standard CID decoding
|
590
|
+
try:
|
591
|
+
import base58
|
592
|
+
import binascii
|
593
|
+
|
594
|
+
# Try to decode hex to binary then to base58 for CIDv0
|
595
|
+
try:
|
596
|
+
binary_data = binascii.unhexlify(cid_str)
|
597
|
+
if (
|
598
|
+
len(binary_data) > 2
|
599
|
+
and binary_data[0] == 0x12
|
600
|
+
and binary_data[1] == 0x20
|
601
|
+
):
|
602
|
+
# This looks like a CIDv0 (Qm...)
|
603
|
+
decoded_cid = base58.b58encode(binary_data).decode(
|
604
|
+
"utf-8"
|
605
|
+
)
|
606
|
+
return decoded_cid
|
607
|
+
except Exception:
|
608
|
+
pass
|
609
|
+
|
610
|
+
# If not successful, just return hex with 0x prefix as fallback
|
611
|
+
return f"0x{cid_str}"
|
612
|
+
except ImportError:
|
613
|
+
# If base58 is not available, return hex with prefix
|
614
|
+
return f"0x{cid_str}"
|
615
|
+
|
616
|
+
# Default case - return as is
|
617
|
+
return cid_str
|
618
|
+
|
619
|
+
# Helper function to format file sizes
|
620
|
+
def format_file_size(size_bytes):
|
621
|
+
if size_bytes >= 1024 * 1024:
|
622
|
+
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
623
|
+
else:
|
624
|
+
return f"{size_bytes / 1024:.2f} KB"
|
625
|
+
|
626
|
+
# Helper function to format miner IDs for display
|
627
|
+
def format_miner_id(miner_id):
|
628
|
+
if (
|
629
|
+
truncate_miners
|
630
|
+
and isinstance(miner_id, str)
|
631
|
+
and miner_id.startswith("1")
|
632
|
+
and len(miner_id) > 40
|
633
|
+
):
|
634
|
+
# Truncate long peer IDs
|
635
|
+
return f"{miner_id[:12]}...{miner_id[-4:]}"
|
636
|
+
return miner_id
|
637
|
+
|
638
|
+
# Process the response
|
639
|
+
processed_files = []
|
640
|
+
for file in files:
|
641
|
+
processed_file = {"file_size": file.get("file_size", 0)}
|
642
|
+
|
643
|
+
# Add formatted file size
|
644
|
+
processed_file["size_formatted"] = format_file_size(
|
645
|
+
processed_file["file_size"]
|
646
|
+
)
|
647
|
+
|
648
|
+
# Convert file_hash from byte array to string
|
649
|
+
if "file_hash" in file:
|
650
|
+
cid_str = ascii_to_string(file["file_hash"])
|
651
|
+
processed_file["file_hash"] = format_cid(cid_str)
|
652
|
+
|
653
|
+
# Convert file_name from byte array to string
|
654
|
+
if "file_name" in file:
|
655
|
+
processed_file["file_name"] = ascii_to_string(file["file_name"])
|
656
|
+
|
657
|
+
# Convert miner_ids from byte arrays to strings
|
658
|
+
if "miner_ids" in file and isinstance(file["miner_ids"], list):
|
659
|
+
all_miners = [
|
660
|
+
ascii_to_string(miner_id) for miner_id in file["miner_ids"]
|
661
|
+
]
|
662
|
+
processed_file["miner_ids_full"] = all_miners
|
663
|
+
processed_file["miner_count"] = len(all_miners)
|
664
|
+
|
665
|
+
# Truncate miner list if requested
|
666
|
+
if max_miners > 0 and len(all_miners) > max_miners:
|
667
|
+
displayed_miners = all_miners[:max_miners]
|
668
|
+
else:
|
669
|
+
displayed_miners = all_miners
|
670
|
+
|
671
|
+
# Format and store the displayed miners
|
672
|
+
processed_file["miner_ids"] = [
|
673
|
+
{"id": miner_id, "formatted": format_miner_id(miner_id)}
|
674
|
+
for miner_id in displayed_miners
|
675
|
+
]
|
676
|
+
else:
|
677
|
+
processed_file["miner_ids"] = []
|
678
|
+
processed_file["miner_ids_full"] = []
|
679
|
+
processed_file["miner_count"] = 0
|
680
|
+
|
681
|
+
processed_files.append(processed_file)
|
682
|
+
|
683
|
+
return processed_files
|
684
|
+
|
685
|
+
except Exception as e:
|
686
|
+
error_msg = f"Error querying user files: {str(e)}"
|
687
|
+
print(error_msg)
|
688
|
+
raise ValueError(error_msg)
|