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.
Files changed (64) hide show
  1. pynapse/__init__.py +6 -0
  2. pynapse/_version.py +1 -0
  3. pynapse/contracts/__init__.py +34 -0
  4. pynapse/contracts/abi_registry.py +11 -0
  5. pynapse/contracts/addresses.json +30 -0
  6. pynapse/contracts/erc20_abi.json +92 -0
  7. pynapse/contracts/errorsAbi.json +933 -0
  8. pynapse/contracts/filecoinPayV1Abi.json +2424 -0
  9. pynapse/contracts/filecoinWarmStorageServiceAbi.json +2363 -0
  10. pynapse/contracts/filecoinWarmStorageServiceStateViewAbi.json +651 -0
  11. pynapse/contracts/generated.py +35 -0
  12. pynapse/contracts/payments_abi.json +205 -0
  13. pynapse/contracts/pdpVerifierAbi.json +1266 -0
  14. pynapse/contracts/providerIdSetAbi.json +161 -0
  15. pynapse/contracts/serviceProviderRegistryAbi.json +1479 -0
  16. pynapse/contracts/sessionKeyRegistryAbi.json +147 -0
  17. pynapse/core/__init__.py +68 -0
  18. pynapse/core/abis.py +25 -0
  19. pynapse/core/chains.py +97 -0
  20. pynapse/core/constants.py +27 -0
  21. pynapse/core/errors.py +22 -0
  22. pynapse/core/piece.py +263 -0
  23. pynapse/core/rand.py +14 -0
  24. pynapse/core/typed_data.py +320 -0
  25. pynapse/core/utils.py +30 -0
  26. pynapse/evm/__init__.py +3 -0
  27. pynapse/evm/client.py +26 -0
  28. pynapse/filbeam/__init__.py +3 -0
  29. pynapse/filbeam/service.py +39 -0
  30. pynapse/payments/__init__.py +17 -0
  31. pynapse/payments/service.py +826 -0
  32. pynapse/pdp/__init__.py +21 -0
  33. pynapse/pdp/server.py +331 -0
  34. pynapse/pdp/types.py +38 -0
  35. pynapse/pdp/verifier.py +82 -0
  36. pynapse/retriever/__init__.py +12 -0
  37. pynapse/retriever/async_chain.py +227 -0
  38. pynapse/retriever/chain.py +209 -0
  39. pynapse/session/__init__.py +12 -0
  40. pynapse/session/key.py +30 -0
  41. pynapse/session/permissions.py +57 -0
  42. pynapse/session/registry.py +90 -0
  43. pynapse/sp_registry/__init__.py +11 -0
  44. pynapse/sp_registry/capabilities.py +25 -0
  45. pynapse/sp_registry/pdp_capabilities.py +102 -0
  46. pynapse/sp_registry/service.py +446 -0
  47. pynapse/sp_registry/types.py +52 -0
  48. pynapse/storage/__init__.py +57 -0
  49. pynapse/storage/async_context.py +682 -0
  50. pynapse/storage/async_manager.py +757 -0
  51. pynapse/storage/context.py +680 -0
  52. pynapse/storage/manager.py +758 -0
  53. pynapse/synapse.py +191 -0
  54. pynapse/utils/__init__.py +25 -0
  55. pynapse/utils/constants.py +25 -0
  56. pynapse/utils/errors.py +3 -0
  57. pynapse/utils/metadata.py +35 -0
  58. pynapse/utils/piece_url.py +16 -0
  59. pynapse/warm_storage/__init__.py +13 -0
  60. pynapse/warm_storage/service.py +513 -0
  61. synapse_filecoin_sdk-0.1.0.dist-info/METADATA +74 -0
  62. synapse_filecoin_sdk-0.1.0.dist-info/RECORD +64 -0
  63. synapse_filecoin_sdk-0.1.0.dist-info/WHEEL +4 -0
  64. synapse_filecoin_sdk-0.1.0.dist-info/licenses/LICENSE.md +228 -0
@@ -0,0 +1,320 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
6
+
7
+ from eth_abi import encode as abi_encode
8
+ from eth_account import Account
9
+ from eth_account.messages import encode_structured_data
10
+ from eth_utils import to_bytes
11
+
12
+ from .chains import Chain
13
+ from .rand import rand_u256
14
+
15
+ EIP712_TYPES: Dict[str, List[Dict[str, str]]] = {
16
+ "EIP712Domain": [
17
+ {"name": "name", "type": "string"},
18
+ {"name": "version", "type": "string"},
19
+ {"name": "chainId", "type": "uint256"},
20
+ {"name": "verifyingContract", "type": "address"},
21
+ ],
22
+ "MetadataEntry": [
23
+ {"name": "key", "type": "string"},
24
+ {"name": "value", "type": "string"},
25
+ ],
26
+ "CreateDataSet": [
27
+ {"name": "clientDataSetId", "type": "uint256"},
28
+ {"name": "payee", "type": "address"},
29
+ {"name": "metadata", "type": "MetadataEntry[]"},
30
+ ],
31
+ "Cid": [
32
+ {"name": "data", "type": "bytes"},
33
+ ],
34
+ "PieceMetadata": [
35
+ {"name": "pieceIndex", "type": "uint256"},
36
+ {"name": "metadata", "type": "MetadataEntry[]"},
37
+ ],
38
+ "AddPieces": [
39
+ {"name": "clientDataSetId", "type": "uint256"},
40
+ {"name": "nonce", "type": "uint256"},
41
+ {"name": "pieceData", "type": "Cid[]"},
42
+ {"name": "pieceMetadata", "type": "PieceMetadata[]"},
43
+ ],
44
+ "SchedulePieceRemovals": [
45
+ {"name": "clientDataSetId", "type": "uint256"},
46
+ {"name": "pieceIds", "type": "uint256[]"},
47
+ ],
48
+ "DeleteDataSet": [
49
+ {"name": "clientDataSetId", "type": "uint256"},
50
+ ],
51
+ "Permit": [
52
+ {"name": "owner", "type": "address"},
53
+ {"name": "spender", "type": "address"},
54
+ {"name": "value", "type": "uint256"},
55
+ {"name": "nonce", "type": "uint256"},
56
+ {"name": "deadline", "type": "uint256"},
57
+ ],
58
+ }
59
+
60
+
61
+ def get_storage_domain(chain: Chain, verifying_contract: Optional[str] = None) -> Dict[str, Any]:
62
+ return {
63
+ "name": "FilecoinWarmStorageService",
64
+ "version": "1",
65
+ "chainId": chain.id,
66
+ "verifyingContract": verifying_contract or chain.contracts.warm_storage,
67
+ }
68
+
69
+
70
+ def _sign_typed_data(private_key: str, domain: Dict[str, Any], primary_type: str, message: Dict[str, Any]) -> str:
71
+ typed = {
72
+ "types": EIP712_TYPES,
73
+ "primaryType": primary_type,
74
+ "domain": domain,
75
+ "message": message,
76
+ }
77
+ msg = encode_structured_data(typed)
78
+ signed = Account.sign_message(msg, private_key=private_key)
79
+ return signed.signature.hex()
80
+
81
+
82
+ def _piece_cid_bytes(piece_cid: str) -> bytes:
83
+ """Convert a piece CID string to its raw bytes.
84
+
85
+ Handles both PieceCIDv1 (baga...) and PieceCIDv2 (bafk...) formats
86
+ by decoding the base32lower multibase encoding directly.
87
+ """
88
+ if not (piece_cid.startswith('baga') or piece_cid.startswith('bafk')):
89
+ raise ValueError(f"Unsupported CID format: {piece_cid}")
90
+
91
+ # Remove 'b' prefix (base32lower multibase prefix)
92
+ raw = piece_cid[1:]
93
+ # base32lower uses lowercase RFC 4648 alphabet
94
+ # Python's base64.b32decode expects uppercase
95
+ raw_upper = raw.upper()
96
+ # Add padding
97
+ padding = (8 - len(raw_upper) % 8) % 8
98
+ raw_padded = raw_upper + '=' * padding
99
+ return base64.b32decode(raw_padded)
100
+
101
+
102
+ def sign_create_dataset(
103
+ private_key: str,
104
+ chain: Chain,
105
+ client_data_set_id: int,
106
+ payee: str,
107
+ metadata: Sequence[Dict[str, str]],
108
+ verifying_contract: Optional[str] = None,
109
+ ) -> str:
110
+ domain = get_storage_domain(chain, verifying_contract)
111
+ message = {
112
+ "clientDataSetId": int(client_data_set_id),
113
+ "payee": payee,
114
+ "metadata": list(metadata),
115
+ }
116
+ return _sign_typed_data(private_key, domain, "CreateDataSet", message)
117
+
118
+
119
+ def sign_schedule_piece_removals(
120
+ private_key: str,
121
+ chain: Chain,
122
+ client_data_set_id: int,
123
+ piece_ids: Sequence[int],
124
+ verifying_contract: Optional[str] = None,
125
+ ) -> str:
126
+ domain = get_storage_domain(chain, verifying_contract)
127
+ message = {
128
+ "clientDataSetId": int(client_data_set_id),
129
+ "pieceIds": [int(pid) for pid in piece_ids],
130
+ }
131
+ return _sign_typed_data(private_key, domain, "SchedulePieceRemovals", message)
132
+
133
+
134
+ def sign_add_pieces_extra_data(
135
+ private_key: str,
136
+ chain: Chain,
137
+ client_data_set_id: int,
138
+ pieces: Sequence[Tuple[str, Sequence[Dict[str, str]]]],
139
+ nonce: Optional[int] = None,
140
+ verifying_contract: Optional[str] = None,
141
+ ) -> str:
142
+ use_nonce = nonce if nonce is not None else rand_u256()
143
+
144
+ piece_data = [{"data": _piece_cid_bytes(piece_cid)} for piece_cid, _ in pieces]
145
+ piece_metadata = []
146
+ for idx, (_, metadata) in enumerate(pieces):
147
+ piece_metadata.append(
148
+ {
149
+ "pieceIndex": int(idx),
150
+ "metadata": list(metadata),
151
+ }
152
+ )
153
+
154
+ domain = get_storage_domain(chain, verifying_contract)
155
+ message = {
156
+ "clientDataSetId": int(client_data_set_id),
157
+ "nonce": int(use_nonce),
158
+ "pieceData": piece_data,
159
+ "pieceMetadata": piece_metadata,
160
+ }
161
+
162
+ signature = _sign_typed_data(private_key, domain, "AddPieces", message)
163
+
164
+ metadata_keys = [[entry["key"] for entry in metadata] for _, metadata in pieces]
165
+ metadata_values = [[entry["value"] for entry in metadata] for _, metadata in pieces]
166
+
167
+ encoded = abi_encode(
168
+ ["uint256", "string[][]", "string[][]", "bytes"],
169
+ [use_nonce, metadata_keys, metadata_values, bytes.fromhex(signature[2:])],
170
+ )
171
+ return "0x" + encoded.hex()
172
+
173
+
174
+ def sign_delete_dataset(
175
+ private_key: str,
176
+ chain: Chain,
177
+ client_data_set_id: int,
178
+ verifying_contract: Optional[str] = None,
179
+ ) -> str:
180
+ """
181
+ Sign a DeleteDataSet typed data message.
182
+
183
+ Args:
184
+ private_key: Private key to sign with
185
+ chain: Chain configuration
186
+ client_data_set_id: Client-side dataset identifier
187
+ verifying_contract: Optional override for verifying contract address
188
+
189
+ Returns:
190
+ Hex-encoded signature
191
+ """
192
+ domain = get_storage_domain(chain, verifying_contract)
193
+ message = {
194
+ "clientDataSetId": int(client_data_set_id),
195
+ }
196
+ return _sign_typed_data(private_key, domain, "DeleteDataSet", message)
197
+
198
+
199
+ def sign_delete_dataset_extra_data(
200
+ private_key: str,
201
+ chain: Chain,
202
+ client_data_set_id: int,
203
+ verifying_contract: Optional[str] = None,
204
+ ) -> str:
205
+ """
206
+ Create encoded extra data for delete dataset operation.
207
+
208
+ This is the ABI-encoded format expected by the warm storage contract.
209
+
210
+ Args:
211
+ private_key: Private key to sign with
212
+ chain: Chain configuration
213
+ client_data_set_id: Client-side dataset identifier
214
+ verifying_contract: Optional override for verifying contract address
215
+
216
+ Returns:
217
+ Hex-encoded extra data (ABI encoded signature)
218
+ """
219
+ signature = sign_delete_dataset(private_key, chain, client_data_set_id, verifying_contract)
220
+ encoded = abi_encode(["bytes"], [bytes.fromhex(signature[2:])])
221
+ return "0x" + encoded.hex()
222
+
223
+
224
+ def sign_create_dataset_extra_data(
225
+ private_key: str,
226
+ chain: Chain,
227
+ client_data_set_id: int,
228
+ payee: str,
229
+ metadata: Optional[Sequence[Dict[str, str]]] = None,
230
+ payer: Optional[str] = None,
231
+ verifying_contract: Optional[str] = None,
232
+ ) -> str:
233
+ """
234
+ Create encoded extra data for create dataset operation.
235
+
236
+ This is the ABI-encoded format expected by the warm storage contract.
237
+
238
+ Args:
239
+ private_key: Private key to sign with
240
+ chain: Chain configuration
241
+ client_data_set_id: Client-side dataset identifier
242
+ payee: Address to receive payments
243
+ metadata: Optional dataset metadata as list of {key, value} dicts
244
+ payer: Address that will pay for storage (defaults to signer's address)
245
+ verifying_contract: Optional override for verifying contract address
246
+
247
+ Returns:
248
+ Hex-encoded extra data
249
+ """
250
+ metadata = metadata or []
251
+ signature = sign_create_dataset(private_key, chain, client_data_set_id, payee, metadata, verifying_contract)
252
+
253
+ # Derive payer address from private key if not provided
254
+ if payer is None:
255
+ acct = Account.from_key(private_key)
256
+ payer = acct.address
257
+
258
+ metadata_keys = [entry["key"] for entry in metadata]
259
+ metadata_values = [entry["value"] for entry in metadata]
260
+
261
+ # ABI encoding must match TypeScript SDK:
262
+ # [payer (address), clientDataSetId (uint256), keys (string[]), values (string[]), signature (bytes)]
263
+ encoded = abi_encode(
264
+ ["address", "uint256", "string[]", "string[]", "bytes"],
265
+ [payer, client_data_set_id, metadata_keys, metadata_values, bytes.fromhex(signature[2:])],
266
+ )
267
+ return "0x" + encoded.hex()
268
+
269
+
270
+ def sign_schedule_removals_extra_data(
271
+ private_key: str,
272
+ chain: Chain,
273
+ client_data_set_id: int,
274
+ piece_ids: Sequence[int],
275
+ verifying_contract: Optional[str] = None,
276
+ ) -> str:
277
+ """
278
+ Create encoded extra data for schedule piece removals operation.
279
+
280
+ Args:
281
+ private_key: Private key to sign with
282
+ chain: Chain configuration
283
+ client_data_set_id: Client-side dataset identifier
284
+ piece_ids: List of piece IDs to schedule for removal
285
+ verifying_contract: Optional override for verifying contract address
286
+
287
+ Returns:
288
+ Hex-encoded extra data
289
+ """
290
+ signature = sign_schedule_piece_removals(private_key, chain, client_data_set_id, piece_ids, verifying_contract)
291
+ encoded = abi_encode(["bytes"], [bytes.fromhex(signature[2:])])
292
+ return "0x" + encoded.hex()
293
+
294
+
295
+ def sign_erc20_permit(
296
+ private_key: str,
297
+ name: str,
298
+ version: str,
299
+ chain_id: int,
300
+ verifying_contract: str,
301
+ owner: str,
302
+ spender: str,
303
+ value: int,
304
+ nonce: int,
305
+ deadline: int,
306
+ ) -> str:
307
+ domain = {
308
+ "name": name,
309
+ "version": version,
310
+ "chainId": chain_id,
311
+ "verifyingContract": verifying_contract,
312
+ }
313
+ message = {
314
+ "owner": owner,
315
+ "spender": spender,
316
+ "value": int(value),
317
+ "nonce": int(nonce),
318
+ "deadline": int(deadline),
319
+ }
320
+ return _sign_typed_data(private_key, domain, "Permit", message)
pynapse/core/utils.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from decimal import Decimal, ROUND_DOWN, getcontext
4
+ from typing import Union
5
+
6
+ NumberLike = Union[int, float, str, Decimal]
7
+
8
+
9
+ def parse_units(value: NumberLike, decimals: int = 18) -> int:
10
+ if decimals < 0:
11
+ raise ValueError("decimals must be >= 0")
12
+
13
+ if isinstance(value, Decimal):
14
+ dec = value
15
+ else:
16
+ dec = Decimal(str(value))
17
+
18
+ scale = Decimal(10) ** decimals
19
+ scaled = (dec * scale).quantize(Decimal(1), rounding=ROUND_DOWN)
20
+ return int(scaled)
21
+
22
+
23
+ def format_units(value: int, decimals: int = 18) -> str:
24
+ if decimals < 0:
25
+ raise ValueError("decimals must be >= 0")
26
+
27
+ getcontext().prec = max(28, decimals + 10)
28
+ scale = Decimal(10) ** decimals
29
+ dec = Decimal(value) / scale
30
+ return format(dec.normalize(), "f")
@@ -0,0 +1,3 @@
1
+ from .client import AsyncEVMClient, SyncEVMClient
2
+
3
+ __all__ = ["AsyncEVMClient", "SyncEVMClient"]
pynapse/evm/client.py ADDED
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from web3 import AsyncWeb3, Web3
7
+ from web3.providers.async_rpc import AsyncHTTPProvider
8
+ from web3.providers.rpc import HTTPProvider
9
+
10
+
11
+ @dataclass
12
+ class SyncEVMClient:
13
+ web3: Web3
14
+
15
+ @classmethod
16
+ def from_rpc_url(cls, rpc_url: str) -> "SyncEVMClient":
17
+ return cls(web3=Web3(HTTPProvider(rpc_url)))
18
+
19
+
20
+ @dataclass
21
+ class AsyncEVMClient:
22
+ web3: AsyncWeb3
23
+
24
+ @classmethod
25
+ def from_rpc_url(cls, rpc_url: str) -> "AsyncEVMClient":
26
+ return cls(web3=AsyncWeb3(AsyncHTTPProvider(rpc_url)))
@@ -0,0 +1,3 @@
1
+ from .service import DataSetStats, FilBeamService
2
+
3
+ __all__ = ["FilBeamService", "DataSetStats"]
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import httpx
6
+
7
+ from pynapse.core.chains import Chain, as_chain
8
+
9
+
10
+ @dataclass
11
+ class DataSetStats:
12
+ cdn_egress_quota: int
13
+ cache_miss_egress_quota: int
14
+
15
+
16
+ class FilBeamService:
17
+ def __init__(self, chain: Chain, client: httpx.Client | None = None) -> None:
18
+ self._chain = as_chain(chain)
19
+ self._client = client or httpx.Client(timeout=30)
20
+
21
+ def _stats_base_url(self) -> str:
22
+ if self._chain.id == 314159:
23
+ return "https://calibration.stats.filbeam.com"
24
+ return "https://stats.filbeam.com"
25
+
26
+ def get_data_set_stats(self, data_set_id: str | int) -> DataSetStats:
27
+ url = f"{self._stats_base_url()}/data-set/{data_set_id}"
28
+ resp = self._client.get(url)
29
+ if resp.status_code == 404:
30
+ raise RuntimeError(f"Data set not found: {data_set_id}")
31
+ if resp.status_code != 200:
32
+ raise RuntimeError(f"HTTP {resp.status_code}: {resp.text}")
33
+ data = resp.json()
34
+ if not isinstance(data, dict):
35
+ raise RuntimeError("Invalid response from FilBeam")
36
+ return DataSetStats(
37
+ cdn_egress_quota=int(data.get("cdnEgressQuota", "0")),
38
+ cache_miss_egress_quota=int(data.get("cacheMissEgressQuota", "0")),
39
+ )
@@ -0,0 +1,17 @@
1
+ from .service import (
2
+ AccountInfo,
3
+ AsyncPaymentsService,
4
+ RailInfo,
5
+ ServiceApproval,
6
+ SettlementResult,
7
+ SyncPaymentsService,
8
+ )
9
+
10
+ __all__ = [
11
+ "AccountInfo",
12
+ "AsyncPaymentsService",
13
+ "RailInfo",
14
+ "ServiceApproval",
15
+ "SettlementResult",
16
+ "SyncPaymentsService",
17
+ ]