dkg 8.0.0a3__py3-none-any.whl → 8.0.2__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 (72) hide show
  1. dkg/__init__.py +1 -1
  2. dkg/assertion.py +2 -2
  3. dkg/clients/__init__.py +4 -0
  4. dkg/clients/async_dkg.py +109 -0
  5. dkg/{main.py → clients/dkg.py} +42 -21
  6. dkg/constants.py +117 -6
  7. dkg/data/interfaces/AskStorage.json +366 -0
  8. dkg/data/interfaces/Chronos.json +202 -0
  9. dkg/data/interfaces/Hub.json +294 -2
  10. dkg/data/interfaces/IdentityStorage.json +58 -0
  11. dkg/data/interfaces/{ContentAsset.json → KnowledgeCollection.json} +256 -343
  12. dkg/data/interfaces/KnowledgeCollectionStorage.json +2312 -0
  13. dkg/data/interfaces/Paranet.json +30 -214
  14. dkg/data/interfaces/ParanetIncentivesPoolFactory.json +18 -2
  15. dkg/data/interfaces/ParanetKnowledgeMinersRegistry.json +20 -4
  16. dkg/data/interfaces/{ParanetNeurowebIncentivesPool.json → ParanetNeuroIncentivesPool.json} +7 -7
  17. dkg/data/interfaces/ParanetsRegistry.json +102 -32
  18. dkg/data/interfaces/Token.json +146 -17
  19. dkg/managers/__init__.py +0 -0
  20. dkg/managers/async_manager.py +69 -0
  21. dkg/{manager.py → managers/manager.py} +5 -3
  22. dkg/method.py +5 -2
  23. dkg/modules/__init__.py +0 -0
  24. dkg/modules/asset/__init__.py +0 -0
  25. dkg/modules/asset/asset.py +739 -0
  26. dkg/modules/asset/async_asset.py +753 -0
  27. dkg/modules/async_module.py +66 -0
  28. dkg/modules/graph/__init__.py +0 -0
  29. dkg/modules/graph/async_graph.py +112 -0
  30. dkg/modules/graph/graph.py +87 -0
  31. dkg/{module.py → modules/module.py} +1 -1
  32. dkg/modules/network/__init__.py +0 -0
  33. dkg/{network.py → modules/network/network.py} +4 -4
  34. dkg/modules/node/__init__.py +0 -0
  35. dkg/modules/node/async_node.py +39 -0
  36. dkg/{node.py → modules/node/node.py} +2 -2
  37. dkg/modules/paranet/__init__.py +0 -0
  38. dkg/{paranet.py → modules/paranet/paranet.py} +2 -6
  39. dkg/providers/__init__.py +9 -2
  40. dkg/providers/blockchain/__init__.py +4 -0
  41. dkg/providers/blockchain/async_blockchain.py +245 -0
  42. dkg/providers/blockchain/base_blockchain.py +102 -0
  43. dkg/providers/{blockchain.py → blockchain/blockchain.py} +15 -96
  44. dkg/providers/node/__init__.py +4 -0
  45. dkg/providers/node/async_node_http.py +72 -0
  46. dkg/providers/node/base_node_http.py +25 -0
  47. dkg/providers/{node_http.py → node/node_http.py} +12 -10
  48. dkg/services/__init__.py +0 -0
  49. dkg/services/blockchain_services/__init__.py +0 -0
  50. dkg/services/blockchain_services/async_blockchain_service.py +180 -0
  51. dkg/services/blockchain_services/blockchain_service.py +174 -0
  52. dkg/services/input_service.py +181 -0
  53. dkg/services/node_services/__init__.py +0 -0
  54. dkg/services/node_services/async_node_service.py +184 -0
  55. dkg/services/node_services/node_service.py +167 -0
  56. dkg/types/__init__.py +11 -11
  57. dkg/utils/blockchain_request.py +76 -50
  58. dkg/utils/knowledge_asset_tools.py +5 -0
  59. dkg/utils/knowledge_collection_tools.py +248 -0
  60. dkg/utils/node_request.py +60 -14
  61. dkg/utils/rdf.py +9 -3
  62. {dkg-8.0.0a3.dist-info → dkg-8.0.2.dist-info}/METADATA +28 -19
  63. dkg-8.0.2.dist-info/RECORD +82 -0
  64. {dkg-8.0.0a3.dist-info → dkg-8.0.2.dist-info}/WHEEL +1 -1
  65. dkg/asset.py +0 -912
  66. dkg/data/interfaces/AssertionStorage.json +0 -229
  67. dkg/data/interfaces/ContentAssetStorage.json +0 -706
  68. dkg/data/interfaces/ServiceAgreementStorageProxy.json +0 -1314
  69. dkg/graph.py +0 -63
  70. dkg-8.0.0a3.dist-info/RECORD +0 -52
  71. {dkg-8.0.0a3.dist-info → dkg-8.0.2.dist-info}/LICENSE +0 -0
  72. {dkg-8.0.0a3.dist-info → dkg-8.0.2.dist-info}/NOTICE +0 -0
@@ -0,0 +1,66 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ from dataclasses import asdict
19
+ from typing import Any, Callable, Sequence
20
+
21
+ from dkg.exceptions import ValidationError
22
+ from dkg.managers.async_manager import AsyncRequestManager
23
+ from dkg.method import Method
24
+ from dkg.types import TReturn
25
+
26
+
27
+ class AsyncModule:
28
+ manager: AsyncRequestManager
29
+
30
+ def retrieve_caller_fn(
31
+ self, method: Method[Callable[..., TReturn]]
32
+ ) -> Callable[..., TReturn]:
33
+ async def caller(*args: Any, **kwargs: Any) -> TReturn:
34
+ processed_args = method.process_args(*args, **kwargs)
35
+ request_params = asdict(method.action)
36
+ request_params.update(processed_args)
37
+
38
+ return await self.manager.blocking_request(
39
+ type(method.action), request_params
40
+ )
41
+
42
+ return caller
43
+
44
+ def _attach_modules(self, module_definitions: dict[str, Any]) -> None:
45
+ for module_name, module_info in module_definitions.items():
46
+ module_info_is_list_like = isinstance(module_info, Sequence)
47
+
48
+ module = module_info[0] if module_info_is_list_like else module_info
49
+
50
+ if hasattr(self, module_name):
51
+ raise AttributeError(
52
+ f"Cannot set {self} module named '{module_name}'. "
53
+ " The dkg object already has an attribute with that name"
54
+ )
55
+
56
+ setattr(self, module_name, module)
57
+
58
+ if module_info_is_list_like:
59
+ if len(module_info) == 2:
60
+ submodule_definitions = module_info[1]
61
+ module: "AsyncModule" = getattr(self, module_name)
62
+ module._attach_modules(submodule_definitions)
63
+ elif len(module_info) != 1:
64
+ raise ValidationError(
65
+ "Module definitions can only have 1 or 2 elements."
66
+ )
File without changes
@@ -0,0 +1,112 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+
19
+ from rdflib.plugins.sparql.parser import parseQuery
20
+
21
+ from dkg.managers.async_manager import AsyncRequestManager
22
+ from dkg.modules.async_module import AsyncModule
23
+ from dkg.types import NQuads
24
+ from dkg.constants import Status
25
+ from dkg.services.input_service import InputService
26
+ from dkg.services.node_services.async_node_service import AsyncNodeService
27
+ from dkg.types import UAL
28
+
29
+
30
+ class AsyncGraph(AsyncModule):
31
+ def __init__(
32
+ self,
33
+ manager: AsyncRequestManager,
34
+ input_service: InputService,
35
+ node_service: AsyncNodeService,
36
+ ):
37
+ self.manager = manager
38
+ self.input_service = input_service
39
+ self.node_service = node_service
40
+
41
+ async def query(
42
+ self,
43
+ query: str,
44
+ options: dict = None,
45
+ ) -> NQuads:
46
+ if options is None:
47
+ options = {}
48
+
49
+ arguments = self.input_service.get_query_arguments(options)
50
+
51
+ paranet_ual = arguments.get("paranet_ual")
52
+ repository = arguments.get("repository")
53
+
54
+ parsed_query = parseQuery(query)
55
+ query_type = parsed_query[1].name.replace("Query", "").upper()
56
+
57
+ result = await self.node_service.query(
58
+ query, query_type, repository, paranet_ual
59
+ )
60
+
61
+ return result.get("data")
62
+
63
+ async def publish_finality(self, UAL: UAL, options=None):
64
+ if options is None:
65
+ options = {}
66
+
67
+ arguments = self.input_service.get_publish_finality_arguments(options)
68
+ max_number_of_retries = arguments.get("max_number_of_retries")
69
+ minimum_number_of_finalization_confirmations = arguments.get(
70
+ "minimum_number_of_finalization_confirmations"
71
+ )
72
+ frequency = arguments.get("frequency")
73
+ try:
74
+ finality_status_result = await self.node_service.finality_status(
75
+ UAL,
76
+ minimum_number_of_finalization_confirmations,
77
+ max_number_of_retries,
78
+ frequency,
79
+ )
80
+ except Exception as e:
81
+ return {"status": Status.ERROR.value, "error": str(e)}
82
+
83
+ if finality_status_result == 0:
84
+ try:
85
+ finality_operation_id = await self.node_service.finality(
86
+ UAL,
87
+ minimum_number_of_finalization_confirmations,
88
+ max_number_of_retries,
89
+ frequency,
90
+ )
91
+ except Exception as e:
92
+ return {"status": Status.ERROR.value, "error": str(e)}
93
+
94
+ try:
95
+ return await self.node_service.get_operation_result(
96
+ finality_operation_id, "finality", max_number_of_retries, frequency
97
+ )
98
+ except Exception as e:
99
+ return {"status": Status.NOT_FINALIZED.value, "error": str(e)}
100
+
101
+ elif finality_status_result >= minimum_number_of_finalization_confirmations:
102
+ return {
103
+ "status": Status.FINALIZED.value,
104
+ "numberOfConfirmations": finality_status_result,
105
+ "requiredConfirmations": minimum_number_of_finalization_confirmations,
106
+ }
107
+ else:
108
+ return {
109
+ "status": Status.NOT_FINALIZED.value,
110
+ "numberOfConfirmations": finality_status_result,
111
+ "requiredConfirmations": minimum_number_of_finalization_confirmations,
112
+ }
@@ -0,0 +1,87 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ from rdflib.plugins.sparql.parser import parseQuery
19
+
20
+
21
+ from dkg.managers.manager import DefaultRequestManager
22
+ from dkg.modules.module import Module
23
+ from dkg.types import NQuads
24
+ from dkg.services.node_services.node_service import NodeService
25
+ from dkg.services.input_service import InputService
26
+ from dkg.constants import Status
27
+
28
+
29
+ class Graph(Module):
30
+ def __init__(
31
+ self,
32
+ manager: DefaultRequestManager,
33
+ input_service: InputService,
34
+ node_service: NodeService,
35
+ ):
36
+ self.manager = manager
37
+ self.input_service = input_service
38
+ self.node_service = node_service
39
+
40
+ def query(
41
+ self,
42
+ query: str,
43
+ options: dict = {},
44
+ ) -> NQuads:
45
+ arguments = self.input_service.get_query_arguments(options)
46
+ paranet_ual = arguments.get("paranet_ual")
47
+ repository = arguments.get("repository")
48
+
49
+ parsed_query = parseQuery(query)
50
+ query_type = parsed_query[1].name.replace("Query", "").upper()
51
+
52
+ result = self.node_service.query(query, query_type, repository, paranet_ual)
53
+
54
+ return result.get("data")
55
+
56
+ def publish_finality(self, UAL, options=None):
57
+ if options is None:
58
+ options = {}
59
+
60
+ arguments = self.input_service.get_publish_finality_arguments(options)
61
+ max_number_of_retries = arguments.get("max_number_of_retries")
62
+ minimum_number_of_finalization_confirmations = arguments.get(
63
+ "minimum_number_of_finalization_confirmations"
64
+ )
65
+ frequency = arguments.get("frequency")
66
+ try:
67
+ finality_status_result = self.node_service.finality_status(
68
+ UAL,
69
+ minimum_number_of_finalization_confirmations,
70
+ max_number_of_retries,
71
+ frequency,
72
+ )
73
+ except Exception as e:
74
+ return {"status": Status.ERROR.value, "error": str(e)}
75
+
76
+ if finality_status_result >= minimum_number_of_finalization_confirmations:
77
+ return {
78
+ "status": Status.FINALIZED.value,
79
+ "numberOfConfirmations": finality_status_result,
80
+ "requiredConfirmations": minimum_number_of_finalization_confirmations,
81
+ }
82
+ else:
83
+ return {
84
+ "status": Status.NOT_FINALIZED.value,
85
+ "numberOfConfirmations": finality_status_result,
86
+ "requiredConfirmations": minimum_number_of_finalization_confirmations,
87
+ }
@@ -19,7 +19,7 @@ from dataclasses import asdict
19
19
  from typing import Any, Callable, Sequence
20
20
 
21
21
  from dkg.exceptions import ValidationError
22
- from dkg.manager import DefaultRequestManager
22
+ from dkg.managers.manager import DefaultRequestManager
23
23
  from dkg.method import Method
24
24
  from dkg.types import TReturn
25
25
 
File without changes
@@ -15,11 +15,11 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
- from dkg.constants import DEFAULT_HASH_FUNCTION_ID
18
+ from dkg.constants import DefaultParameters
19
19
  from dkg.dataclasses import BidSuggestionRange
20
- from dkg.manager import DefaultRequestManager
20
+ from dkg.managers.manager import DefaultRequestManager
21
21
  from dkg.method import Method
22
- from dkg.module import Module
22
+ from dkg.modules.module import Module
23
23
  from dkg.types import DataHexStr
24
24
  from dkg.utils.blockchain_request import BlockchainRequest
25
25
  from dkg.utils.node_request import NodeRequest
@@ -50,7 +50,7 @@ class Network(Module):
50
50
  size_in_bytes,
51
51
  content_asset_storage_address,
52
52
  public_assertion_id,
53
- DEFAULT_HASH_FUNCTION_ID,
53
+ DefaultParameters.HASH_FUNCTION_ID.value,
54
54
  range,
55
55
  )
56
56
 
File without changes
@@ -0,0 +1,39 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ from dkg.dataclasses import NodeResponseDict
19
+ from dkg.managers.async_manager import AsyncRequestManager
20
+ from dkg.method import Method
21
+ from dkg.modules.async_module import AsyncModule
22
+ from dkg.utils.blockchain_request import BlockchainRequest
23
+ from dkg.types import Address
24
+ from dkg.services.node_services.async_node_service import AsyncNodeService
25
+
26
+
27
+ class AsyncNode(AsyncModule):
28
+ def __init__(self, manager: AsyncRequestManager, node_service: AsyncNodeService):
29
+ self.manager = manager
30
+ self.node_service = node_service
31
+
32
+ @property
33
+ async def info(self) -> NodeResponseDict:
34
+ return await self.node_service.info()
35
+
36
+ _get_identity_id = Method(BlockchainRequest.get_identity_id)
37
+
38
+ async def get_identity_id(self, operational: Address) -> int:
39
+ return await self._get_identity_id(operational)
@@ -16,9 +16,9 @@
16
16
  # under the License.
17
17
 
18
18
  from dkg.dataclasses import NodeResponseDict
19
- from dkg.manager import DefaultRequestManager
19
+ from dkg.managers.manager import DefaultRequestManager
20
20
  from dkg.method import Method
21
- from dkg.module import Module
21
+ from dkg.modules.module import Module
22
22
  from dkg.utils.node_request import NodeRequest
23
23
  from dkg.utils.blockchain_request import BlockchainRequest
24
24
  from dkg.types import Address
File without changes
@@ -27,9 +27,9 @@ from dkg.dataclasses import (
27
27
  ParanetNodesAccessPolicy,
28
28
  ParanetMinersAccessPolicy,
29
29
  )
30
- from dkg.manager import DefaultRequestManager
30
+ from dkg.managers.manager import DefaultRequestManager
31
31
  from dkg.method import Method
32
- from dkg.module import Module
32
+ from dkg.modules.module import Module
33
33
  from dkg.types import Address, UAL, HexStr
34
34
  from dkg.utils.blockchain_request import BlockchainRequest
35
35
  from dkg.utils.ual import parse_ual
@@ -755,10 +755,6 @@ class Paranet(Module):
755
755
  "operation": json.loads(Web3.to_json(receipt)),
756
756
  }
757
757
 
758
- _get_updating_knowledge_asset_states = Method(
759
- BlockchainRequest.get_updating_knowledge_asset_states
760
- )
761
-
762
758
  def _get_incentives_pool_contract(
763
759
  self,
764
760
  ual: UAL,
dkg/providers/__init__.py CHANGED
@@ -1,2 +1,9 @@
1
- from .blockchain import BlockchainProvider # NOQA
2
- from .node_http import NodeHTTPProvider # NOQA
1
+ from .blockchain import BlockchainProvider, AsyncBlockchainProvider
2
+ from .node import NodeHTTPProvider, AsyncNodeHTTPProvider
3
+
4
+ __all__ = [
5
+ "BlockchainProvider",
6
+ "AsyncBlockchainProvider",
7
+ "NodeHTTPProvider",
8
+ "AsyncNodeHTTPProvider",
9
+ ]
@@ -0,0 +1,4 @@
1
+ from .blockchain import BlockchainProvider
2
+ from .async_blockchain import AsyncBlockchainProvider
3
+
4
+ __all__ = ["BlockchainProvider", "AsyncBlockchainProvider"]
@@ -0,0 +1,245 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ import os
19
+ import asyncio
20
+ from dotenv import load_dotenv
21
+ from functools import wraps
22
+ from typing import Any
23
+
24
+ import aiohttp
25
+ from dkg.constants import BLOCKCHAINS
26
+ from dkg.exceptions import (
27
+ AccountMissing,
28
+ NetworkNotSupported,
29
+ )
30
+ from dkg.types import URI, Address, Environment, Wei
31
+ from web3.contract import Contract
32
+ from web3.contract.contract import ContractFunction
33
+ from web3.types import TxReceipt
34
+ from web3.providers import AsyncHTTPProvider
35
+ from web3 import AsyncWeb3
36
+ from dkg.providers.blockchain.base_blockchain import BaseBlockchainProvider
37
+
38
+
39
+ class AsyncBlockchainProvider(BaseBlockchainProvider):
40
+ def __init__(
41
+ self,
42
+ environment: Environment,
43
+ blockchain_id: str,
44
+ rpc_uri: URI | None = None,
45
+ gas_price: Wei | None = None,
46
+ verify: bool = True,
47
+ ):
48
+ super().__init__(environment, blockchain_id, rpc_uri, gas_price)
49
+
50
+ ssl_context = None if verify else False
51
+ self.w3 = AsyncWeb3(
52
+ AsyncHTTPProvider(self.rpc_uri, request_kwargs={"ssl": ssl_context})
53
+ )
54
+
55
+ if self.blockchain_id is None:
56
+ self.blockchain_id = f"{blockchain_id}:{self.w3.eth.chain_id}"
57
+ if self.blockchain_id not in BLOCKCHAINS[self.environment]:
58
+ raise NetworkNotSupported(
59
+ f"Network with blockchain ID {self.blockchain_id} isn't supported!"
60
+ )
61
+
62
+ self.gas_price_oracle = BLOCKCHAINS[self.environment][self.blockchain_id].get(
63
+ "gas_price_oracle",
64
+ None,
65
+ )
66
+
67
+ hub_address: Address = BLOCKCHAINS[self.environment][self.blockchain_id]["hub"]
68
+ self.contracts: dict[str, Contract] = {
69
+ "Hub": self.w3.eth.contract(
70
+ address=hub_address,
71
+ abi=self.abi["Hub"],
72
+ decode_tuples=True,
73
+ )
74
+ }
75
+
76
+ self.contracts_initialized = False
77
+
78
+ load_dotenv()
79
+ if private_key := os.environ.get("PRIVATE_KEY"):
80
+ self.set_account(private_key)
81
+
82
+ async def ensure_contracts_initialized(self):
83
+ if not self.contracts_initialized:
84
+ await self._init_contracts()
85
+ self.contracts_initialized = True
86
+
87
+ async def make_json_rpc_request(
88
+ self, endpoint: str, args: dict[str, Any] = {}
89
+ ) -> Any:
90
+ web3_method = getattr(self.w3.eth, endpoint)
91
+
92
+ if callable(web3_method):
93
+ return await web3_method(**args)
94
+ else:
95
+ return web3_method
96
+
97
+ @staticmethod
98
+ def handle_updated_contract(func):
99
+ @wraps(func)
100
+ async def wrapper(self, *args, **kwargs):
101
+ contract_name = kwargs.get("contract") or (args[0] if args else None)
102
+
103
+ try:
104
+ return await func(self, *args, **kwargs)
105
+ except Exception as err:
106
+ if (
107
+ contract_name
108
+ and isinstance(contract_name, str)
109
+ and any(msg in str(err) for msg in ["revert", "VM Exception"])
110
+ and not await self._check_contract_status(contract_name)
111
+ ):
112
+ is_updated = await self._update_contract_instance(contract_name)
113
+ if is_updated:
114
+ return await func(self, *args, **kwargs)
115
+ raise err
116
+
117
+ return wrapper
118
+
119
+ @handle_updated_contract
120
+ async def call_function(
121
+ self,
122
+ contract: str | dict[str, str],
123
+ function: str,
124
+ args: dict[str, Any] = {},
125
+ state_changing: bool = False,
126
+ gas_price: Wei | None = None,
127
+ gas_limit: Wei | None = None,
128
+ ) -> TxReceipt | Any:
129
+ await self.ensure_contracts_initialized()
130
+ if isinstance(contract, str):
131
+ contract_name = contract
132
+ contract_instance = self.contracts[contract_name]
133
+ else:
134
+ contract_name = contract["name"]
135
+ contract_instance = self.w3.eth.contract(
136
+ address=contract["address"],
137
+ abi=self.abi[contract_name],
138
+ decode_tuples=True,
139
+ )
140
+ self.contracts[contract_name] = contract_instance
141
+
142
+ contract_function: ContractFunction = getattr(
143
+ contract_instance.functions, function
144
+ )
145
+
146
+ if not state_changing:
147
+ result = await contract_function(**args).call()
148
+ if function in (
149
+ output_named_tuples := self.output_named_tuples[contract_name]
150
+ ):
151
+ result = output_named_tuples[function](*result)
152
+ return result
153
+ else:
154
+ if not hasattr(self, "account"):
155
+ raise AccountMissing(
156
+ "State-changing transactions can be performed only with specified "
157
+ "account."
158
+ )
159
+
160
+ options = {
161
+ "gas": gas_limit or await contract_function(**args).estimate_gas(),
162
+ }
163
+
164
+ gas_price = (
165
+ self.gas_price or gas_price or await self._get_network_gas_price()
166
+ )
167
+
168
+ if gas_price is not None:
169
+ options["gasPrice"] = gas_price
170
+
171
+ tx_hash = await contract_function(**args).transact(options)
172
+ tx_receipt = await self.w3.eth.wait_for_transaction_receipt(tx_hash)
173
+
174
+ return tx_receipt
175
+
176
+ async def _get_network_gas_price(self) -> Wei | None:
177
+ if self.environment == "development":
178
+ return None
179
+
180
+ async def fetch_gas_price(oracle_url: str) -> Wei | None:
181
+ try:
182
+ async with aiohttp.ClientSession() as session:
183
+ async with session.get(oracle_url) as response:
184
+ response.raise_for_status()
185
+ data: dict = await response.json()
186
+
187
+ if "result" in data:
188
+ return int(data["result"], 16)
189
+ elif "average" in data:
190
+ return self.w3.to_wei(data["average"], "gwei")
191
+ else:
192
+ return None
193
+ except Exception:
194
+ return None
195
+
196
+ oracles = self.gas_price_oracle
197
+ if oracles is not None:
198
+ if isinstance(oracles, str):
199
+ oracles = [oracles]
200
+
201
+ for oracle_url in oracles:
202
+ gas_price = await fetch_gas_price(oracle_url)
203
+ if gas_price is not None:
204
+ return gas_price
205
+
206
+ return None
207
+
208
+ async def _init_contracts(self):
209
+ init_tasks = []
210
+ for contract in self.abi.keys():
211
+ if contract == "Hub":
212
+ continue
213
+ init_tasks.append(self._update_contract_instance(contract))
214
+ await asyncio.gather(*init_tasks)
215
+
216
+ async def _update_contract_instance(self, contract: str) -> bool:
217
+ [is_contract, is_storage] = await asyncio.gather(
218
+ self.contracts["Hub"].functions.isContract(contractName=contract).call(),
219
+ self.contracts["Hub"]
220
+ .functions.isAssetStorage(assetStorageName=contract)
221
+ .call(),
222
+ )
223
+ if is_contract or is_storage:
224
+ self.contracts[contract] = self.w3.eth.contract(
225
+ address=(
226
+ await self.contracts["Hub"]
227
+ .functions.getAssetStorageAddress(contract)
228
+ .call()
229
+ if contract.endswith("AssetStorage")
230
+ or contract.endswith("CollectionStorage")
231
+ else await self.contracts["Hub"]
232
+ .functions.getContractAddress(contract)
233
+ .call()
234
+ ),
235
+ abi=self.abi[contract],
236
+ decode_tuples=True,
237
+ )
238
+ return True
239
+ return False
240
+
241
+ async def _check_contract_status(self, contract: str) -> bool:
242
+ try:
243
+ return await self.call_function(contract, "status")
244
+ except Exception:
245
+ return False