naeural-client 3.0.9__py3-none-any.whl → 3.1.1__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.
naeural_client/bc/evm.py CHANGED
@@ -1,5 +1,10 @@
1
+ import json
1
2
  import os
2
3
 
4
+ from collections import namedtuple
5
+
6
+ from datetime import timezone, datetime
7
+
3
8
  from eth_account import Account
4
9
  from eth_utils import keccak, to_checksum_address
5
10
  from eth_account.messages import encode_defunct
@@ -10,346 +15,835 @@ EE_VPN_IMPL = str(os.environ.get(EE_VPN_IMPL_ENV_KEY, False)).lower() in [
10
15
  'true', '1', 'yes', 'y', 't', 'on'
11
16
  ]
12
17
 
13
- if EE_VPN_IMPL:
18
+ Web3Vars = namedtuple("Web3Vars", [
19
+ "w3", "rpc_url", "nd_contract_address", "r1_contract_address", "network",
20
+ "genesis_date", "epoch_length_seconds"
21
+ ])
22
+
23
+
24
+ if not EE_VPN_IMPL:
25
+ from web3 import Web3
26
+ else:
14
27
  class Web3:
15
28
  """
16
29
  VPS enabled. Web3 is not available.
17
30
  """
18
- else:
19
- from web3 import Web3
20
-
21
31
 
32
+ # A minimal ERC20 ABI for balanceOf, transfer, and decimals functions.
33
+ ERC20_ABI = [
34
+ {
35
+ "constant": True,
36
+ "inputs": [{"name": "_owner", "type": "address"}],
37
+ "name": "balanceOf",
38
+ "outputs": [{"name": "balance", "type": "uint256"}],
39
+ "payable": False,
40
+ "stateMutability": "view",
41
+ "type": "function"
42
+ },
43
+ {
44
+ "constant": False,
45
+ "inputs": [
46
+ {"name": "_to", "type": "address"},
47
+ {"name": "_value", "type": "uint256"}
48
+ ],
49
+ "name": "transfer",
50
+ "outputs": [{"name": "success", "type": "bool"}],
51
+ "payable": False,
52
+ "stateMutability": "nonpayable",
53
+ "type": "function"
54
+ },
55
+ {
56
+ "constant": True,
57
+ "inputs": [],
58
+ "name": "decimals",
59
+ "outputs": [{"name": "", "type": "uint8"}],
60
+ "payable": False,
61
+ "stateMutability": "view",
62
+ "type": "function"
63
+ }
64
+ ]
22
65
 
23
66
  class _EVMMixin:
24
67
 
25
-
26
- @staticmethod
27
- def is_valid_evm_address(address: str) -> bool:
28
- """
29
- Check if the input string is a valid Ethereum (EVM) address using basic heuristics.
30
-
31
- Parameters
32
- ----------
33
- address : str
34
- The address string to verify.
35
-
36
- Returns
37
- -------
38
- bool
39
- True if `address` meets the basic criteria for an EVM address, False otherwise.
40
- """
41
- # Basic checks:
42
- # A) Must start with '0x'
43
- # B) Must be exactly 42 characters in total
44
- # C) All remaining characters must be valid hexadecimal digits
45
- if not address.startswith("0x"):
46
- return False
47
- if len(address) != 42:
48
- return False
68
+ # EVM address methods
69
+ if True:
70
+ @staticmethod
71
+ def is_valid_evm_address(address: str) -> bool:
72
+ """
73
+ Check if the input string is a valid Ethereum (EVM) address using basic heuristics.
74
+
75
+ Parameters
76
+ ----------
77
+ address : str
78
+ The address string to verify.
79
+
80
+ Returns
81
+ -------
82
+ bool
83
+ True if `address` meets the basic criteria for an EVM address, False otherwise.
84
+ """
85
+ # Basic checks:
86
+ # A) Must start with '0x'
87
+ # B) Must be exactly 42 characters in total
88
+ # C) All remaining characters must be valid hexadecimal digits
89
+ if not address.startswith("0x"):
90
+ return False
91
+ if len(address) != 42:
92
+ return False
93
+
94
+ hex_part = address[2:]
95
+ # Ensure all characters in the hex part are valid hex digits
96
+ return all(c in "0123456789abcdefABCDEF" for c in hex_part)
49
97
 
50
- hex_part = address[2:]
51
- # Ensure all characters in the hex part are valid hex digits
52
- return all(c in "0123456789abcdefABCDEF" for c in hex_part)
53
-
54
- @staticmethod
55
- def is_valid_eth_address(address: str) -> bool:
56
- """
57
- Check if the input string is a valid Ethereum (EVM) address using basic heuristics.
58
-
59
- Parameters
60
- ----------
61
- address : str
62
- The address string to verify.
63
-
64
- Returns
65
- -------
66
- bool
67
- True if `address` meets the basic criteria for an EVM address, False otherwise.
68
- """
69
- return _EVMMixin.is_valid_evm_address(address)
70
-
71
-
72
- @staticmethod
73
- def get_evm_network() -> str:
74
- """
75
- Get the current network
98
+ @staticmethod
99
+ def is_valid_eth_address(address: str) -> bool:
100
+ """
101
+ Check if the input string is a valid Ethereum (EVM) address using basic heuristics.
102
+
103
+ Parameters
104
+ ----------
105
+ address : str
106
+ The address string to verify.
107
+
108
+ Returns
109
+ -------
110
+ bool
111
+ True if `address` meets the basic criteria for an EVM address, False otherwise.
112
+ """
113
+ return _EVMMixin.is_valid_evm_address(address)
114
+
115
+
116
+ def _get_eth_address(self, pk=None):
117
+ if pk is None:
118
+ pk = self.public_key
119
+ raw_public_key = pk.public_numbers()
120
+
121
+ # Compute Ethereum-compatible address
122
+ x = raw_public_key.x.to_bytes(32, 'big')
123
+ y = raw_public_key.y.to_bytes(32, 'big')
124
+ uncompressed_key = b'\x04' + x + y
125
+ keccak_hash = keccak(uncompressed_key[1:]) # Remove 0x04 prefix
126
+ eth_address = "0x" + keccak_hash[-20:].hex()
127
+ eth_address = to_checksum_address(eth_address)
128
+ return eth_address
129
+
130
+
131
+ def _get_eth_account(self):
132
+ private_key_bytes = self.private_key.private_numbers().private_value.to_bytes(32, 'big')
133
+ return Account.from_key(private_key_bytes)
134
+
135
+
136
+ def node_address_to_eth_address(self, address):
137
+ """
138
+ Converts a node address to an Ethereum address.
139
+
140
+ Parameters
141
+ ----------
142
+ address : str
143
+ The node address convert.
144
+
145
+ Returns
146
+ -------
147
+ str
148
+ The Ethereum address.
149
+ """
150
+ public_key = self._address_to_pk(address)
151
+ return self._get_eth_address(pk=public_key)
152
+
153
+
154
+ def is_node_address_in_eth_addresses(self, node_address: str, lst_eth_addrs) -> bool:
155
+ """
156
+ Check if the node address is in the list of Ethereum addresses
157
+
158
+ Parameters
159
+ ----------
160
+ node_address : str
161
+ the node address.
162
+
163
+ lst_eth_addrs : list
164
+ list of Ethereum addresses.
76
165
 
77
- Returns
78
- -------
79
- str
80
- the network name.
166
+ Returns
167
+ -------
168
+ bool
169
+ True if the node address is in the list of Ethereum addresses.
81
170
 
82
- """
83
- return os.environ.get(dAuth.DAUTH_NET_ENV_KEY, dAuth.DAUTH_SDK_NET_DEFAULT)
171
+ """
172
+ eth_addr = self.node_address_to_eth_address(node_address)
173
+ return eth_addr in lst_eth_addrs
84
174
 
85
- @property
86
- def evm_network(self):
87
- return self.get_evm_network()
88
175
 
89
- def get_network_data(self, network=None):
90
- assert isinstance(network, str) and network.lower() in dAuth.EVM_NET_DATA, f"Invalid network: {network}"
91
- return dAuth.EVM_NET_DATA[network.lower()]
92
-
93
-
94
- def web3_is_node_licensed(self, address : str, network=None, debug=False) -> bool:
95
- """
96
- Check if the address is allowed to send commands to the node
97
-
98
- Parameters
99
- ----------
100
- address : str
101
- the address to check.
102
- """
103
- if EE_VPN_IMPL:
104
- self.P("VPN implementation. Skipping Ethereum check.", color='r')
105
- return False
176
+ # EVM networks
177
+ if True:
178
+ def reset_network(self, network: str):
179
+ assert network.lower() in dAuth.EVM_NET_DATA, f"Invalid network: {network}"
180
+ os.environ[dAuth.DAUTH_NET_ENV_KEY] = network
181
+ return
106
182
 
107
- if network is None:
108
- network = self.evm_network
183
+ def get_evm_network(self) -> str:
184
+ """
185
+ Get the current network
186
+
187
+ Returns
188
+ -------
189
+ str
190
+ the network name.
191
+
192
+ """
193
+ network = os.environ.get(dAuth.DAUTH_NET_ENV_KEY, dAuth.DAUTH_SDK_NET_DEFAULT)
194
+ if not hasattr(self, "current_evm_network") or self.current_evm_network != network:
195
+ self.current_evm_network = network
196
+ network_data = self.get_network_data(network)
197
+ rpc_url = network_data[dAuth.EvmNetData.DAUTH_RPC_KEY]
198
+ self.web3 = Web3(Web3.HTTPProvider(rpc_url))
199
+ self.P(f"Resetting Web3 for {network=} via {rpc_url=}...")
200
+ return network
109
201
 
110
- assert self.is_valid_eth_address(address), "Invalid Ethereum address"
111
-
112
- network_data = self.get_network_data(network)
202
+ @property
203
+ def evm_network(self):
204
+ return self.get_evm_network()
205
+
113
206
 
114
- contract_address = network_data[dAuth.EvmNetData.DAUTH_ND_ADDR_KEY]
115
- rpc_url = network_data[dAuth.EvmNetData.DAUTH_RPC_KEY]
116
-
117
- if debug:
118
- self.P(f"Checking if {address} ({network}) is allowed via {rpc_url}...")
207
+ def get_network_data(self, network: str) -> dict:
208
+ assert isinstance(network, str) and network.lower() in dAuth.EVM_NET_DATA, f"Invalid network: {network}"
209
+ return dAuth.EVM_NET_DATA[network.lower()]
119
210
 
120
- w3 = Web3(Web3.HTTPProvider(rpc_url))
121
211
 
122
- contract_abi = dAuth.DAUTH_ABI_IS_NODE_ACTIVE
212
+ @property
213
+ def network_rpc(self):
214
+ return self.get_network_data(self.evm_network)[dAuth.EvmNetData.DAUTH_RPC_KEY]
123
215
 
124
- contract = w3.eth.contract(address=contract_address, abi=contract_abi)
125
216
 
126
- result = contract.functions.isNodeActive(address).call()
127
- return result
217
+ @property
218
+ def nd_contract_address(self):
219
+ return self.get_network_data(self.evm_network)[dAuth.EvmNetData.DAUTH_ND_ADDR_KEY]
220
+
221
+ @property
222
+ def r1_contract_address(self):
223
+ return self.get_network_data(self.evm_network)[dAuth.EvmNetData.DAUTH_R1_ADDR_KEY]
128
224
 
129
225
 
130
- def web3_get_oracles(self, network=None, debug=False) -> list:
131
- """
132
- Get the list of oracles from the contract
226
+ def _get_web3_vars(self, network=None) -> Web3Vars:
227
+ if network is None:
228
+ network = self.evm_network
229
+ w3 = self.web3
230
+ else:
231
+ w3 = None
232
+
233
+ network_data = self.get_network_data(network)
234
+ nd_contract_address = network_data[dAuth.EvmNetData.DAUTH_ND_ADDR_KEY]
235
+ rpc_url = network_data[dAuth.EvmNetData.DAUTH_RPC_KEY]
236
+ r1_contract_address = network_data[dAuth.EvmNetData.DAUTH_R1_ADDR_KEY]
237
+ str_genesis_date = network_data[dAuth.EvmNetData.EE_GENESIS_EPOCH_DATE_KEY]
238
+ genesis_date = self.log.str_to_date(str_genesis_date).replace(tzinfo=timezone.utc)
239
+ ep_sec = (
240
+ network_data[dAuth.EvmNetData.EE_EPOCH_INTERVAL_SECONDS_KEY] *
241
+ network_data[dAuth.EvmNetData.EE_EPOCH_INTERVALS_KEY]
242
+ )
243
+
244
+ if w3 is None:
245
+ w3 = Web3(Web3.HTTPProvider(rpc_url))
246
+ self.P(f"Created temporary Web3 for {network=} via {rpc_url=}...", verbosity=2)
247
+ #end if
248
+
249
+ result = Web3Vars(
250
+ w3=w3,
251
+ rpc_url=rpc_url,
252
+ nd_contract_address=nd_contract_address,
253
+ r1_contract_address=r1_contract_address,
254
+ network=network,
255
+ genesis_date=genesis_date,
256
+ epoch_length_seconds=ep_sec
257
+ )
258
+ return result
259
+
260
+ # Epoch handling
261
+ if True:
262
+ def get_epoch_id(self, date : any, network: str = None):
263
+ """
264
+ Given a date as string or datetime, returns the epoch id - ie the number of days since
265
+ the genesis epoch.
266
+
267
+ Parameters
268
+ ----------
269
+ date : str or date
270
+ The date as string that will be converted to epoch id.
271
+ """
272
+ w3vars = self._get_web3_vars(network)
273
+ if isinstance(date, str):
274
+ # remove milliseconds from string
275
+ date = date.split('.')[0]
276
+ date = self.log.str_to_date(date)
277
+ # again this is correct to replace in order to have a timezone aware date
278
+ # and not consider the local timezone. the `date` string naive should be UTC offsetted
279
+ date = date.replace(tzinfo=timezone.utc)
280
+ # compute difference between date and self.__genesis_date in seconds
281
+ elapsed_seconds = (date - w3vars.genesis_date).total_seconds()
282
+
283
+ # the epoch id starts from 0 - the genesis epoch
284
+ # the epoch id is the number of days since the genesis epoch
285
+ # # TODO: change this if we move to start-from-one offset by adding +1
286
+ # OBS: epoch always ends at AB:CD:59 no matter what
287
+ epoch_id = int(elapsed_seconds / w3vars.epoch_length_seconds)
288
+ return epoch_id
133
289
 
134
- Parameters
135
- ----------
136
- network : str, optional
137
- the network to use. The default is None.
138
290
 
139
- Returns
140
- -------
141
- list
142
- the list of oracles addresses.
291
+ def get_current_date(self):
292
+ # we convert local time to UTC time
293
+ return datetime.now(timezone.utc)
143
294
 
144
- """
145
- if network is None:
146
- network = self.evm_network
147
295
 
148
- network_data = self.get_network_data(network)
149
-
150
- contract_address = network_data[dAuth.EvmNetData.DAUTH_ND_ADDR_KEY]
151
- rpc_url = network_data[dAuth.EvmNetData.DAUTH_RPC_KEY]
296
+ def get_time_epoch(self):
297
+ """
298
+ Returns the current epoch id.
299
+ """
300
+ return self.get_epoch_id(self.get_current_date())
152
301
 
153
- if debug:
154
- self.P(f"Getting oracles for {network} via {rpc_url}...")
155
302
 
156
- w3 = Web3(Web3.HTTPProvider(rpc_url))
157
-
158
- contract_abi = dAuth.DAUTH_ABI_GET_SIGNERS
159
-
160
- contract = w3.eth.contract(address=contract_address, abi=contract_abi)
303
+ def get_current_epoch(self):
304
+ """
305
+ Returns the current epoch id using `get_time_epoch`.
306
+ """
307
+ return self.get_time_epoch()
308
+
309
+ ## End Epoch handling
310
+
161
311
 
162
- result = contract.functions.getSigners().call()
163
- return result
164
-
312
+ # EVM signing methods (internal)
313
+ if True:
314
+ def eth_hash_message(self, types, values, as_hex=False):
315
+ """
316
+ Hashes a message using the keccak256 algorithm.
165
317
 
166
-
167
- ### ETH
168
-
169
- def _get_eth_address(self, pk=None):
170
- if pk is None:
171
- pk = self.public_key
172
- raw_public_key = pk.public_numbers()
173
-
174
- # Compute Ethereum-compatible address
175
- x = raw_public_key.x.to_bytes(32, 'big')
176
- y = raw_public_key.y.to_bytes(32, 'big')
177
- uncompressed_key = b'\x04' + x + y
178
- keccak_hash = keccak(uncompressed_key[1:]) # Remove 0x04 prefix
179
- eth_address = "0x" + keccak_hash[-20:].hex()
180
- eth_address = to_checksum_address(eth_address)
181
- return eth_address
182
-
183
- def _get_eth_account(self):
184
- private_key_bytes = self.private_key.private_numbers().private_value.to_bytes(32, 'big')
185
- return Account.from_key(private_key_bytes)
186
-
187
-
188
- def node_address_to_eth_address(self, address):
189
- """
190
- Converts a node address to an Ethereum address.
318
+ Parameters
319
+ ----------
320
+ types : list
321
+ The types of the values.
322
+
323
+ values : list of any
324
+ The values to hash.
325
+
326
+ Returns
327
+ -------
328
+ bytes
329
+ The hash of the message in hexadecimal format.
330
+ """
331
+ message = Web3.solidity_keccak(types, values)
332
+ if as_hex:
333
+ return message.hex()
334
+ return message
335
+
336
+
337
+ def eth_sign_message(self, types, values):
338
+ """
339
+ Signs a message using the private key.
340
+
341
+ Parameters
342
+ ----------
343
+ types : list
344
+ The types of the values.
345
+
346
+ values : list of any
347
+ The values to sign.
191
348
 
192
- Parameters
193
- ----------
194
- address : str
195
- The node address convert.
349
+ Returns
350
+ -------
351
+ str
352
+ The signature of the message.
353
+
354
+ Notes
355
+ -----
356
+
357
+ This function is using the `eth_account` property generated from the private key via
358
+ the `_get_eth_account` method at the time of the object creation.
359
+ """
360
+ message_hash = self.eth_hash_message(types, values, as_hex=False)
361
+ signable_message = encode_defunct(primitive=message_hash)
362
+ signed_message = Account.sign_message(signable_message, private_key=self.eth_account.key)
363
+ if hasattr(signed_message, "message_hash"): # backward compatibility
364
+ signed_message_hash = signed_message.message_hash
365
+ else:
366
+ signed_message_hash = signed_message.messageHash
367
+ return {
368
+ "message_hash": message_hash.hex(),
369
+ "r": hex(signed_message.r),
370
+ "s": hex(signed_message.s),
371
+ "v": signed_message.v,
372
+ "signature": signed_message.signature.hex(),
373
+ "signed_message": signed_message_hash.hex(),
374
+ "sender" : self.eth_address,
375
+ "eth_signed_data" : types,
376
+ }
377
+
378
+ def eth_sign_text(self, message, signature_only=True):
379
+ """
380
+ Signs a text message using the private key.
381
+
382
+ Parameters
383
+ ----------
384
+ message : str
385
+ The message to sign.
386
+
387
+ signature_only : bool, optional
388
+ Whether to return only the signature. The default is True
389
+
390
+ Returns
391
+ -------
392
+ str
393
+ The signature of the message.
394
+ """
395
+ types = ["string"]
396
+ values = [message]
397
+ result = self.eth_sign_message(types, values)
398
+ if signature_only:
399
+ return result["signature"]
400
+ return result
401
+
402
+
403
+
404
+ def eth_sign_node_epochs(
405
+ self,
406
+ node,
407
+ epochs,
408
+ epochs_vals,
409
+ signature_only=True,
410
+ use_evm_node_addr=True
411
+ ):
412
+ """
413
+ Signs the node availability
414
+
415
+ Parameters
416
+ ----------
417
+ node : str
418
+ The node address to sign. Either the node address or the Ethereum address based on `use_evm_node_addr`.
419
+
420
+ epochs : list of int
421
+ The epochs to sign.
422
+
423
+ epochs_vals : list of int
424
+ The values for each epoch.
425
+
426
+ signature_only : bool, optional
427
+ Whether to return only the signature. The default is True.
428
+
429
+ use_evm_node_addr : bool, optional
430
+ Whether to use the Ethereum address of the node. The default is True.
431
+
432
+ Returns
433
+ -------
434
+ str
435
+ The signature of the message.
436
+ """
437
+ if use_evm_node_addr:
438
+ types = ["address", "uint256[]", "uint256[]"]
439
+ else:
440
+ types = ["string", "uint256[]", "uint256[]"]
441
+ values = [node, epochs, epochs_vals]
442
+ result = self.eth_sign_message(types, values)
443
+ if signature_only:
444
+ return result["signature"]
445
+ return result
446
+
447
+
448
+ ### Web3 functions
449
+ if True:
450
+ def web3_hash_message(self, types, values, as_hex=False):
451
+ """
452
+ Hashes a message using the keccak256 algorithm.
453
+
454
+ Parameters
455
+ ----------
456
+ types : list
457
+ The types of the values.
458
+
459
+ values : list of any
460
+ The values to hash.
461
+
462
+ Returns
463
+ -------
464
+ bytes
465
+ The hash of the message in hexadecimal format.
466
+ """
467
+ return self.eth_hash_message(types, values, as_hex=as_hex)
468
+
469
+ def web3_sign_message(self, types, values):
470
+ """
471
+ Signs a message using the private key.
472
+
473
+ Parameters
474
+ ----------
475
+ types : list
476
+ The types of the values.
477
+
478
+ values : list of any
479
+ The values to sign.
196
480
 
197
- Returns
198
- -------
199
- str
200
- The Ethereum address.
201
- """
202
- public_key = self._address_to_pk(address)
203
- return self._get_eth_address(pk=public_key)
204
-
205
- def is_node_address_in_eth_addresses(self, node_address: str, lst_eth_addrs) -> bool:
206
- """
207
- Check if the node address is in the list of Ethereum addresses
481
+ Returns
482
+ -------
483
+ str
484
+ The signature of the message.
485
+
486
+ Notes
487
+ -----
208
488
 
209
- Parameters
210
- ----------
211
- node_address : str
212
- the node address.
489
+ """
490
+ return self.eth_sign_message(types, values)
491
+
492
+ def web3_is_node_licensed(self, address : str, network=None, debug=False) -> bool:
493
+ """
494
+ Check if the address is allowed to send commands to the node
495
+
496
+ Parameters
497
+ ----------
498
+ address : str
499
+ the address to check.
500
+ """
501
+ if EE_VPN_IMPL:
502
+ self.P("VPN implementation. Skipping Ethereum check.", color='r')
503
+ return False
213
504
 
214
- lst_eth_addrs : list
215
- list of Ethereum addresses.
216
-
217
- Returns
218
- -------
219
- bool
220
- True if the node address is in the list of Ethereum addresses.
505
+ w3vars = self._get_web3_vars(network)
506
+
507
+ assert self.is_valid_eth_address(address), "Invalid Ethereum address"
508
+
509
+ if debug:
510
+ self.P(f"Checking if {address} ({network}) is allowed...")
511
+
512
+ contract_abi = dAuth.DAUTH_ABI_IS_NODE_ACTIVE
513
+ contract = w3vars.w3.eth.contract(address=w3vars.nd_contract_address, abi=contract_abi)
221
514
 
222
- """
223
- eth_addr = self.node_address_to_eth_address(node_address)
224
- return eth_addr in lst_eth_addrs
515
+ result = contract.functions.isNodeActive(address).call()
516
+ return result
225
517
 
226
518
 
227
- def eth_hash_message(self, types, values, as_hex=False):
228
- """
229
- Hashes a message using the keccak256 algorithm.
519
+ def web3_get_oracles(self, network=None, debug=False) -> list:
520
+ """
521
+ Get the list of oracles from the contract
230
522
 
231
- Parameters
232
- ----------
233
- types : list
234
- The types of the values.
235
-
236
- values : list of any
237
- The values to hash.
523
+ Parameters
524
+ ----------
525
+ network : str, optional
526
+ the network to use. The default is None.
238
527
 
239
- Returns
240
- -------
241
- bytes
242
- The hash of the message in hexadecimal format.
243
- """
244
- message = Web3.solidity_keccak(types, values)
245
- if as_hex:
246
- return message.hex()
247
- return message
248
-
249
-
250
- def eth_sign_message(self, types, values):
251
- """
252
- Signs a message using the private key.
528
+ Returns
529
+ -------
530
+ list
531
+ the list of oracles addresses.
253
532
 
254
- Parameters
255
- ----------
256
- types : list
257
- The types of the values.
258
-
259
- values : list of any
260
- The values to sign.
533
+ """
534
+ w3vars = self._get_web3_vars(network)
261
535
 
262
- Returns
263
- -------
264
- str
265
- The signature of the message.
266
- """
267
- message_hash = self.eth_hash_message(types, values, as_hex=False)
268
- signable_message = encode_defunct(primitive=message_hash)
269
- signed_message = Account.sign_message(signable_message, private_key=self.eth_account.key)
270
- if hasattr(signed_message, "message_hash"): # backward compatibility
271
- signed_message_hash = signed_message.message_hash
272
- else:
273
- signed_message_hash = signed_message.messageHash
274
- return {
275
- "message_hash": message_hash.hex(),
276
- "r": hex(signed_message.r),
277
- "s": hex(signed_message.s),
278
- "v": signed_message.v,
279
- "signature": signed_message.signature.hex(),
280
- "signed_message": signed_message_hash.hex(),
281
- "sender" : self.eth_address,
282
- "eth_signed_data" : types,
283
- }
284
-
285
- def eth_sign_text(self, message, signature_only=True):
286
- """
287
- Signs a text message using the private key.
536
+ if debug:
537
+ self.P(f"Getting oracles for {w3vars.network} via {w3vars.rpc_url}...")
538
+
539
+ contract_abi = dAuth.DAUTH_ABI_GET_SIGNERS
540
+ contract = w3vars.w3.eth.contract(
541
+ address=w3vars.nd_contract_address, abi=contract_abi
542
+ )
288
543
 
289
- Parameters
290
- ----------
291
- message : str
292
- The message to sign.
293
-
294
- signature_only : bool, optional
295
- Whether to return only the signature. The default is True
544
+ result = contract.functions.getSigners().call()
545
+ return result
296
546
 
297
- Returns
298
- -------
299
- str
300
- The signature of the message.
301
- """
302
- types = ["string"]
303
- values = [message]
304
- result = self.eth_sign_message(types, values)
305
- if signature_only:
306
- return result["signature"]
307
- return result
308
-
309
547
 
548
+ def web3_get_balance_eth(self, address=None, network=None):
549
+ """
550
+ Get the ETH balance of the address
551
+
552
+ Parameters
553
+ ----------
554
+ address : str
555
+ The address to check.
556
+
557
+ Returns
558
+ -------
559
+ float
560
+ The balance of the address.
561
+ """
562
+ if address is None:
563
+ address = self.eth_address
564
+ assert self.is_valid_eth_address(address), "Invalid Ethereum address"
565
+ w3vars = self._get_web3_vars(network)
566
+ balance_wei = w3vars.w3.eth.get_balance(address)
567
+ balance_eth = w3vars.w3.from_wei(balance_wei, 'ether')
568
+ return float(balance_eth)
569
+
570
+
571
+ def web3_send_eth(
572
+ self,
573
+ to_address,
574
+ amount_eth,
575
+ extra_buffer_eth=0.005,
576
+ network=None,
577
+ wait_for_tx=True,
578
+ timeout=120,
579
+ return_receipt=False,
580
+ raise_if_error=False,
581
+ ):
582
+ """
583
+ Send ETH from the account associated with this object to another address,
584
+ ensuring there is enough balance to cover the transfer amount, gas costs,
585
+ and an additional buffer.
586
+
587
+ Parameters
588
+ ----------
589
+ to_address : str
590
+ The recipient Ethereum address.
591
+
592
+ amount_eth : float
593
+ The amount of ETH to send.
594
+
595
+ extra_buffer_eth : float, optional
596
+ An additional amount (in ETH) as a safety margin. Default is 0.005 ETH.
597
+
598
+ network : str, optional
599
+ The network to use. Default is None.
600
+
601
+ wait_for_tx : bool, optional
602
+ Whether to wait for the transaction to be mined. Default is True.
603
+
604
+ timeout : int, optional
605
+ The maximum time to wait for the transaction to be mined, in seconds. Default is 120 seconds.
606
+
607
+ return_receipt : bool, optional
608
+ If True, returns the transaction receipt instead of the transaction hash. Default is False.
609
+
610
+ raise_if_error : bool, optional
611
+ If True, raises an exception if the transaction fails. Default is False.
612
+
613
+ Returns
614
+ -------
615
+ str
616
+ The transaction hash of the broadcasted transaction.
617
+ """
618
+ w3vars = self._get_web3_vars(network=network)
619
+ network = w3vars.network
620
+
621
+ # Get the sender's address from the object's stored attribute (assumed available)
622
+ from_address = self.eth_address
623
+
624
+ # Fetch the current balance (in Wei)
625
+ balance_wei = w3vars.w3.eth.get_balance(from_address)
626
+
627
+ # Define gas parameters for a standard ETH transfer.
628
+ gas_limit = 21000 # typical gas limit for a simple ETH transfer
629
+ gas_price = w3vars.w3.to_wei('50', 'gwei') # example gas price; you may choose a dynamic approach
630
+
631
+ # Calculate the total gas cost.
632
+ gas_cost = gas_limit * gas_price
633
+
634
+ # Convert transfer amount and buffer to Wei.
635
+ amount_wei = w3vars.w3.to_wei(amount_eth, 'ether')
636
+ extra_buffer = w3vars.w3.to_wei(extra_buffer_eth, 'ether')
637
+
638
+ # Compute the total cost: amount to send + gas cost + extra buffer.
639
+ total_cost = amount_wei + gas_cost + extra_buffer
640
+
641
+ # Check if the balance is sufficient.
642
+ if balance_wei < total_cost:
643
+ msg = "Insufficient funds: your balance is less than the required amount plus gas cost and buffer."
644
+ if raise_if_error:
645
+ raise Exception(msg)
646
+ else:
647
+ self.P(msg, color='r')
648
+ return None
649
+
650
+ # Get the nonce for the transaction.
651
+ nonce = w3vars.w3.eth.get_transaction_count(from_address)
652
+
653
+ chain_id = w3vars.w3.eth.chain_id
654
+
655
+ # Build the transaction dictionary.
656
+ tx = {
657
+ 'nonce': nonce,
658
+ 'to': to_address,
659
+ 'value': amount_wei,
660
+ 'gas': gas_limit,
661
+ 'gasPrice': gas_price,
662
+ 'chainId': chain_id,
663
+ }
664
+
665
+ self.P(f"Executing transaction on {network} via {w3vars.rpc_url}:\n {json.dumps(tx, indent=2)}", verbosity=2)
666
+
667
+ # Sign the transaction with the account's private key.
668
+ signed_tx = w3vars.w3.eth.account.sign_transaction(tx, self.eth_account.key)
669
+
670
+ # Broadcast the signed transaction.
671
+ tx_hash = w3vars.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
672
+
673
+ if wait_for_tx:
674
+ # Wait for the transaction receipt with the specified timeout.
675
+ self.P("Waiting for transaction to be mined...", verbosity=2)
676
+ tx_receipt = w3vars.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
677
+ tx_hash_hex = tx_receipt.transactionHash.hex()
678
+ self.P(f"Transaction mined: {tx_hash_hex}", color='g', verbosity=2)
679
+ if return_receipt:
680
+ return tx_receipt
681
+ else:
682
+ return tx_hash_hex
683
+ else:
684
+ return tx_hash.hex()
685
+
686
+
687
+ def web3_get_balance_r1(self, address=None, network=None):
688
+ """
689
+ Get the R1 balance of the address
690
+
691
+ Parameters
692
+ ----------
693
+ address : str
694
+ The address to check.
695
+
696
+ Returns
697
+ -------
698
+ float
699
+ The balance of the address.
700
+ """
701
+ if address is None:
702
+ address = self.eth_address
703
+ assert self.is_valid_eth_address(address), "Invalid Ethereum address"
704
+ w3vars = self._get_web3_vars(network)
705
+
706
+ token_contract = w3vars.w3.eth.contract(
707
+ address=w3vars.r1_contract_address, abi=ERC20_ABI
708
+ )
709
+
710
+ try:
711
+ decimals = token_contract.functions.decimals().call()
712
+ except Exception:
713
+ decimals = 18 # default to 18 if the decimals call fails
714
+
715
+ raw_balance = token_contract.functions.balanceOf(address).call()
716
+ human_balance = raw_balance / (10 ** decimals)
717
+ return float(human_balance)
310
718
 
311
- def eth_sign_node_epochs(
312
- self,
313
- node,
314
- epochs,
315
- epochs_vals,
316
- signature_only=True,
317
- use_evm_node_addr=True
318
- ):
319
- """
320
- Signs the node availability
719
+ def web3_send_r1(
720
+ self,
721
+ to_address: str,
722
+ amount: float,
723
+ extra_buffer_eth: float = 0.005,
724
+ wait_for_tx: bool = False,
725
+ timeout: int = 120,
726
+ network: str = None,
727
+ return_receipt=False,
728
+ raise_if_error=False,
729
+ ):
730
+ """
731
+ Send R1 tokens from the default account (self.eth_address) to the specified address.
732
+
733
+ Parameters
734
+ ----------
735
+ to_address : str
736
+ The recipient's Ethereum address.
737
+
738
+ amount : float
739
+ The amount of R1 tokens to send (in human-readable units).
740
+
741
+ extra_buffer_eth : float, optional
742
+ Additional ETH (in Ether) as a buffer for gas fees. Default is 0.005 ETH.
743
+
744
+ wait_for_tx : bool, optional
745
+ If True, waits for the transaction to be mined and returns the receipt.
746
+ If False, returns immediately with the transaction hash.
747
+
748
+ timeout : int, optional
749
+ Maximum number of seconds to wait for the transaction receipt. Default is 120.
750
+
751
+ network : str, optional
752
+ The network to use. If None, uses the default self.evm_network.
321
753
 
322
- Parameters
323
- ----------
324
- node : str
325
- The node address to sign. Either the node address or the Ethereum address based on `use_evm_node_addr`.
326
-
327
- epochs : list of int
328
- The epochs to sign.
329
-
330
- epochs_vals : list of int
331
- The values for each epoch.
332
-
333
- signature_only : bool, optional
334
- Whether to return only the signature. The default is True.
335
-
336
- use_evm_node_addr : bool, optional
337
- Whether to use the Ethereum address of the node. The default is True.
754
+ return_receipt: bool, optional
755
+ If True, returns the transaction receipt instead of the transaction hash.
756
+
757
+ raise_if_error : bool, optional
758
+ If True, raises an exception if the transaction fails. Default is False.
759
+
760
+ Returns
761
+ -------
762
+ If wait_for_tx is False, returns the transaction hash as a string.
763
+ If wait_for_tx is True, returns the transaction receipt as a dict.
764
+ """
765
+ # Validate the recipient address.
766
+ assert self.is_valid_eth_address(to_address), "Invalid Ethereum address"
767
+
768
+ # Retrieve the Web3 instance, RPC URL, and the R1 contract address.
769
+ # Note: This follows the same pattern as web3_get_balance_r1.
770
+ w3vars = self._get_web3_vars(network)
771
+ network = w3vars.network
772
+
773
+ # Create the token contract instance.
774
+ token_contract = w3vars.w3.eth.contract(
775
+ address=w3vars.r1_contract_address, abi=ERC20_ABI
776
+ )
777
+
778
+ # Get the token's decimals (default to 18 if not available).
779
+ try:
780
+ decimals = token_contract.functions.decimals().call()
781
+ except Exception:
782
+ decimals = 18
783
+
784
+ # Convert the human-readable amount to the token's smallest unit.
785
+ token_amount = int(amount * (10 ** decimals))
786
+
787
+ # Ensure the sender has enough R1 token balance.
788
+ sender_balance = token_contract.functions.balanceOf(self.eth_address).call()
789
+ if sender_balance < token_amount:
790
+ msg = "Insufficient funds: your $R1 balance is less than the required amount."
791
+ if raise_if_error:
792
+ raise Exception(msg)
793
+ else:
794
+ self.P(msg, color='r')
795
+ return None
796
+
797
+ # Estimate gas fees for the token transfer.
798
+ gas_price = w3vars.w3.to_wei('50', 'gwei') # Adjust as needed or use a dynamic gas strategy.
799
+ estimated_gas = token_contract.functions.transfer(
800
+ to_address, token_amount
801
+ ).estimate_gas(
802
+ {'from': self.eth_address}
803
+ )
804
+ gas_cost = estimated_gas * gas_price
805
+
806
+ # Check that the sender's ETH balance can cover gas costs plus an extra buffer.
807
+ eth_balance = w3vars.w3.eth.get_balance(self.eth_address)
808
+ extra_buffer = w3vars.w3.to_wei(extra_buffer_eth, 'ether')
809
+ if eth_balance < gas_cost + extra_buffer:
810
+ raise Exception("Insufficient ETH balance to cover gas fees and extra buffer.")
811
+
812
+ # Get the transaction count for the nonce.
813
+ nonce = w3vars.w3.eth.get_transaction_count(self.eth_address)
814
+
815
+ # Programmatically determine the chainId.
816
+ chain_id = w3vars.w3.eth.chain_id
817
+
818
+ # Build the transaction for the ERC20 transfer.
819
+ tx = token_contract.functions.transfer(to_address, token_amount).build_transaction({
820
+ 'from': self.eth_address,
821
+ 'nonce': nonce,
822
+ 'gas': estimated_gas,
823
+ 'gasPrice': gas_price,
824
+ 'chainId': chain_id,
825
+ })
826
+
827
+ self.P(f"Executing transaction on {network} via {w3vars.rpc_url}:\n {json.dumps(dict(tx), indent=2)}", verbosity=2)
828
+
829
+ # Sign the transaction using the internal account (via _get_eth_account).
830
+ eth_account = self._get_eth_account()
831
+ signed_tx = w3vars.w3.eth.account.sign_transaction(tx, eth_account.key)
832
+
833
+ # Broadcast the transaction.
834
+ tx_hash = w3vars.w3.eth.send_raw_transaction(signed_tx.raw_transaction)
835
+
836
+ if wait_for_tx:
837
+ # Wait for the transaction receipt if required.
838
+ tx_receipt = w3vars.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=timeout)
839
+ tx_hash_hex = tx_receipt.transactionHash.hex()
840
+ self.P(f"Transaction mined: {tx_hash_hex}", color='g', verbosity=2)
841
+ if return_receipt:
842
+ return tx_receipt
843
+ else:
844
+ return tx_hash_hex
845
+ else:
846
+ return tx_hash.hex()
338
847
 
339
- Returns
340
- -------
341
- str
342
- The signature of the message.
343
- """
344
- if use_evm_node_addr:
345
- types = ["address", "uint256[]", "uint256[]"]
346
- else:
347
- types = ["string", "uint256[]", "uint256[]"]
348
- values = [node, epochs, epochs_vals]
349
- result = self.eth_sign_message(types, values)
350
- if signature_only:
351
- return result["signature"]
352
- return result
353
-
354
848
 
355
-
849
+