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/_ver.py +1 -1
- naeural_client/base/generic_session.py +21 -2
- naeural_client/bc/base.py +1 -1
- naeural_client/bc/evm.py +789 -295
- naeural_client/cli/nodes.py +2 -0
- naeural_client/const/evm_net.py +4 -0
- naeural_client/utils/config.py +8 -2
- {naeural_client-3.0.9.dist-info → naeural_client-3.1.1.dist-info}/METADATA +1 -1
- {naeural_client-3.0.9.dist-info → naeural_client-3.1.1.dist-info}/RECORD +12 -12
- {naeural_client-3.0.9.dist-info → naeural_client-3.1.1.dist-info}/WHEEL +0 -0
- {naeural_client-3.0.9.dist-info → naeural_client-3.1.1.dist-info}/entry_points.txt +0 -0
- {naeural_client-3.0.9.dist-info → naeural_client-3.1.1.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
166
|
+
Returns
|
167
|
+
-------
|
168
|
+
bool
|
169
|
+
True if the node address is in the list of Ethereum addresses.
|
81
170
|
|
82
|
-
|
83
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
108
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
202
|
+
@property
|
203
|
+
def evm_network(self):
|
204
|
+
return self.get_evm_network()
|
205
|
+
|
113
206
|
|
114
|
-
|
115
|
-
|
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
|
-
|
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
|
-
|
127
|
-
|
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
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
163
|
-
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
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
|
-
|
224
|
-
return eth_addr in lst_eth_addrs
|
515
|
+
result = contract.functions.isNodeActive(address).call()
|
516
|
+
return result
|
225
517
|
|
226
518
|
|
227
|
-
|
228
|
-
|
229
|
-
|
519
|
+
def web3_get_oracles(self, network=None, debug=False) -> list:
|
520
|
+
"""
|
521
|
+
Get the list of oracles from the contract
|
230
522
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
The
|
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
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
-
|
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
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
+
|