synapse-filecoin-sdk 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.
- pynapse/__init__.py +6 -0
- pynapse/_version.py +1 -0
- pynapse/contracts/__init__.py +34 -0
- pynapse/contracts/abi_registry.py +11 -0
- pynapse/contracts/addresses.json +30 -0
- pynapse/contracts/erc20_abi.json +92 -0
- pynapse/contracts/errorsAbi.json +933 -0
- pynapse/contracts/filecoinPayV1Abi.json +2424 -0
- pynapse/contracts/filecoinWarmStorageServiceAbi.json +2363 -0
- pynapse/contracts/filecoinWarmStorageServiceStateViewAbi.json +651 -0
- pynapse/contracts/generated.py +35 -0
- pynapse/contracts/payments_abi.json +205 -0
- pynapse/contracts/pdpVerifierAbi.json +1266 -0
- pynapse/contracts/providerIdSetAbi.json +161 -0
- pynapse/contracts/serviceProviderRegistryAbi.json +1479 -0
- pynapse/contracts/sessionKeyRegistryAbi.json +147 -0
- pynapse/core/__init__.py +68 -0
- pynapse/core/abis.py +25 -0
- pynapse/core/chains.py +97 -0
- pynapse/core/constants.py +27 -0
- pynapse/core/errors.py +22 -0
- pynapse/core/piece.py +263 -0
- pynapse/core/rand.py +14 -0
- pynapse/core/typed_data.py +320 -0
- pynapse/core/utils.py +30 -0
- pynapse/evm/__init__.py +3 -0
- pynapse/evm/client.py +26 -0
- pynapse/filbeam/__init__.py +3 -0
- pynapse/filbeam/service.py +39 -0
- pynapse/payments/__init__.py +17 -0
- pynapse/payments/service.py +826 -0
- pynapse/pdp/__init__.py +21 -0
- pynapse/pdp/server.py +331 -0
- pynapse/pdp/types.py +38 -0
- pynapse/pdp/verifier.py +82 -0
- pynapse/retriever/__init__.py +12 -0
- pynapse/retriever/async_chain.py +227 -0
- pynapse/retriever/chain.py +209 -0
- pynapse/session/__init__.py +12 -0
- pynapse/session/key.py +30 -0
- pynapse/session/permissions.py +57 -0
- pynapse/session/registry.py +90 -0
- pynapse/sp_registry/__init__.py +11 -0
- pynapse/sp_registry/capabilities.py +25 -0
- pynapse/sp_registry/pdp_capabilities.py +102 -0
- pynapse/sp_registry/service.py +446 -0
- pynapse/sp_registry/types.py +52 -0
- pynapse/storage/__init__.py +57 -0
- pynapse/storage/async_context.py +682 -0
- pynapse/storage/async_manager.py +757 -0
- pynapse/storage/context.py +680 -0
- pynapse/storage/manager.py +758 -0
- pynapse/synapse.py +191 -0
- pynapse/utils/__init__.py +25 -0
- pynapse/utils/constants.py +25 -0
- pynapse/utils/errors.py +3 -0
- pynapse/utils/metadata.py +35 -0
- pynapse/utils/piece_url.py +16 -0
- pynapse/warm_storage/__init__.py +13 -0
- pynapse/warm_storage/service.py +513 -0
- synapse_filecoin_sdk-0.1.0.dist-info/METADATA +74 -0
- synapse_filecoin_sdk-0.1.0.dist-info/RECORD +64 -0
- synapse_filecoin_sdk-0.1.0.dist-info/WHEEL +4 -0
- synapse_filecoin_sdk-0.1.0.dist-info/licenses/LICENSE.md +228 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsyncStorageContext - Async version of StorageContext for Python async/await patterns.
|
|
3
|
+
|
|
4
|
+
Represents a specific Service Provider + DataSet pair with full async support.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import random
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Callable, Dict, List, Optional, TYPE_CHECKING, Awaitable
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from pynapse.core.piece import calculate_piece_cid
|
|
15
|
+
from pynapse.core.typed_data import sign_add_pieces_extra_data, sign_create_dataset_extra_data
|
|
16
|
+
from pynapse.pdp import AsyncPDPServer
|
|
17
|
+
from pynapse.utils.metadata import combine_metadata, metadata_matches, metadata_object_to_entries
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from pynapse.sp_registry import AsyncSPRegistryService, ProviderInfo
|
|
21
|
+
from pynapse.warm_storage import AsyncWarmStorageService
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Size constants
|
|
25
|
+
MIN_UPLOAD_SIZE = 256 # bytes
|
|
26
|
+
MAX_UPLOAD_SIZE = 254 * 1024 * 1024 # 254 MiB
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class AsyncUploadResult:
|
|
31
|
+
"""Result of an async upload operation."""
|
|
32
|
+
piece_cid: str
|
|
33
|
+
size: int
|
|
34
|
+
tx_hash: Optional[str] = None
|
|
35
|
+
piece_id: Optional[int] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class AsyncProviderSelectionResult:
|
|
40
|
+
"""Result of async provider and dataset selection."""
|
|
41
|
+
provider: "ProviderInfo"
|
|
42
|
+
pdp_endpoint: str
|
|
43
|
+
data_set_id: int # -1 means needs to be created
|
|
44
|
+
client_data_set_id: int
|
|
45
|
+
is_existing: bool
|
|
46
|
+
metadata: Dict[str, str] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class AsyncStorageContextOptions:
|
|
51
|
+
"""Options for creating an async storage context."""
|
|
52
|
+
provider_id: Optional[int] = None
|
|
53
|
+
provider_address: Optional[str] = None
|
|
54
|
+
data_set_id: Optional[int] = None
|
|
55
|
+
with_cdn: bool = False
|
|
56
|
+
force_create_data_set: bool = False
|
|
57
|
+
metadata: Optional[Dict[str, str]] = None
|
|
58
|
+
exclude_provider_ids: Optional[List[int]] = None
|
|
59
|
+
# Async callbacks
|
|
60
|
+
on_provider_selected: Optional[Callable[["ProviderInfo"], Awaitable[None]]] = None
|
|
61
|
+
on_data_set_resolved: Optional[Callable[[dict], Awaitable[None]]] = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AsyncStorageContext:
|
|
65
|
+
"""
|
|
66
|
+
Async storage context for a specific provider and dataset.
|
|
67
|
+
|
|
68
|
+
Use the factory methods `create()` or `create_contexts()` to construct
|
|
69
|
+
instances with proper provider selection and dataset resolution.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
ctx = await AsyncStorageContext.create(
|
|
73
|
+
chain=chain,
|
|
74
|
+
private_key=private_key,
|
|
75
|
+
warm_storage=warm_storage,
|
|
76
|
+
sp_registry=sp_registry,
|
|
77
|
+
)
|
|
78
|
+
result = await ctx.upload(data)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
pdp_endpoint: str,
|
|
84
|
+
chain,
|
|
85
|
+
private_key: str,
|
|
86
|
+
data_set_id: int,
|
|
87
|
+
client_data_set_id: int,
|
|
88
|
+
provider: Optional["ProviderInfo"] = None,
|
|
89
|
+
with_cdn: bool = False,
|
|
90
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
self._pdp = AsyncPDPServer(pdp_endpoint)
|
|
93
|
+
self._pdp_endpoint = pdp_endpoint
|
|
94
|
+
self._chain = chain
|
|
95
|
+
self._private_key = private_key
|
|
96
|
+
self._data_set_id = data_set_id
|
|
97
|
+
self._client_data_set_id = client_data_set_id
|
|
98
|
+
self._provider = provider
|
|
99
|
+
self._with_cdn = with_cdn
|
|
100
|
+
self._metadata = metadata or {}
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def data_set_id(self) -> int:
|
|
104
|
+
return self._data_set_id
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def client_data_set_id(self) -> int:
|
|
108
|
+
return self._client_data_set_id
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def provider(self) -> Optional["ProviderInfo"]:
|
|
112
|
+
return self._provider
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def with_cdn(self) -> bool:
|
|
116
|
+
return self._with_cdn
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def data_set_metadata(self) -> Dict[str, str]:
|
|
120
|
+
return self._metadata
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _validate_size(size_bytes: int, context: str = "upload") -> None:
|
|
124
|
+
"""Validate data size against limits."""
|
|
125
|
+
if size_bytes < MIN_UPLOAD_SIZE:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Data size {size_bytes} bytes is below minimum allowed size of {MIN_UPLOAD_SIZE} bytes"
|
|
128
|
+
)
|
|
129
|
+
if size_bytes > MAX_UPLOAD_SIZE:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Data size {size_bytes} bytes exceeds maximum allowed size of {MAX_UPLOAD_SIZE} bytes "
|
|
132
|
+
f"({MAX_UPLOAD_SIZE // 1024 // 1024} MiB)"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
async def create(
|
|
137
|
+
cls,
|
|
138
|
+
chain,
|
|
139
|
+
private_key: str,
|
|
140
|
+
warm_storage: "AsyncWarmStorageService",
|
|
141
|
+
sp_registry: "AsyncSPRegistryService",
|
|
142
|
+
options: Optional[AsyncStorageContextOptions] = None,
|
|
143
|
+
) -> "AsyncStorageContext":
|
|
144
|
+
"""
|
|
145
|
+
Create an async storage context with smart provider and dataset selection.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
chain: The chain configuration
|
|
149
|
+
private_key: Private key for signing
|
|
150
|
+
warm_storage: AsyncWarmStorageService instance
|
|
151
|
+
sp_registry: AsyncSPRegistryService instance
|
|
152
|
+
options: Optional configuration for context creation
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
A configured AsyncStorageContext instance
|
|
156
|
+
"""
|
|
157
|
+
from eth_account import Account
|
|
158
|
+
acct = Account.from_key(private_key)
|
|
159
|
+
client_address = acct.address
|
|
160
|
+
|
|
161
|
+
options = options or AsyncStorageContextOptions()
|
|
162
|
+
requested_metadata = combine_metadata(options.metadata, options.with_cdn)
|
|
163
|
+
|
|
164
|
+
# Resolve provider and dataset
|
|
165
|
+
resolution = await cls._resolve_provider_and_data_set(
|
|
166
|
+
client_address=client_address,
|
|
167
|
+
chain=chain,
|
|
168
|
+
private_key=private_key,
|
|
169
|
+
warm_storage=warm_storage,
|
|
170
|
+
sp_registry=sp_registry,
|
|
171
|
+
options=options,
|
|
172
|
+
requested_metadata=requested_metadata,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Fire callbacks
|
|
176
|
+
if options.on_provider_selected and resolution.provider:
|
|
177
|
+
try:
|
|
178
|
+
await options.on_provider_selected(resolution.provider)
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
# Create dataset if needed
|
|
183
|
+
data_set_id = resolution.data_set_id
|
|
184
|
+
client_data_set_id = resolution.client_data_set_id
|
|
185
|
+
|
|
186
|
+
if data_set_id == -1:
|
|
187
|
+
# Need to create a new dataset
|
|
188
|
+
pdp = AsyncPDPServer(resolution.pdp_endpoint)
|
|
189
|
+
|
|
190
|
+
# Use a random client_data_set_id like the TypeScript SDK does
|
|
191
|
+
# This ensures uniqueness and avoids collisions with existing datasets
|
|
192
|
+
from pynapse.core.rand import rand_u256
|
|
193
|
+
next_client_id = rand_u256()
|
|
194
|
+
|
|
195
|
+
# Convert metadata dict to list of {key, value} entries
|
|
196
|
+
metadata_entries = metadata_object_to_entries(requested_metadata)
|
|
197
|
+
|
|
198
|
+
extra_data = sign_create_dataset_extra_data(
|
|
199
|
+
private_key=private_key,
|
|
200
|
+
chain=chain,
|
|
201
|
+
client_data_set_id=next_client_id,
|
|
202
|
+
payee=resolution.provider.payee,
|
|
203
|
+
metadata=metadata_entries,
|
|
204
|
+
)
|
|
205
|
+
resp = await pdp.create_data_set(
|
|
206
|
+
record_keeper=chain.contracts.warm_storage,
|
|
207
|
+
extra_data=extra_data,
|
|
208
|
+
)
|
|
209
|
+
# Wait for creation
|
|
210
|
+
status = await pdp.wait_for_data_set_creation(resp.tx_hash)
|
|
211
|
+
data_set_id = status.data_set_id
|
|
212
|
+
# Get client_data_set_id from the new dataset
|
|
213
|
+
ds_info = await warm_storage.get_data_set(data_set_id)
|
|
214
|
+
client_data_set_id = ds_info.client_data_set_id
|
|
215
|
+
|
|
216
|
+
# Fire dataset resolved callback
|
|
217
|
+
if options.on_data_set_resolved:
|
|
218
|
+
try:
|
|
219
|
+
await options.on_data_set_resolved({
|
|
220
|
+
"is_existing": resolution.is_existing,
|
|
221
|
+
"data_set_id": data_set_id,
|
|
222
|
+
"provider": resolution.provider,
|
|
223
|
+
})
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
return cls(
|
|
228
|
+
pdp_endpoint=resolution.pdp_endpoint,
|
|
229
|
+
chain=chain,
|
|
230
|
+
private_key=private_key,
|
|
231
|
+
data_set_id=data_set_id,
|
|
232
|
+
client_data_set_id=client_data_set_id,
|
|
233
|
+
provider=resolution.provider,
|
|
234
|
+
with_cdn=options.with_cdn,
|
|
235
|
+
metadata=requested_metadata,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
async def create_contexts(
|
|
240
|
+
cls,
|
|
241
|
+
chain,
|
|
242
|
+
private_key: str,
|
|
243
|
+
warm_storage: "AsyncWarmStorageService",
|
|
244
|
+
sp_registry: "AsyncSPRegistryService",
|
|
245
|
+
count: int = 2,
|
|
246
|
+
options: Optional[AsyncStorageContextOptions] = None,
|
|
247
|
+
) -> List["AsyncStorageContext"]:
|
|
248
|
+
"""
|
|
249
|
+
Create multiple async storage contexts for multi-provider redundancy.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
chain: The chain configuration
|
|
253
|
+
private_key: Private key for signing
|
|
254
|
+
warm_storage: AsyncWarmStorageService instance
|
|
255
|
+
sp_registry: AsyncSPRegistryService instance
|
|
256
|
+
count: Number of contexts to create (default: 2)
|
|
257
|
+
options: Optional configuration for context creation
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of configured AsyncStorageContext instances
|
|
261
|
+
"""
|
|
262
|
+
contexts: List[AsyncStorageContext] = []
|
|
263
|
+
used_provider_ids: List[int] = []
|
|
264
|
+
|
|
265
|
+
options = options or AsyncStorageContextOptions()
|
|
266
|
+
|
|
267
|
+
for _ in range(count):
|
|
268
|
+
# Build options with exclusions
|
|
269
|
+
ctx_options = AsyncStorageContextOptions(
|
|
270
|
+
provider_id=options.provider_id if not contexts else None,
|
|
271
|
+
provider_address=options.provider_address if not contexts else None,
|
|
272
|
+
data_set_id=options.data_set_id if not contexts else None,
|
|
273
|
+
with_cdn=options.with_cdn,
|
|
274
|
+
force_create_data_set=options.force_create_data_set,
|
|
275
|
+
metadata=options.metadata,
|
|
276
|
+
exclude_provider_ids=(options.exclude_provider_ids or []) + used_provider_ids,
|
|
277
|
+
on_provider_selected=options.on_provider_selected,
|
|
278
|
+
on_data_set_resolved=options.on_data_set_resolved,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
ctx = await cls.create(
|
|
283
|
+
chain=chain,
|
|
284
|
+
private_key=private_key,
|
|
285
|
+
warm_storage=warm_storage,
|
|
286
|
+
sp_registry=sp_registry,
|
|
287
|
+
options=ctx_options,
|
|
288
|
+
)
|
|
289
|
+
contexts.append(ctx)
|
|
290
|
+
if ctx.provider:
|
|
291
|
+
used_provider_ids.append(ctx.provider.provider_id)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
# If we can't create more contexts, return what we have
|
|
294
|
+
if not contexts:
|
|
295
|
+
raise
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
return contexts
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
async def _resolve_provider_and_data_set(
|
|
302
|
+
cls,
|
|
303
|
+
client_address: str,
|
|
304
|
+
chain,
|
|
305
|
+
private_key: str,
|
|
306
|
+
warm_storage: "AsyncWarmStorageService",
|
|
307
|
+
sp_registry: "AsyncSPRegistryService",
|
|
308
|
+
options: AsyncStorageContextOptions,
|
|
309
|
+
requested_metadata: Dict[str, str],
|
|
310
|
+
) -> AsyncProviderSelectionResult:
|
|
311
|
+
"""Resolve provider and dataset based on options."""
|
|
312
|
+
|
|
313
|
+
# 1. If explicit data_set_id provided
|
|
314
|
+
if options.data_set_id is not None and not options.force_create_data_set:
|
|
315
|
+
return await cls._resolve_by_data_set_id(
|
|
316
|
+
data_set_id=options.data_set_id,
|
|
317
|
+
client_address=client_address,
|
|
318
|
+
warm_storage=warm_storage,
|
|
319
|
+
sp_registry=sp_registry,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# 2. If explicit provider_id provided
|
|
323
|
+
if options.provider_id is not None:
|
|
324
|
+
return await cls._resolve_by_provider_id(
|
|
325
|
+
provider_id=options.provider_id,
|
|
326
|
+
client_address=client_address,
|
|
327
|
+
warm_storage=warm_storage,
|
|
328
|
+
sp_registry=sp_registry,
|
|
329
|
+
requested_metadata=requested_metadata,
|
|
330
|
+
force_create=options.force_create_data_set,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# 3. If explicit provider_address provided
|
|
334
|
+
if options.provider_address is not None:
|
|
335
|
+
provider = await sp_registry.get_provider_by_address(options.provider_address)
|
|
336
|
+
if provider is None:
|
|
337
|
+
raise ValueError(f"Provider {options.provider_address} not found in registry")
|
|
338
|
+
return await cls._resolve_by_provider_id(
|
|
339
|
+
provider_id=provider.provider_id,
|
|
340
|
+
client_address=client_address,
|
|
341
|
+
warm_storage=warm_storage,
|
|
342
|
+
sp_registry=sp_registry,
|
|
343
|
+
requested_metadata=requested_metadata,
|
|
344
|
+
force_create=options.force_create_data_set,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# 4. Smart selection
|
|
348
|
+
return await cls._smart_select_provider(
|
|
349
|
+
client_address=client_address,
|
|
350
|
+
warm_storage=warm_storage,
|
|
351
|
+
sp_registry=sp_registry,
|
|
352
|
+
requested_metadata=requested_metadata,
|
|
353
|
+
exclude_provider_ids=options.exclude_provider_ids or [],
|
|
354
|
+
force_create=options.force_create_data_set,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
async def _resolve_by_data_set_id(
|
|
359
|
+
cls,
|
|
360
|
+
data_set_id: int,
|
|
361
|
+
client_address: str,
|
|
362
|
+
warm_storage: "AsyncWarmStorageService",
|
|
363
|
+
sp_registry: "AsyncSPRegistryService",
|
|
364
|
+
) -> AsyncProviderSelectionResult:
|
|
365
|
+
"""Resolve using explicit dataset ID."""
|
|
366
|
+
await warm_storage.validate_data_set(data_set_id)
|
|
367
|
+
ds_info = await warm_storage.get_data_set(data_set_id)
|
|
368
|
+
|
|
369
|
+
if ds_info.payer.lower() != client_address.lower():
|
|
370
|
+
raise ValueError(
|
|
371
|
+
f"Data set {data_set_id} is not owned by {client_address} (owned by {ds_info.payer})"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
provider = await sp_registry.get_provider(ds_info.provider_id)
|
|
375
|
+
if provider is None:
|
|
376
|
+
raise ValueError(f"Provider ID {ds_info.provider_id} for data set {data_set_id} not found")
|
|
377
|
+
|
|
378
|
+
# Get PDP endpoint from provider product info
|
|
379
|
+
pdp_endpoint = await cls._get_pdp_endpoint(sp_registry, provider.provider_id)
|
|
380
|
+
metadata = await warm_storage.get_all_data_set_metadata(data_set_id)
|
|
381
|
+
|
|
382
|
+
return AsyncProviderSelectionResult(
|
|
383
|
+
provider=provider,
|
|
384
|
+
pdp_endpoint=pdp_endpoint,
|
|
385
|
+
data_set_id=data_set_id,
|
|
386
|
+
client_data_set_id=ds_info.client_data_set_id,
|
|
387
|
+
is_existing=True,
|
|
388
|
+
metadata=metadata,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
@classmethod
|
|
392
|
+
async def _resolve_by_provider_id(
|
|
393
|
+
cls,
|
|
394
|
+
provider_id: int,
|
|
395
|
+
client_address: str,
|
|
396
|
+
warm_storage: "AsyncWarmStorageService",
|
|
397
|
+
sp_registry: "AsyncSPRegistryService",
|
|
398
|
+
requested_metadata: Dict[str, str],
|
|
399
|
+
force_create: bool = False,
|
|
400
|
+
) -> AsyncProviderSelectionResult:
|
|
401
|
+
"""Resolve by provider ID, finding or creating dataset."""
|
|
402
|
+
provider = await sp_registry.get_provider(provider_id)
|
|
403
|
+
if provider is None:
|
|
404
|
+
raise ValueError(f"Provider ID {provider_id} not found in registry")
|
|
405
|
+
|
|
406
|
+
pdp_endpoint = await cls._get_pdp_endpoint(sp_registry, provider_id)
|
|
407
|
+
|
|
408
|
+
if force_create:
|
|
409
|
+
return AsyncProviderSelectionResult(
|
|
410
|
+
provider=provider,
|
|
411
|
+
pdp_endpoint=pdp_endpoint,
|
|
412
|
+
data_set_id=-1,
|
|
413
|
+
client_data_set_id=0,
|
|
414
|
+
is_existing=False,
|
|
415
|
+
metadata=requested_metadata,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Try to find existing dataset for this provider
|
|
419
|
+
try:
|
|
420
|
+
datasets = await warm_storage.get_client_data_sets(client_address)
|
|
421
|
+
for ds in datasets:
|
|
422
|
+
if ds.provider_id == provider_id and ds.pdp_end_epoch == 0:
|
|
423
|
+
# Check metadata match
|
|
424
|
+
ds_metadata = await warm_storage.get_all_data_set_metadata(ds.data_set_id)
|
|
425
|
+
if metadata_matches(ds_metadata, requested_metadata):
|
|
426
|
+
return AsyncProviderSelectionResult(
|
|
427
|
+
provider=provider,
|
|
428
|
+
pdp_endpoint=pdp_endpoint,
|
|
429
|
+
data_set_id=ds.data_set_id,
|
|
430
|
+
client_data_set_id=ds.client_data_set_id,
|
|
431
|
+
is_existing=True,
|
|
432
|
+
metadata=ds_metadata,
|
|
433
|
+
)
|
|
434
|
+
except Exception:
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
# No matching dataset found, need to create
|
|
438
|
+
return AsyncProviderSelectionResult(
|
|
439
|
+
provider=provider,
|
|
440
|
+
pdp_endpoint=pdp_endpoint,
|
|
441
|
+
data_set_id=-1,
|
|
442
|
+
client_data_set_id=0,
|
|
443
|
+
is_existing=False,
|
|
444
|
+
metadata=requested_metadata,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
async def _smart_select_provider(
|
|
449
|
+
cls,
|
|
450
|
+
client_address: str,
|
|
451
|
+
warm_storage: "AsyncWarmStorageService",
|
|
452
|
+
sp_registry: "AsyncSPRegistryService",
|
|
453
|
+
requested_metadata: Dict[str, str],
|
|
454
|
+
exclude_provider_ids: List[int],
|
|
455
|
+
force_create: bool = False,
|
|
456
|
+
) -> AsyncProviderSelectionResult:
|
|
457
|
+
"""Smart provider selection with existing dataset reuse."""
|
|
458
|
+
exclude_set = set(exclude_provider_ids)
|
|
459
|
+
|
|
460
|
+
# First, try to find existing datasets with matching metadata
|
|
461
|
+
if not force_create:
|
|
462
|
+
try:
|
|
463
|
+
datasets = await warm_storage.get_client_data_sets_with_details(client_address)
|
|
464
|
+
# Filter for live, managed datasets with matching metadata
|
|
465
|
+
matching = [
|
|
466
|
+
ds for ds in datasets
|
|
467
|
+
if ds.is_live
|
|
468
|
+
and ds.is_managed
|
|
469
|
+
and ds.pdp_end_epoch == 0
|
|
470
|
+
and ds.provider_id not in exclude_set
|
|
471
|
+
and metadata_matches(ds.metadata, requested_metadata)
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
# Prefer datasets with pieces, sorted by ID (older first)
|
|
475
|
+
matching.sort(key=lambda ds: (-ds.active_piece_count, ds.data_set_id))
|
|
476
|
+
|
|
477
|
+
for ds in matching:
|
|
478
|
+
provider = await sp_registry.get_provider(ds.provider_id)
|
|
479
|
+
if provider and provider.is_active:
|
|
480
|
+
# Health check: try to ping the PDP endpoint
|
|
481
|
+
pdp_endpoint = await cls._get_pdp_endpoint(sp_registry, ds.provider_id)
|
|
482
|
+
if await cls._ping_provider(pdp_endpoint):
|
|
483
|
+
return AsyncProviderSelectionResult(
|
|
484
|
+
provider=provider,
|
|
485
|
+
pdp_endpoint=pdp_endpoint,
|
|
486
|
+
data_set_id=ds.data_set_id,
|
|
487
|
+
client_data_set_id=ds.client_data_set_id,
|
|
488
|
+
is_existing=True,
|
|
489
|
+
metadata=ds.metadata,
|
|
490
|
+
)
|
|
491
|
+
except Exception:
|
|
492
|
+
pass
|
|
493
|
+
|
|
494
|
+
# No existing dataset, select a new provider
|
|
495
|
+
try:
|
|
496
|
+
approved_ids = await warm_storage.get_approved_provider_ids()
|
|
497
|
+
except Exception:
|
|
498
|
+
approved_ids = []
|
|
499
|
+
|
|
500
|
+
# Filter out excluded providers
|
|
501
|
+
candidate_ids = [pid for pid in approved_ids if pid not in exclude_set]
|
|
502
|
+
|
|
503
|
+
# Shuffle for random selection
|
|
504
|
+
random.shuffle(candidate_ids)
|
|
505
|
+
|
|
506
|
+
# Find a healthy provider
|
|
507
|
+
for pid in candidate_ids:
|
|
508
|
+
try:
|
|
509
|
+
provider = await sp_registry.get_provider(pid)
|
|
510
|
+
if provider and provider.is_active:
|
|
511
|
+
pdp_endpoint = await cls._get_pdp_endpoint(sp_registry, pid)
|
|
512
|
+
if await cls._ping_provider(pdp_endpoint):
|
|
513
|
+
return AsyncProviderSelectionResult(
|
|
514
|
+
provider=provider,
|
|
515
|
+
pdp_endpoint=pdp_endpoint,
|
|
516
|
+
data_set_id=-1,
|
|
517
|
+
client_data_set_id=0,
|
|
518
|
+
is_existing=False,
|
|
519
|
+
metadata=requested_metadata,
|
|
520
|
+
)
|
|
521
|
+
except Exception:
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
raise ValueError("No approved service providers available")
|
|
525
|
+
|
|
526
|
+
@classmethod
|
|
527
|
+
async def _get_pdp_endpoint(cls, sp_registry: "AsyncSPRegistryService", provider_id: int) -> str:
|
|
528
|
+
"""Get the PDP service URL for a provider."""
|
|
529
|
+
try:
|
|
530
|
+
product = await sp_registry.get_provider_with_product(provider_id, 0) # PDP product type
|
|
531
|
+
# Look for serviceURL in capability values
|
|
532
|
+
for i, key in enumerate(product.product.capability_keys):
|
|
533
|
+
if key == "serviceURL" and i < len(product.product_capability_values):
|
|
534
|
+
val = product.product_capability_values[i]
|
|
535
|
+
# Values are returned as bytes from the contract
|
|
536
|
+
if isinstance(val, bytes):
|
|
537
|
+
return val.decode('utf-8')
|
|
538
|
+
return str(val)
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
raise ValueError(f"Could not find PDP endpoint for provider {provider_id}")
|
|
543
|
+
|
|
544
|
+
@classmethod
|
|
545
|
+
async def _ping_provider(cls, pdp_endpoint: str, timeout: float = 5.0) -> bool:
|
|
546
|
+
"""Health check a provider's PDP endpoint."""
|
|
547
|
+
try:
|
|
548
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
549
|
+
resp = await client.head(pdp_endpoint)
|
|
550
|
+
return resp.status_code < 500
|
|
551
|
+
except Exception:
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
async def upload(
|
|
555
|
+
self,
|
|
556
|
+
data: bytes,
|
|
557
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
558
|
+
on_progress: Optional[Callable[[int], Awaitable[None]]] = None,
|
|
559
|
+
on_upload_complete: Optional[Callable[[str], Awaitable[None]]] = None,
|
|
560
|
+
on_pieces_added: Optional[Callable[[str], Awaitable[None]]] = None,
|
|
561
|
+
) -> AsyncUploadResult:
|
|
562
|
+
"""
|
|
563
|
+
Upload data to this storage context asynchronously.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
data: Bytes to upload
|
|
567
|
+
metadata: Optional piece metadata
|
|
568
|
+
on_progress: Async callback for upload progress
|
|
569
|
+
on_upload_complete: Async callback when upload completes
|
|
570
|
+
on_pieces_added: Async callback when pieces are added on-chain
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
AsyncUploadResult with piece CID and transaction info
|
|
574
|
+
"""
|
|
575
|
+
self._validate_size(len(data))
|
|
576
|
+
|
|
577
|
+
info = calculate_piece_cid(data)
|
|
578
|
+
|
|
579
|
+
# Upload to PDP server (include padded_piece_size for PieceCIDv1)
|
|
580
|
+
await self._pdp.upload_piece(data, info.piece_cid, info.padded_piece_size)
|
|
581
|
+
|
|
582
|
+
# Wait for piece to be indexed before adding to dataset
|
|
583
|
+
# The PDP server needs time to process and index uploaded pieces
|
|
584
|
+
await self._pdp.wait_for_piece(info.piece_cid, timeout_seconds=60, poll_interval=2)
|
|
585
|
+
|
|
586
|
+
if on_upload_complete:
|
|
587
|
+
try:
|
|
588
|
+
await on_upload_complete(info.piece_cid)
|
|
589
|
+
except Exception:
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
# Add piece to dataset
|
|
593
|
+
pieces = [(info.piece_cid, [{"key": k, "value": v} for k, v in (metadata or {}).items()])]
|
|
594
|
+
extra_data = sign_add_pieces_extra_data(
|
|
595
|
+
private_key=self._private_key,
|
|
596
|
+
chain=self._chain,
|
|
597
|
+
client_data_set_id=self._client_data_set_id,
|
|
598
|
+
pieces=pieces,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
add_resp = await self._pdp.add_pieces(self._data_set_id, [info.piece_cid], extra_data)
|
|
602
|
+
|
|
603
|
+
if on_pieces_added:
|
|
604
|
+
try:
|
|
605
|
+
await on_pieces_added(add_resp.tx_hash)
|
|
606
|
+
except Exception:
|
|
607
|
+
pass
|
|
608
|
+
|
|
609
|
+
return AsyncUploadResult(
|
|
610
|
+
piece_cid=info.piece_cid,
|
|
611
|
+
size=info.payload_size,
|
|
612
|
+
tx_hash=add_resp.tx_hash,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
async def upload_multi(
|
|
616
|
+
self,
|
|
617
|
+
data_items: List[bytes],
|
|
618
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
619
|
+
) -> List[AsyncUploadResult]:
|
|
620
|
+
"""
|
|
621
|
+
Upload multiple pieces in a batch asynchronously.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
data_items: List of byte arrays to upload
|
|
625
|
+
metadata: Optional metadata to apply to all pieces
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
List of AsyncUploadResults
|
|
629
|
+
"""
|
|
630
|
+
results = []
|
|
631
|
+
piece_infos = []
|
|
632
|
+
|
|
633
|
+
# Calculate CIDs and upload all pieces
|
|
634
|
+
for data in data_items:
|
|
635
|
+
self._validate_size(len(data))
|
|
636
|
+
info = calculate_piece_cid(data)
|
|
637
|
+
await self._pdp.upload_piece(data, info.piece_cid, info.padded_piece_size)
|
|
638
|
+
piece_infos.append(info)
|
|
639
|
+
|
|
640
|
+
# Wait for all pieces to be indexed before adding to dataset
|
|
641
|
+
for info in piece_infos:
|
|
642
|
+
await self._pdp.wait_for_piece(info.piece_cid, timeout_seconds=60, poll_interval=2)
|
|
643
|
+
|
|
644
|
+
# Batch add pieces
|
|
645
|
+
pieces = [
|
|
646
|
+
(info.piece_cid, [{"key": k, "value": v} for k, v in (metadata or {}).items()])
|
|
647
|
+
for info in piece_infos
|
|
648
|
+
]
|
|
649
|
+
extra_data = sign_add_pieces_extra_data(
|
|
650
|
+
private_key=self._private_key,
|
|
651
|
+
chain=self._chain,
|
|
652
|
+
client_data_set_id=self._client_data_set_id,
|
|
653
|
+
pieces=pieces,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
piece_cids = [info.piece_cid for info in piece_infos]
|
|
657
|
+
add_resp = await self._pdp.add_pieces(self._data_set_id, piece_cids, extra_data)
|
|
658
|
+
|
|
659
|
+
for info in piece_infos:
|
|
660
|
+
results.append(AsyncUploadResult(
|
|
661
|
+
piece_cid=info.piece_cid,
|
|
662
|
+
size=info.payload_size,
|
|
663
|
+
tx_hash=add_resp.tx_hash,
|
|
664
|
+
))
|
|
665
|
+
|
|
666
|
+
return results
|
|
667
|
+
|
|
668
|
+
async def download(self, piece_cid: str) -> bytes:
|
|
669
|
+
"""Download a piece by CID asynchronously."""
|
|
670
|
+
return await self._pdp.download_piece(piece_cid)
|
|
671
|
+
|
|
672
|
+
async def has_piece(self, piece_cid: str) -> bool:
|
|
673
|
+
"""Check if a piece exists on this provider asynchronously."""
|
|
674
|
+
try:
|
|
675
|
+
await self._pdp.find_piece(piece_cid)
|
|
676
|
+
return True
|
|
677
|
+
except Exception:
|
|
678
|
+
return False
|
|
679
|
+
|
|
680
|
+
async def wait_for_piece(self, piece_cid: str, timeout_seconds: int = 300) -> None:
|
|
681
|
+
"""Wait for a piece to be available on this provider asynchronously."""
|
|
682
|
+
await self._pdp.wait_for_piece(piece_cid, timeout_seconds)
|