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,826 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from eth_account import Account
7
+ from web3 import AsyncWeb3, Web3
8
+
9
+ from pynapse.contracts import ERC20_ABI, PAYMENTS_ABI
10
+ from pynapse.core.chains import Chain
11
+ from pynapse.utils.constants import TOKENS
12
+
13
+
14
+ @dataclass
15
+ class AccountInfo:
16
+ funds: int
17
+ lockup_current: int
18
+ lockup_rate: int
19
+ lockup_last_settled_at: int
20
+ funded_until_epoch: int
21
+ available_funds: int
22
+ current_lockup_rate: int
23
+
24
+
25
+ @dataclass
26
+ class ServiceApproval:
27
+ """Operator approval status and allowances."""
28
+ is_approved: bool
29
+ rate_allowance: int
30
+ lockup_allowance: int
31
+ max_lockup_period: int
32
+ rate_usage: int = 0
33
+ lockup_usage: int = 0
34
+
35
+
36
+ @dataclass
37
+ class RailInfo:
38
+ """Information about a payment rail."""
39
+ rail_id: int
40
+ token: str
41
+ from_address: str
42
+ to_address: str
43
+ operator: str
44
+ validator: str
45
+ payment_rate: int
46
+ lockup_period: int
47
+ lockup_fixed: int
48
+ settled_up_to: int
49
+ end_epoch: int
50
+ commission_rate_bps: int
51
+ service_fee_recipient: str
52
+
53
+
54
+ @dataclass
55
+ class SettlementResult:
56
+ """Result of a settlement operation."""
57
+ total_settled_amount: int
58
+ total_net_payee_amount: int
59
+ total_operator_commission: int
60
+ total_network_fee: int
61
+ final_settled_epoch: int
62
+ note: int
63
+
64
+
65
+ class SyncPaymentsService:
66
+ def __init__(self, web3: Web3, chain: Chain, account_address: str, private_key: Optional[str] = None) -> None:
67
+ self._web3 = web3
68
+ self._chain = chain
69
+ self._account = account_address
70
+ self._private_key = private_key
71
+ self._payments = self._web3.eth.contract(address=chain.contracts.payments, abi=PAYMENTS_ABI)
72
+ self._erc20 = self._web3.eth.contract(address=chain.contracts.usdfc, abi=ERC20_ABI)
73
+
74
+ def balance(self, token: str = TOKENS["USDFC"]) -> int:
75
+ if token != TOKENS["USDFC"]:
76
+ raise ValueError("Only USDFC is supported for payments contract balance")
77
+ funds, _, _, _ = self._payments.functions.accounts(self._chain.contracts.usdfc, self._account).call()
78
+ return int(funds)
79
+
80
+ def account_info(self, token: str = TOKENS["USDFC"]) -> AccountInfo:
81
+ if token != TOKENS["USDFC"]:
82
+ raise ValueError("Only USDFC is supported for payments contract account info")
83
+ funds, lockup_current, lockup_rate, lockup_last = self._payments.functions.accounts(
84
+ self._chain.contracts.usdfc, self._account
85
+ ).call()
86
+ funded_until, _, available, current_lockup_rate = self._payments.functions.getAccountInfoIfSettled(
87
+ self._chain.contracts.usdfc, self._account
88
+ ).call()
89
+ return AccountInfo(
90
+ funds=int(funds),
91
+ lockup_current=int(lockup_current),
92
+ lockup_rate=int(lockup_rate),
93
+ lockup_last_settled_at=int(lockup_last),
94
+ funded_until_epoch=int(funded_until),
95
+ available_funds=int(available),
96
+ current_lockup_rate=int(current_lockup_rate),
97
+ )
98
+
99
+ def wallet_balance(self, token: Optional[str] = None) -> int:
100
+ if token is None or token == TOKENS["FIL"]:
101
+ return int(self._web3.eth.get_balance(self._account))
102
+ if token == TOKENS["USDFC"]:
103
+ return int(self._erc20.functions.balanceOf(self._account).call())
104
+ raise ValueError(f"Unsupported token {token}")
105
+
106
+ def allowance(self, spender: str, token: str = TOKENS["USDFC"]) -> int:
107
+ if token != TOKENS["USDFC"]:
108
+ raise ValueError("Only USDFC is supported for allowance")
109
+ return int(self._erc20.functions.allowance(self._account, spender).call())
110
+
111
+ def approve(self, spender: str, amount: int, token: str = TOKENS["USDFC"]) -> str:
112
+ if token != TOKENS["USDFC"]:
113
+ raise ValueError("Only USDFC is supported for approve")
114
+ if not self._private_key:
115
+ raise ValueError("private_key required for approve")
116
+ txn = self._erc20.functions.approve(spender, amount).build_transaction(
117
+ {
118
+ "from": self._account,
119
+ "nonce": self._web3.eth.get_transaction_count(self._account),
120
+ }
121
+ )
122
+ signed = self._web3.eth.account.sign_transaction(txn, private_key=self._private_key)
123
+ tx_hash = self._web3.eth.send_raw_transaction(signed.rawTransaction)
124
+ return tx_hash.hex()
125
+
126
+ def deposit(self, amount: int, to: Optional[str] = None, token: str = TOKENS["USDFC"]) -> str:
127
+ if token != TOKENS["USDFC"]:
128
+ raise ValueError("Only USDFC is supported for deposit")
129
+ if not self._private_key:
130
+ raise ValueError("private_key required for deposit")
131
+ to_addr = to or self._account
132
+ txn = self._payments.functions.deposit(self._chain.contracts.usdfc, to_addr, amount).build_transaction(
133
+ {
134
+ "from": self._account,
135
+ "nonce": self._web3.eth.get_transaction_count(self._account),
136
+ }
137
+ )
138
+ signed = self._web3.eth.account.sign_transaction(txn, private_key=self._private_key)
139
+ tx_hash = self._web3.eth.send_raw_transaction(signed.rawTransaction)
140
+ return tx_hash.hex()
141
+
142
+ def withdraw(self, amount: int, token: str = TOKENS["USDFC"]) -> str:
143
+ if token != TOKENS["USDFC"]:
144
+ raise ValueError("Only USDFC is supported for withdraw")
145
+ if not self._private_key:
146
+ raise ValueError("private_key required for withdraw")
147
+ txn = self._payments.functions.withdraw(self._chain.contracts.usdfc, amount).build_transaction(
148
+ {
149
+ "from": self._account,
150
+ "nonce": self._web3.eth.get_transaction_count(self._account),
151
+ }
152
+ )
153
+ signed = self._web3.eth.account.sign_transaction(txn, private_key=self._private_key)
154
+ tx_hash = self._web3.eth.send_raw_transaction(signed.rawTransaction)
155
+ return tx_hash.hex()
156
+
157
+ def service_approval(self, service: str, token: str = TOKENS["USDFC"]) -> ServiceApproval:
158
+ """
159
+ Get the operator approval status and allowances for a service.
160
+
161
+ Args:
162
+ service: The service contract address to check
163
+ token: The token to check approval for (defaults to USDFC)
164
+
165
+ Returns:
166
+ ServiceApproval with approval status and allowances
167
+ """
168
+ if token != TOKENS["USDFC"]:
169
+ raise ValueError("Only USDFC is supported for service_approval")
170
+
171
+ result = self._payments.functions.operatorApprovals(
172
+ self._chain.contracts.usdfc, self._account, service
173
+ ).call()
174
+
175
+ # Result format: (isApproved, rateAllowance, lockupAllowance, rateUsage, lockupUsage, maxLockupPeriod)
176
+ return ServiceApproval(
177
+ is_approved=bool(result[0]),
178
+ rate_allowance=int(result[1]),
179
+ lockup_allowance=int(result[2]),
180
+ max_lockup_period=int(result[5]),
181
+ rate_usage=int(result[3]),
182
+ lockup_usage=int(result[4]),
183
+ )
184
+
185
+ def approve_service(
186
+ self,
187
+ service: str,
188
+ rate_allowance: int,
189
+ lockup_allowance: int,
190
+ max_lockup_period: int,
191
+ token: str = TOKENS["USDFC"],
192
+ ) -> str:
193
+ """
194
+ Approve a service contract to act as an operator for payment rails.
195
+
196
+ Args:
197
+ service: The service contract address to approve
198
+ rate_allowance: Maximum payment rate per epoch the operator can set
199
+ lockup_allowance: Maximum lockup amount the operator can set
200
+ max_lockup_period: Maximum lockup period in epochs the operator can set
201
+ token: The token to approve for (defaults to USDFC)
202
+
203
+ Returns:
204
+ Transaction hash
205
+ """
206
+ if token != TOKENS["USDFC"]:
207
+ raise ValueError("Only USDFC is supported for approve_service")
208
+ if not self._private_key:
209
+ raise ValueError("private_key required for approve_service")
210
+
211
+ txn = self._payments.functions.setOperatorApproval(
212
+ self._chain.contracts.usdfc,
213
+ service,
214
+ True, # approve
215
+ rate_allowance,
216
+ lockup_allowance,
217
+ max_lockup_period,
218
+ ).build_transaction(
219
+ {
220
+ "from": self._account,
221
+ "nonce": self._web3.eth.get_transaction_count(self._account),
222
+ }
223
+ )
224
+ signed = self._web3.eth.account.sign_transaction(txn, private_key=self._private_key)
225
+ tx_hash = self._web3.eth.send_raw_transaction(signed.rawTransaction)
226
+ return tx_hash.hex()
227
+
228
+ def revoke_service(self, service: str, token: str = TOKENS["USDFC"]) -> str:
229
+ """
230
+ Revoke a service contract's operator approval.
231
+
232
+ Args:
233
+ service: The service contract address to revoke
234
+ token: The token to revoke approval for (defaults to USDFC)
235
+
236
+ Returns:
237
+ Transaction hash
238
+ """
239
+ if token != TOKENS["USDFC"]:
240
+ raise ValueError("Only USDFC is supported for revoke_service")
241
+ if not self._private_key:
242
+ raise ValueError("private_key required for revoke_service")
243
+
244
+ txn = self._payments.functions.setOperatorApproval(
245
+ self._chain.contracts.usdfc,
246
+ service,
247
+ False, # revoke
248
+ 0, # rate_allowance (ignored for revoke)
249
+ 0, # lockup_allowance (ignored for revoke)
250
+ 0, # max_lockup_period (ignored for revoke)
251
+ ).build_transaction(
252
+ {
253
+ "from": self._account,
254
+ "nonce": self._web3.eth.get_transaction_count(self._account),
255
+ }
256
+ )
257
+ signed = self._web3.eth.account.sign_transaction(txn, private_key=self._private_key)
258
+ tx_hash = self._web3.eth.send_raw_transaction(signed.rawTransaction)
259
+ return tx_hash.hex()
260
+
261
+ def get_rail(self, rail_id: int) -> RailInfo:
262
+ """
263
+ Get detailed information about a specific rail.
264
+
265
+ Args:
266
+ rail_id: The rail ID to query
267
+
268
+ Returns:
269
+ Rail information including all parameters and current state
270
+ """
271
+ result = self._payments.functions.getRail(rail_id).call()
272
+ return RailInfo(
273
+ rail_id=rail_id,
274
+ token=result[0],
275
+ from_address=result[1],
276
+ to_address=result[2],
277
+ operator=result[3],
278
+ validator=result[4],
279
+ payment_rate=int(result[5]),
280
+ lockup_period=int(result[6]),
281
+ lockup_fixed=int(result[7]),
282
+ settled_up_to=int(result[8]),
283
+ end_epoch=int(result[9]),
284
+ commission_rate_bps=int(result[10]),
285
+ service_fee_recipient=result[11],
286
+ )
287
+
288
+ def settle(self, rail_id: int, until_epoch: Optional[int] = None, token: str = TOKENS["USDFC"]) -> str:
289
+ """
290
+ Settle a payment rail up to a specific epoch.
291
+
292
+ Args:
293
+ rail_id: The rail ID to settle
294
+ until_epoch: The epoch to settle up to (defaults to current block number)
295
+ token: The token to settle (defaults to USDFC)
296
+
297
+ Returns:
298
+ Transaction hash
299
+ """
300
+ if token != TOKENS["USDFC"]:
301
+ raise ValueError("Only USDFC is supported for settle")
302
+ if not self._private_key:
303
+ raise ValueError("private_key required for settle")
304
+
305
+ _until_epoch = until_epoch if until_epoch is not None else self._web3.eth.block_number
306
+
307
+ txn = self._payments.functions.settleRail(
308
+ self._chain.contracts.usdfc, rail_id, _until_epoch
309
+ ).build_transaction(
310
+ {
311
+ "from": self._account,
312
+ "nonce": self._web3.eth.get_transaction_count(self._account),
313
+ }
314
+ )
315
+ signed = self._web3.eth.account.sign_transaction(txn, private_key=self._private_key)
316
+ tx_hash = self._web3.eth.send_raw_transaction(signed.rawTransaction)
317
+ return tx_hash.hex()
318
+
319
+ def settle_terminated_rail(self, rail_id: int, token: str = TOKENS["USDFC"]) -> str:
320
+ """
321
+ Emergency settlement for terminated rails only.
322
+
323
+ Bypasses service contract validation. Can only be called by the client
324
+ after the max settlement epoch has passed.
325
+
326
+ Args:
327
+ rail_id: The rail ID to settle
328
+ token: The token to settle (defaults to USDFC)
329
+
330
+ Returns:
331
+ Transaction hash
332
+ """
333
+ if token != TOKENS["USDFC"]:
334
+ raise ValueError("Only USDFC is supported for settle_terminated_rail")
335
+ if not self._private_key:
336
+ raise ValueError("private_key required for settle_terminated_rail")
337
+
338
+ txn = self._payments.functions.settleTerminatedRailWithoutValidation(
339
+ self._chain.contracts.usdfc, rail_id
340
+ ).build_transaction(
341
+ {
342
+ "from": self._account,
343
+ "nonce": self._web3.eth.get_transaction_count(self._account),
344
+ }
345
+ )
346
+ signed = self._web3.eth.account.sign_transaction(txn, private_key=self._private_key)
347
+ tx_hash = self._web3.eth.send_raw_transaction(signed.rawTransaction)
348
+ return tx_hash.hex()
349
+
350
+ def settle_auto(self, rail_id: int, until_epoch: Optional[int] = None, token: str = TOKENS["USDFC"]) -> str:
351
+ """
352
+ Automatically settle a rail, detecting whether it's terminated or active.
353
+
354
+ For terminated rails: calls settle_terminated_rail()
355
+ For active rails: calls settle() with optional until_epoch
356
+
357
+ Args:
358
+ rail_id: The rail ID to settle
359
+ until_epoch: The epoch to settle up to (ignored for terminated rails)
360
+ token: The token to settle (defaults to USDFC)
361
+
362
+ Returns:
363
+ Transaction hash
364
+ """
365
+ rail = self.get_rail(rail_id)
366
+
367
+ if rail.end_epoch > 0:
368
+ # Rail is terminated
369
+ return self.settle_terminated_rail(rail_id, token)
370
+ else:
371
+ # Rail is active
372
+ return self.settle(rail_id, until_epoch, token)
373
+
374
+ def get_rails_as_payer(self, token: str = TOKENS["USDFC"]) -> list:
375
+ """
376
+ Get all rails where the wallet is the payer.
377
+
378
+ Args:
379
+ token: The token to filter by (defaults to USDFC)
380
+
381
+ Returns:
382
+ List of RailInfo objects
383
+ """
384
+ if token != TOKENS["USDFC"]:
385
+ raise ValueError("Only USDFC is supported for get_rails_as_payer")
386
+
387
+ results, has_more = self._payments.functions.getRailsForPayerAndToken(
388
+ self._chain.contracts.usdfc, self._account, 0, 100 # offset, limit
389
+ ).call()
390
+
391
+ rails = []
392
+ for r in results:
393
+ rails.append(RailInfo(
394
+ rail_id=int(r[0]),
395
+ token=r[1],
396
+ from_address=r[2],
397
+ to_address=r[3],
398
+ operator=r[4],
399
+ validator=r[5],
400
+ payment_rate=int(r[6]),
401
+ lockup_period=int(r[7]),
402
+ lockup_fixed=int(r[8]),
403
+ settled_up_to=int(r[9]),
404
+ end_epoch=int(r[10]),
405
+ commission_rate_bps=int(r[11]),
406
+ service_fee_recipient=r[12],
407
+ ))
408
+ return rails
409
+
410
+ def get_rails_as_payee(self, token: str = TOKENS["USDFC"]) -> list:
411
+ """
412
+ Get all rails where the wallet is the payee.
413
+
414
+ Args:
415
+ token: The token to filter by (defaults to USDFC)
416
+
417
+ Returns:
418
+ List of RailInfo objects
419
+ """
420
+ if token != TOKENS["USDFC"]:
421
+ raise ValueError("Only USDFC is supported for get_rails_as_payee")
422
+
423
+ results, has_more = self._payments.functions.getRailsForPayeeAndToken(
424
+ self._chain.contracts.usdfc, self._account, 0, 100 # offset, limit
425
+ ).call()
426
+
427
+ rails = []
428
+ for r in results:
429
+ rails.append(RailInfo(
430
+ rail_id=int(r[0]),
431
+ token=r[1],
432
+ from_address=r[2],
433
+ to_address=r[3],
434
+ operator=r[4],
435
+ validator=r[5],
436
+ payment_rate=int(r[6]),
437
+ lockup_period=int(r[7]),
438
+ lockup_fixed=int(r[8]),
439
+ settled_up_to=int(r[9]),
440
+ end_epoch=int(r[10]),
441
+ commission_rate_bps=int(r[11]),
442
+ service_fee_recipient=r[12],
443
+ ))
444
+ return rails
445
+
446
+
447
+ class AsyncPaymentsService:
448
+ def __init__(self, web3: AsyncWeb3, chain: Chain, account_address: str, private_key: Optional[str] = None) -> None:
449
+ self._web3 = web3
450
+ self._chain = chain
451
+ self._account = account_address
452
+ self._private_key = private_key
453
+ self._payments = self._web3.eth.contract(address=chain.contracts.payments, abi=PAYMENTS_ABI)
454
+ self._erc20 = self._web3.eth.contract(address=chain.contracts.usdfc, abi=ERC20_ABI)
455
+
456
+ async def balance(self, token: str = TOKENS["USDFC"]) -> int:
457
+ if token != TOKENS["USDFC"]:
458
+ raise ValueError("Only USDFC is supported for payments contract balance")
459
+ funds, _, _, _ = await self._payments.functions.accounts(self._chain.contracts.usdfc, self._account).call()
460
+ return int(funds)
461
+
462
+ async def account_info(self, token: str = TOKENS["USDFC"]) -> AccountInfo:
463
+ if token != TOKENS["USDFC"]:
464
+ raise ValueError("Only USDFC is supported for payments contract account info")
465
+ funds, lockup_current, lockup_rate, lockup_last = await self._payments.functions.accounts(
466
+ self._chain.contracts.usdfc, self._account
467
+ ).call()
468
+ funded_until, _, available, current_lockup_rate = await self._payments.functions.getAccountInfoIfSettled(
469
+ self._chain.contracts.usdfc, self._account
470
+ ).call()
471
+ return AccountInfo(
472
+ funds=int(funds),
473
+ lockup_current=int(lockup_current),
474
+ lockup_rate=int(lockup_rate),
475
+ lockup_last_settled_at=int(lockup_last),
476
+ funded_until_epoch=int(funded_until),
477
+ available_funds=int(available),
478
+ current_lockup_rate=int(current_lockup_rate),
479
+ )
480
+
481
+ async def wallet_balance(self, token: Optional[str] = None) -> int:
482
+ if token is None or token == TOKENS["FIL"]:
483
+ return int(await self._web3.eth.get_balance(self._account))
484
+ if token == TOKENS["USDFC"]:
485
+ return int(await self._erc20.functions.balanceOf(self._account).call())
486
+ raise ValueError(f"Unsupported token {token}")
487
+
488
+ async def allowance(self, spender: str, token: str = TOKENS["USDFC"]) -> int:
489
+ if token != TOKENS["USDFC"]:
490
+ raise ValueError("Only USDFC is supported for allowance")
491
+ return int(await self._erc20.functions.allowance(self._account, spender).call())
492
+
493
+ async def approve(self, spender: str, amount: int, token: str = TOKENS["USDFC"]) -> str:
494
+ if token != TOKENS["USDFC"]:
495
+ raise ValueError("Only USDFC is supported for approve")
496
+ if not self._private_key:
497
+ raise ValueError("private_key required for approve")
498
+ txn = await self._erc20.functions.approve(spender, amount).build_transaction(
499
+ {
500
+ "from": self._account,
501
+ "nonce": await self._web3.eth.get_transaction_count(self._account),
502
+ }
503
+ )
504
+ signed = Account.sign_transaction(txn, private_key=self._private_key)
505
+ tx_hash = await self._web3.eth.send_raw_transaction(signed.rawTransaction)
506
+ return tx_hash.hex()
507
+
508
+ async def deposit(self, amount: int, to: Optional[str] = None, token: str = TOKENS["USDFC"]) -> str:
509
+ if token != TOKENS["USDFC"]:
510
+ raise ValueError("Only USDFC is supported for deposit")
511
+ if not self._private_key:
512
+ raise ValueError("private_key required for deposit")
513
+ to_addr = to or self._account
514
+ txn = await self._payments.functions.deposit(self._chain.contracts.usdfc, to_addr, amount).build_transaction(
515
+ {
516
+ "from": self._account,
517
+ "nonce": await self._web3.eth.get_transaction_count(self._account),
518
+ }
519
+ )
520
+ signed = Account.sign_transaction(txn, private_key=self._private_key)
521
+ tx_hash = await self._web3.eth.send_raw_transaction(signed.rawTransaction)
522
+ return tx_hash.hex()
523
+
524
+ async def withdraw(self, amount: int, token: str = TOKENS["USDFC"]) -> str:
525
+ if token != TOKENS["USDFC"]:
526
+ raise ValueError("Only USDFC is supported for withdraw")
527
+ if not self._private_key:
528
+ raise ValueError("private_key required for withdraw")
529
+ txn = await self._payments.functions.withdraw(self._chain.contracts.usdfc, amount).build_transaction(
530
+ {
531
+ "from": self._account,
532
+ "nonce": await self._web3.eth.get_transaction_count(self._account),
533
+ }
534
+ )
535
+ signed = Account.sign_transaction(txn, private_key=self._private_key)
536
+ tx_hash = await self._web3.eth.send_raw_transaction(signed.rawTransaction)
537
+ return tx_hash.hex()
538
+
539
+ async def service_approval(self, service: str, token: str = TOKENS["USDFC"]) -> ServiceApproval:
540
+ """
541
+ Get the operator approval status and allowances for a service.
542
+
543
+ Args:
544
+ service: The service contract address to check
545
+ token: The token to check approval for (defaults to USDFC)
546
+
547
+ Returns:
548
+ ServiceApproval with approval status and allowances
549
+ """
550
+ if token != TOKENS["USDFC"]:
551
+ raise ValueError("Only USDFC is supported for service_approval")
552
+
553
+ result = await self._payments.functions.operatorApprovals(
554
+ self._chain.contracts.usdfc, self._account, service
555
+ ).call()
556
+
557
+ # Result format: (isApproved, rateAllowance, lockupAllowance, rateUsage, lockupUsage, maxLockupPeriod)
558
+ return ServiceApproval(
559
+ is_approved=bool(result[0]),
560
+ rate_allowance=int(result[1]),
561
+ lockup_allowance=int(result[2]),
562
+ max_lockup_period=int(result[5]),
563
+ rate_usage=int(result[3]),
564
+ lockup_usage=int(result[4]),
565
+ )
566
+
567
+ async def approve_service(
568
+ self,
569
+ service: str,
570
+ rate_allowance: int,
571
+ lockup_allowance: int,
572
+ max_lockup_period: int,
573
+ token: str = TOKENS["USDFC"],
574
+ ) -> str:
575
+ """
576
+ Approve a service contract to act as an operator for payment rails.
577
+
578
+ Args:
579
+ service: The service contract address to approve
580
+ rate_allowance: Maximum payment rate per epoch the operator can set
581
+ lockup_allowance: Maximum lockup amount the operator can set
582
+ max_lockup_period: Maximum lockup period in epochs the operator can set
583
+ token: The token to approve for (defaults to USDFC)
584
+
585
+ Returns:
586
+ Transaction hash
587
+ """
588
+ if token != TOKENS["USDFC"]:
589
+ raise ValueError("Only USDFC is supported for approve_service")
590
+ if not self._private_key:
591
+ raise ValueError("private_key required for approve_service")
592
+
593
+ txn = await self._payments.functions.setOperatorApproval(
594
+ self._chain.contracts.usdfc,
595
+ service,
596
+ True,
597
+ rate_allowance,
598
+ lockup_allowance,
599
+ max_lockup_period,
600
+ ).build_transaction(
601
+ {
602
+ "from": self._account,
603
+ "nonce": await self._web3.eth.get_transaction_count(self._account),
604
+ }
605
+ )
606
+ signed = Account.sign_transaction(txn, private_key=self._private_key)
607
+ tx_hash = await self._web3.eth.send_raw_transaction(signed.rawTransaction)
608
+ return tx_hash.hex()
609
+
610
+ async def revoke_service(self, service: str, token: str = TOKENS["USDFC"]) -> str:
611
+ """
612
+ Revoke a service contract's operator approval.
613
+
614
+ Args:
615
+ service: The service contract address to revoke
616
+ token: The token to revoke approval for (defaults to USDFC)
617
+
618
+ Returns:
619
+ Transaction hash
620
+ """
621
+ if token != TOKENS["USDFC"]:
622
+ raise ValueError("Only USDFC is supported for revoke_service")
623
+ if not self._private_key:
624
+ raise ValueError("private_key required for revoke_service")
625
+
626
+ txn = await self._payments.functions.setOperatorApproval(
627
+ self._chain.contracts.usdfc,
628
+ service,
629
+ False,
630
+ 0,
631
+ 0,
632
+ 0,
633
+ ).build_transaction(
634
+ {
635
+ "from": self._account,
636
+ "nonce": await self._web3.eth.get_transaction_count(self._account),
637
+ }
638
+ )
639
+ signed = Account.sign_transaction(txn, private_key=self._private_key)
640
+ tx_hash = await self._web3.eth.send_raw_transaction(signed.rawTransaction)
641
+ return tx_hash.hex()
642
+
643
+ async def get_rail(self, rail_id: int) -> RailInfo:
644
+ """
645
+ Get detailed information about a specific rail.
646
+
647
+ Args:
648
+ rail_id: The rail ID to query
649
+
650
+ Returns:
651
+ Rail information including all parameters and current state
652
+ """
653
+ result = await self._payments.functions.getRail(rail_id).call()
654
+ return RailInfo(
655
+ rail_id=rail_id,
656
+ token=result[0],
657
+ from_address=result[1],
658
+ to_address=result[2],
659
+ operator=result[3],
660
+ validator=result[4],
661
+ payment_rate=int(result[5]),
662
+ lockup_period=int(result[6]),
663
+ lockup_fixed=int(result[7]),
664
+ settled_up_to=int(result[8]),
665
+ end_epoch=int(result[9]),
666
+ commission_rate_bps=int(result[10]),
667
+ service_fee_recipient=result[11],
668
+ )
669
+
670
+ async def settle(self, rail_id: int, until_epoch: Optional[int] = None, token: str = TOKENS["USDFC"]) -> str:
671
+ """
672
+ Settle a payment rail up to a specific epoch.
673
+
674
+ Args:
675
+ rail_id: The rail ID to settle
676
+ until_epoch: The epoch to settle up to (defaults to current block number)
677
+ token: The token to settle (defaults to USDFC)
678
+
679
+ Returns:
680
+ Transaction hash
681
+ """
682
+ if token != TOKENS["USDFC"]:
683
+ raise ValueError("Only USDFC is supported for settle")
684
+ if not self._private_key:
685
+ raise ValueError("private_key required for settle")
686
+
687
+ _until_epoch = until_epoch if until_epoch is not None else await self._web3.eth.block_number
688
+
689
+ txn = await self._payments.functions.settleRail(
690
+ self._chain.contracts.usdfc, rail_id, _until_epoch
691
+ ).build_transaction(
692
+ {
693
+ "from": self._account,
694
+ "nonce": await self._web3.eth.get_transaction_count(self._account),
695
+ }
696
+ )
697
+ signed = Account.sign_transaction(txn, private_key=self._private_key)
698
+ tx_hash = await self._web3.eth.send_raw_transaction(signed.rawTransaction)
699
+ return tx_hash.hex()
700
+
701
+ async def settle_terminated_rail(self, rail_id: int, token: str = TOKENS["USDFC"]) -> str:
702
+ """
703
+ Emergency settlement for terminated rails only.
704
+
705
+ Bypasses service contract validation. Can only be called by the client
706
+ after the max settlement epoch has passed.
707
+
708
+ Args:
709
+ rail_id: The rail ID to settle
710
+ token: The token to settle (defaults to USDFC)
711
+
712
+ Returns:
713
+ Transaction hash
714
+ """
715
+ if token != TOKENS["USDFC"]:
716
+ raise ValueError("Only USDFC is supported for settle_terminated_rail")
717
+ if not self._private_key:
718
+ raise ValueError("private_key required for settle_terminated_rail")
719
+
720
+ txn = await self._payments.functions.settleTerminatedRailWithoutValidation(
721
+ self._chain.contracts.usdfc, rail_id
722
+ ).build_transaction(
723
+ {
724
+ "from": self._account,
725
+ "nonce": await self._web3.eth.get_transaction_count(self._account),
726
+ }
727
+ )
728
+ signed = Account.sign_transaction(txn, private_key=self._private_key)
729
+ tx_hash = await self._web3.eth.send_raw_transaction(signed.rawTransaction)
730
+ return tx_hash.hex()
731
+
732
+ async def settle_auto(self, rail_id: int, until_epoch: Optional[int] = None, token: str = TOKENS["USDFC"]) -> str:
733
+ """
734
+ Automatically settle a rail, detecting whether it's terminated or active.
735
+
736
+ For terminated rails: calls settle_terminated_rail()
737
+ For active rails: calls settle() with optional until_epoch
738
+
739
+ Args:
740
+ rail_id: The rail ID to settle
741
+ until_epoch: The epoch to settle up to (ignored for terminated rails)
742
+ token: The token to settle (defaults to USDFC)
743
+
744
+ Returns:
745
+ Transaction hash
746
+ """
747
+ rail = await self.get_rail(rail_id)
748
+
749
+ if rail.end_epoch > 0:
750
+ # Rail is terminated
751
+ return await self.settle_terminated_rail(rail_id, token)
752
+ else:
753
+ # Rail is active
754
+ return await self.settle(rail_id, until_epoch, token)
755
+
756
+ async def get_rails_as_payer(self, token: str = TOKENS["USDFC"]) -> list:
757
+ """
758
+ Get all rails where the wallet is the payer.
759
+
760
+ Args:
761
+ token: The token to filter by (defaults to USDFC)
762
+
763
+ Returns:
764
+ List of RailInfo objects
765
+ """
766
+ if token != TOKENS["USDFC"]:
767
+ raise ValueError("Only USDFC is supported for get_rails_as_payer")
768
+
769
+ results, has_more = await self._payments.functions.getRailsForPayerAndToken(
770
+ self._chain.contracts.usdfc, self._account, 0, 100 # offset, limit
771
+ ).call()
772
+
773
+ rails = []
774
+ for r in results:
775
+ rails.append(RailInfo(
776
+ rail_id=int(r[0]),
777
+ token=r[1],
778
+ from_address=r[2],
779
+ to_address=r[3],
780
+ operator=r[4],
781
+ validator=r[5],
782
+ payment_rate=int(r[6]),
783
+ lockup_period=int(r[7]),
784
+ lockup_fixed=int(r[8]),
785
+ settled_up_to=int(r[9]),
786
+ end_epoch=int(r[10]),
787
+ commission_rate_bps=int(r[11]),
788
+ service_fee_recipient=r[12],
789
+ ))
790
+ return rails
791
+
792
+ async def get_rails_as_payee(self, token: str = TOKENS["USDFC"]) -> list:
793
+ """
794
+ Get all rails where the wallet is the payee.
795
+
796
+ Args:
797
+ token: The token to filter by (defaults to USDFC)
798
+
799
+ Returns:
800
+ List of RailInfo objects
801
+ """
802
+ if token != TOKENS["USDFC"]:
803
+ raise ValueError("Only USDFC is supported for get_rails_as_payee")
804
+
805
+ results, has_more = await self._payments.functions.getRailsForPayeeAndToken(
806
+ self._chain.contracts.usdfc, self._account, 0, 100 # offset, limit
807
+ ).call()
808
+
809
+ rails = []
810
+ for r in results:
811
+ rails.append(RailInfo(
812
+ rail_id=int(r[0]),
813
+ token=r[1],
814
+ from_address=r[2],
815
+ to_address=r[3],
816
+ operator=r[4],
817
+ validator=r[5],
818
+ payment_rate=int(r[6]),
819
+ lockup_period=int(r[7]),
820
+ lockup_fixed=int(r[8]),
821
+ settled_up_to=int(r[9]),
822
+ end_epoch=int(r[10]),
823
+ commission_rate_bps=int(r[11]),
824
+ service_fee_recipient=r[12],
825
+ ))
826
+ return rails