dkg 8.0.0a2__py3-none-any.whl → 8.0.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.
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 +751 -0
  27. dkg/modules/async_module.py +66 -0
  28. dkg/modules/graph/__init__.py +0 -0
  29. dkg/modules/graph/async_graph.py +118 -0
  30. dkg/modules/graph/graph.py +94 -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 -2
  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 +183 -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 +68 -42
  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 -13
  61. dkg/utils/rdf.py +9 -3
  62. {dkg-8.0.0a2.dist-info → dkg-8.0.1.dist-info}/METADATA +28 -19
  63. dkg-8.0.1.dist-info/RECORD +82 -0
  64. {dkg-8.0.0a2.dist-info → dkg-8.0.1.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.0a2.dist-info/RECORD +0 -52
  71. {dkg-8.0.0a2.dist-info → dkg-8.0.1.dist-info}/LICENSE +0 -0
  72. {dkg-8.0.0a2.dist-info → dkg-8.0.1.dist-info}/NOTICE +0 -0
@@ -0,0 +1,102 @@
1
+ import json
2
+ from collections import namedtuple
3
+ from pathlib import Path
4
+ from typing import Any, Type
5
+
6
+ from dkg.constants import BLOCKCHAINS
7
+ from dkg.exceptions import (
8
+ EnvironmentNotSupported,
9
+ RPCURINotDefined,
10
+ )
11
+ from dkg.types import URI, DataHexStr, Environment, Wei
12
+ from eth_account.signers.local import LocalAccount
13
+ from eth_typing import ABI, ABIFunction
14
+ from web3.logs import DISCARD
15
+ from web3.middleware import SignAndSendRawMiddlewareBuilder
16
+ from web3.types import TxReceipt
17
+
18
+
19
+ class BaseBlockchainProvider:
20
+ CONTRACTS_METADATA_DIR = Path(__file__).parents[2] / "data/interfaces"
21
+
22
+ def __init__(
23
+ self,
24
+ environment: Environment,
25
+ blockchain_id: str,
26
+ rpc_uri: URI | None = None,
27
+ gas_price: Wei | None = None,
28
+ ):
29
+ if environment not in BLOCKCHAINS.keys():
30
+ raise EnvironmentNotSupported(f"Environment {environment} isn't supported!")
31
+
32
+ self.environment = environment
33
+ self.rpc_uri = rpc_uri
34
+ self.blockchain_id = (
35
+ blockchain_id
36
+ if blockchain_id in BLOCKCHAINS[self.environment].keys()
37
+ else None
38
+ )
39
+
40
+ if self.rpc_uri is None and self.blockchain_id is not None:
41
+ self.blockchain_id = blockchain_id
42
+ self.rpc_uri = self.rpc_uri or BLOCKCHAINS[self.environment][
43
+ self.blockchain_id
44
+ ].get("rpc", None)
45
+
46
+ if self.rpc_uri is None:
47
+ raise RPCURINotDefined(
48
+ "No RPC URI provided for unrecognized "
49
+ f"blockchain ID {self.blockchain_id}"
50
+ )
51
+
52
+ self.gas_price = gas_price
53
+
54
+ self.abi = self._load_abi()
55
+ self.output_named_tuples = self._generate_output_named_tuples()
56
+
57
+ def _generate_output_named_tuples(self) -> dict[str, dict[str, Type[tuple]]]:
58
+ def generate_output_namedtuple(function_abi: ABIFunction) -> Type[tuple] | None:
59
+ output_names = [output["name"] for output in function_abi["outputs"]]
60
+ if all(name != "" for name in output_names):
61
+ return namedtuple(f"{function_abi['name']}Result", output_names)
62
+ return None
63
+
64
+ output_named_tuples = {}
65
+ for contract_name, contract_abi in self.abi.items():
66
+ output_named_tuples[contract_name] = {}
67
+ for item in contract_abi:
68
+ if (item["type"] != "function") or not item["outputs"]:
69
+ continue
70
+ elif item["name"] in output_named_tuples[contract_name]:
71
+ continue
72
+ named_tuple = generate_output_namedtuple(item)
73
+ if named_tuple is not None:
74
+ output_named_tuples[contract_name][item["name"]] = named_tuple
75
+
76
+ return output_named_tuples
77
+
78
+ def _load_abi(self) -> ABI:
79
+ abi = {}
80
+
81
+ for contract_metadata in self.CONTRACTS_METADATA_DIR.glob("*.json"):
82
+ with open(contract_metadata, "r") as metadata_json:
83
+ abi[contract_metadata.stem] = json.load(metadata_json)
84
+
85
+ return abi
86
+
87
+ def decode_logs_event(
88
+ self, receipt: TxReceipt, contract_name: str, event_name: str
89
+ ) -> Any:
90
+ return (
91
+ self.contracts[contract_name]
92
+ .events[event_name]()
93
+ .process_receipt(receipt, errors=DISCARD)
94
+ )
95
+
96
+ def set_account(self, private_key: DataHexStr):
97
+ self.account: LocalAccount = self.w3.eth.account.from_key(private_key)
98
+ self.w3.middleware_onion.inject(
99
+ SignAndSendRawMiddlewareBuilder.build(private_key),
100
+ layer=0,
101
+ )
102
+ self.w3.eth.default_account = self.account.address
@@ -15,66 +15,35 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
- import json
19
18
  import os
20
- from collections import namedtuple
19
+ from dotenv import load_dotenv
21
20
  from functools import wraps
22
- from pathlib import Path
23
- from typing import Any, Type
21
+ from typing import Any
24
22
 
25
23
  import requests
26
24
  from dkg.constants import BLOCKCHAINS
27
25
  from dkg.exceptions import (
28
26
  AccountMissing,
29
- EnvironmentNotSupported,
30
27
  NetworkNotSupported,
31
- RPCURINotDefined,
32
28
  )
33
- from dkg.types import URI, Address, DataHexStr, Environment, Wei
34
- from eth_account.signers.local import LocalAccount
35
- from eth_typing import ABI, ABIFunction
29
+ from dkg.types import URI, Address, Environment, Wei
36
30
  from web3 import Web3
37
31
  from web3.contract import Contract
38
32
  from web3.contract.contract import ContractFunction
39
- from web3.logs import DISCARD
40
- from web3.middleware import SignAndSendRawMiddlewareBuilder
41
33
  from web3.types import TxReceipt
34
+ from dkg.providers.blockchain.base_blockchain import BaseBlockchainProvider
42
35
 
43
36
 
44
- class BlockchainProvider:
45
- CONTRACTS_METADATA_DIR = Path(__file__).parents[1] / "data/interfaces"
46
-
37
+ class BlockchainProvider(BaseBlockchainProvider):
47
38
  def __init__(
48
39
  self,
49
40
  environment: Environment,
50
41
  blockchain_id: str,
51
42
  rpc_uri: URI | None = None,
52
- private_key: DataHexStr | None = None,
53
43
  gas_price: Wei | None = None,
54
44
  verify: bool = True,
55
45
  ):
56
- if environment not in BLOCKCHAINS.keys():
57
- raise EnvironmentNotSupported(f"Environment {environment} isn't supported!")
58
-
59
- self.environment = environment
60
- self.rpc_uri = rpc_uri
61
- self.blockchain_id = (
62
- blockchain_id
63
- if blockchain_id in BLOCKCHAINS[self.environment].keys()
64
- else None
65
- )
66
-
67
- if self.rpc_uri is None and self.blockchain_id is not None:
68
- self.blockchain_id = blockchain_id
69
- self.rpc_uri = self.rpc_uri or BLOCKCHAINS[self.environment][
70
- self.blockchain_id
71
- ].get("rpc", None)
72
-
73
- if self.rpc_uri is None:
74
- raise RPCURINotDefined(
75
- "No RPC URI provided for unrecognized "
76
- f"blockchain ID {self.blockchain_id}"
77
- )
46
+ super().__init__(environment, blockchain_id, rpc_uri, gas_price)
78
47
 
79
48
  self.w3 = Web3(
80
49
  Web3.HTTPProvider(self.rpc_uri, request_kwargs={"verify": verify})
@@ -87,15 +56,11 @@ class BlockchainProvider:
87
56
  f"Network with blockchain ID {self.blockchain_id} isn't supported!"
88
57
  )
89
58
 
90
- self.gas_price = gas_price
91
59
  self.gas_price_oracle = BLOCKCHAINS[self.environment][self.blockchain_id].get(
92
60
  "gas_price_oracle",
93
61
  None,
94
62
  )
95
63
 
96
- self.abi = self._load_abi()
97
- self.output_named_tuples = self._generate_output_named_tuples()
98
-
99
64
  hub_address: Address = BLOCKCHAINS[self.environment][self.blockchain_id]["hub"]
100
65
  self.contracts: dict[str, Contract] = {
101
66
  "Hub": self.w3.eth.contract(
@@ -106,11 +71,9 @@ class BlockchainProvider:
106
71
  }
107
72
  self._init_contracts()
108
73
 
109
- if (
110
- private_key is not None
111
- or (private_key_env := os.environ.get("PRIVATE_KEY", None)) is not None
112
- ):
113
- self.set_account(private_key or private_key_env)
74
+ load_dotenv()
75
+ if private_key := os.environ.get("PRIVATE_KEY"):
76
+ self.set_account(private_key)
114
77
 
115
78
  def make_json_rpc_request(self, endpoint: str, args: dict[str, Any] = {}) -> Any:
116
79
  web3_method = getattr(self.w3.eth, endpoint)
@@ -196,23 +159,6 @@ class BlockchainProvider:
196
159
 
197
160
  return tx_receipt
198
161
 
199
- def decode_logs_event(
200
- self, receipt: TxReceipt, contract_name: str, event_name: str
201
- ) -> Any:
202
- return (
203
- self.contracts[contract_name]
204
- .events[event_name]()
205
- .process_receipt(receipt, errors=DISCARD)
206
- )
207
-
208
- def set_account(self, private_key: DataHexStr):
209
- self.account: LocalAccount = self.w3.eth.account.from_key(private_key)
210
- self.w3.middleware_onion.inject(
211
- SignAndSendRawMiddlewareBuilder.build(private_key),
212
- layer=0,
213
- )
214
- self.w3.eth.default_account = self.account.address
215
-
216
162
  def _get_network_gas_price(self) -> Wei | None:
217
163
  if self.environment == "development":
218
164
  return None
@@ -260,11 +206,14 @@ class BlockchainProvider:
260
206
  ):
261
207
  self.contracts[contract] = self.w3.eth.contract(
262
208
  address=(
263
- self.contracts["Hub"].functions.getContractAddress(contract).call()
264
- if not contract.endswith("AssetStorage")
265
- else self.contracts["Hub"]
209
+ self.contracts["Hub"]
266
210
  .functions.getAssetStorageAddress(contract)
267
211
  .call()
212
+ if contract.endswith("AssetStorage")
213
+ or contract.endswith("CollectionStorage")
214
+ else self.contracts["Hub"]
215
+ .functions.getContractAddress(contract)
216
+ .call()
268
217
  ),
269
218
  abi=self.abi[contract],
270
219
  decode_tuples=True,
@@ -277,33 +226,3 @@ class BlockchainProvider:
277
226
  return self.call_function(contract, "status")
278
227
  except Exception:
279
228
  return False
280
-
281
- def _generate_output_named_tuples(self) -> dict[str, dict[str, Type[tuple]]]:
282
- def generate_output_namedtuple(function_abi: ABIFunction) -> Type[tuple] | None:
283
- output_names = [output["name"] for output in function_abi["outputs"]]
284
- if all(name != "" for name in output_names):
285
- return namedtuple(f"{function_abi['name']}Result", output_names)
286
- return None
287
-
288
- output_named_tuples = {}
289
- for contract_name, contract_abi in self.abi.items():
290
- output_named_tuples[contract_name] = {}
291
- for item in contract_abi:
292
- if (item["type"] != "function") or not item["outputs"]:
293
- continue
294
- elif item["name"] in output_named_tuples[contract_name]:
295
- continue
296
- named_tuple = generate_output_namedtuple(item)
297
- if named_tuple is not None:
298
- output_named_tuples[contract_name][item["name"]] = named_tuple
299
-
300
- return output_named_tuples
301
-
302
- def _load_abi(self) -> ABI:
303
- abi = {}
304
-
305
- for contract_metadata in self.CONTRACTS_METADATA_DIR.glob("*.json"):
306
- with open(contract_metadata, "r") as metadata_json:
307
- abi[contract_metadata.stem] = json.load(metadata_json)
308
-
309
- return abi
@@ -0,0 +1,4 @@
1
+ from .node_http import NodeHTTPProvider
2
+ from .async_node_http import AsyncNodeHTTPProvider
3
+
4
+ __all__ = ["NodeHTTPProvider", "AsyncNodeHTTPProvider"]
@@ -0,0 +1,72 @@
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 typing import Any
19
+
20
+ import aiohttp
21
+ from dkg.dataclasses import HTTPRequestMethod, NodeResponseDict
22
+ from dkg.exceptions import HTTPRequestMethodNotSupported, NodeRequestError
23
+ from dkg.types import URI
24
+ from dkg.providers.node.base_node_http import BaseNodeHTTPProvider
25
+
26
+
27
+ class AsyncNodeHTTPProvider(BaseNodeHTTPProvider):
28
+ def __init__(
29
+ self,
30
+ endpoint_uri: URI | str,
31
+ api_version: str = "v1",
32
+ auth_token: str | None = None,
33
+ ):
34
+ super().__init__(endpoint_uri, api_version, auth_token)
35
+
36
+ async def make_request(
37
+ self,
38
+ method: HTTPRequestMethod,
39
+ path: str,
40
+ params: dict[str, Any] = {},
41
+ data: dict[str, Any] = {},
42
+ ) -> NodeResponseDict:
43
+ url = f"{self.url}/{path}"
44
+
45
+ async with aiohttp.ClientSession() as session:
46
+ try:
47
+ if method == HTTPRequestMethod.GET:
48
+ async with session.get(
49
+ url, params=params, headers=self.headers
50
+ ) as response:
51
+ response.raise_for_status()
52
+ response = await response.json()
53
+ elif method == HTTPRequestMethod.POST:
54
+ async with session.post(
55
+ url, json=data, headers=self.headers
56
+ ) as response:
57
+ response.raise_for_status()
58
+ response = await response.json()
59
+ else:
60
+ raise HTTPRequestMethodNotSupported(
61
+ f"{method.name} method isn't supported"
62
+ )
63
+
64
+ try:
65
+ return NodeResponseDict(response)
66
+ except ValueError as err:
67
+ raise NodeRequestError(f"JSON decoding failed: {err}")
68
+
69
+ except aiohttp.ClientError as err:
70
+ raise NodeRequestError(f"Network error: {err}")
71
+ except Exception as err:
72
+ raise NodeRequestError(f"Unexpected error: {err}")
@@ -0,0 +1,25 @@
1
+ from abc import ABC, abstractmethod
2
+ from dkg.dataclasses import HTTPRequestMethod, NodeResponseDict
3
+ from typing import Any
4
+ from dkg.types import URI
5
+
6
+
7
+ class BaseNodeHTTPProvider(ABC):
8
+ def __init__(
9
+ self,
10
+ endpoint_uri: URI | str,
11
+ api_version: str = "v1",
12
+ auth_token: str | None = None,
13
+ ):
14
+ self.url = f"{URI(endpoint_uri)}/{api_version}"
15
+ self.headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else {}
16
+
17
+ @abstractmethod
18
+ async def make_request(
19
+ self,
20
+ method: HTTPRequestMethod,
21
+ path: str,
22
+ params: dict[str, Any] = {},
23
+ data: dict[str, Any] = {},
24
+ ) -> NodeResponseDict:
25
+ raise NotImplementedError("Subclasses must implement make_request")
@@ -22,12 +22,17 @@ from dkg.dataclasses import HTTPRequestMethod, NodeResponseDict
22
22
  from dkg.exceptions import HTTPRequestMethodNotSupported, NodeRequestError
23
23
  from dkg.types import URI
24
24
  from requests.exceptions import HTTPError, ConnectionError, Timeout, RequestException
25
+ from dkg.providers.node.base_node_http import BaseNodeHTTPProvider
25
26
 
26
27
 
27
- class NodeHTTPProvider:
28
- def __init__(self, endpoint_uri: URI | str, auth_token: str | None = None):
29
- self.endpoint_uri = URI(endpoint_uri)
30
- self.auth_token = auth_token
28
+ class NodeHTTPProvider(BaseNodeHTTPProvider):
29
+ def __init__(
30
+ self,
31
+ endpoint_uri: URI | str,
32
+ api_version: str = "v1",
33
+ auth_token: str | None = None,
34
+ ):
35
+ super().__init__(endpoint_uri, api_version, auth_token)
31
36
 
32
37
  def make_request(
33
38
  self,
@@ -36,16 +41,13 @@ class NodeHTTPProvider:
36
41
  params: dict[str, Any] = {},
37
42
  data: dict[str, Any] = {},
38
43
  ) -> NodeResponseDict:
39
- url = f"{self.endpoint_uri}/{path}"
40
- headers = (
41
- {"Authorization": f"Bearer {self.auth_token}"} if self.auth_token else {}
42
- )
44
+ url = f"{self.url}/{path}"
43
45
 
44
46
  try:
45
47
  if method == HTTPRequestMethod.GET:
46
- response = requests.get(url, params=params, headers=headers)
48
+ response = requests.get(url, params=params, headers=self.headers)
47
49
  elif method == HTTPRequestMethod.POST:
48
- response = requests.post(url, json=data, headers=headers)
50
+ response = requests.post(url, json=data, headers=self.headers)
49
51
  else:
50
52
  raise HTTPRequestMethodNotSupported(
51
53
  f"{method.name} method isn't supported"
File without changes
File without changes
@@ -0,0 +1,180 @@
1
+ from dkg.modules.async_module import AsyncModule
2
+ from dkg.managers.async_manager import AsyncRequestManager
3
+ from dkg.utils.blockchain_request import BlockchainRequest
4
+ from dkg.method import Method
5
+ from dkg.constants import ZERO_ADDRESS
6
+ from web3 import Web3
7
+ from typing import Optional
8
+ from dkg.types import Address, UAL
9
+ from dkg.utils.blockchain_request import KnowledgeCollectionResult, AllowanceResult
10
+ from dkg.utils.ual import parse_ual
11
+
12
+
13
+ class AsyncBlockchainService(AsyncModule):
14
+ def __init__(self, manager: AsyncRequestManager):
15
+ self.manager = manager
16
+
17
+ _owner = Method(BlockchainRequest.owner_of)
18
+ _get_contract_address = Method(BlockchainRequest.get_contract_address)
19
+ _get_current_allowance = Method(BlockchainRequest.allowance)
20
+ _increase_allowance = Method(BlockchainRequest.increase_allowance)
21
+ _decrease_allowance = Method(BlockchainRequest.decrease_allowance)
22
+ _create_knowledge_collection = Method(BlockchainRequest.create_knowledge_collection)
23
+ _mint_knowledge_asset = Method(BlockchainRequest.mint_knowledge_asset)
24
+ _get_asset_storage_address = Method(BlockchainRequest.get_asset_storage_address)
25
+ _key_is_operational_wallet = Method(BlockchainRequest.key_is_operational_wallet)
26
+ _time_until_next_epoch = Method(BlockchainRequest.time_until_next_epoch)
27
+ _epoch_length = Method(BlockchainRequest.epoch_length)
28
+ _get_stake_weighted_average_ask = Method(
29
+ BlockchainRequest.get_stake_weighted_average_ask
30
+ )
31
+ _get_block = Method(BlockchainRequest.get_block)
32
+
33
+ async def decrease_knowledge_collection_allowance(
34
+ self,
35
+ allowance_gap: int,
36
+ ):
37
+ knowledge_collection_address = await self._get_contract_address(
38
+ "KnowledgeCollection"
39
+ )
40
+ await self._decrease_allowance(knowledge_collection_address, allowance_gap)
41
+
42
+ async def increase_knowledge_collection_allowance(
43
+ self,
44
+ sender: str,
45
+ token_amount: str,
46
+ ) -> AllowanceResult:
47
+ """
48
+ Increases the allowance for knowledge collection if necessary.
49
+
50
+ Args:
51
+ sender: The address of the sender
52
+ token_amount: The amount of tokens to check/increase allowance for
53
+
54
+ Returns:
55
+ AllowanceResult containing whether allowance was increased and the gap
56
+ """
57
+ knowledge_collection_address = await self._get_contract_address(
58
+ "KnowledgeCollection"
59
+ )
60
+
61
+ allowance = await self._get_current_allowance(
62
+ sender, knowledge_collection_address
63
+ )
64
+ allowance_gap = int(token_amount) - int(allowance)
65
+
66
+ if allowance_gap > 0:
67
+ await self._increase_allowance(knowledge_collection_address, allowance_gap)
68
+
69
+ return AllowanceResult(
70
+ allowance_increased=True, allowance_gap=allowance_gap
71
+ )
72
+
73
+ return AllowanceResult(allowance_increased=False, allowance_gap=allowance_gap)
74
+
75
+ async def create_knowledge_collection(
76
+ self,
77
+ request: dict,
78
+ paranet_ka_contract: Optional[Address] = None,
79
+ paranet_token_id: Optional[int] = None,
80
+ ) -> KnowledgeCollectionResult:
81
+ """
82
+ Creates a knowledge collection on the blockchain.
83
+
84
+ Args:
85
+ request: dict containing all collection parameters
86
+ paranet_ka_contract: Optional paranet contract address
87
+ paranet_token_id: Optional paranet token ID
88
+ blockchain: Blockchain configuration
89
+
90
+ Returns:
91
+ KnowledgeCollectionResult containing collection ID and transaction receipt
92
+
93
+ Raises:
94
+ BlockchainError: If the collection creation fails
95
+ """
96
+ sender = self.manager.blockchain_provider.account.address
97
+ allowance_increased = False
98
+ allowance_gap = 0
99
+
100
+ try:
101
+ # Handle allowance
102
+ if request.get("paymaster") and request.get("paymaster") != ZERO_ADDRESS:
103
+ pass
104
+ else:
105
+ allowance_result = await self.increase_knowledge_collection_allowance(
106
+ sender=sender,
107
+ token_amount=request.get("tokenAmount"),
108
+ )
109
+ allowance_increased = allowance_result.allowance_increased
110
+ allowance_gap = allowance_result.allowance_gap
111
+
112
+ if not paranet_ka_contract and not paranet_token_id:
113
+ receipt = await self._create_knowledge_collection(
114
+ request.get("publishOperationId"),
115
+ Web3.to_bytes(hexstr=request.get("merkleRoot")),
116
+ request.get("knowledgeAssetsAmount"),
117
+ request.get("byteSize"),
118
+ request.get("epochs"),
119
+ request.get("tokenAmount"),
120
+ request.get("isImmutable"),
121
+ request.get("paymaster"),
122
+ request.get("publisherNodeIdentityId"),
123
+ Web3.to_bytes(hexstr=request.get("publisherNodeR")),
124
+ Web3.to_bytes(hexstr=request.get("publisherNodeVS")),
125
+ request.get("identityIds"),
126
+ [Web3.to_bytes(hexstr=x) for x in request.get("r")],
127
+ [Web3.to_bytes(hexstr=x) for x in request.get("vs")],
128
+ )
129
+ else:
130
+ receipt = await self._mint_knowledge_asset(
131
+ paranet_ka_contract,
132
+ paranet_token_id,
133
+ list(request.values()),
134
+ )
135
+
136
+ event_data = self.manager.blockchain_provider.decode_logs_event(
137
+ receipt=receipt,
138
+ contract_name="KnowledgeCollectionStorage",
139
+ event_name="KnowledgeCollectionCreated",
140
+ )
141
+ collection_id = (
142
+ int(getattr(event_data[0].get("args", {}), "id", None))
143
+ if event_data
144
+ else None
145
+ )
146
+
147
+ return KnowledgeCollectionResult(
148
+ knowledge_collection_id=collection_id, receipt=receipt
149
+ )
150
+
151
+ except Exception as e:
152
+ if allowance_increased:
153
+ await self.decrease_knowledge_collection_allowance(allowance_gap)
154
+ raise e
155
+
156
+ # TODO: change self._owner to v8 compatible function
157
+ async def get_owner(self, ual: UAL) -> Address:
158
+ token_id = parse_ual(ual)["token_id"]
159
+
160
+ return await self._owner(token_id)
161
+
162
+ async def get_asset_storage_address(self, asset_storage_name: str) -> Address:
163
+ return await self._get_asset_storage_address(asset_storage_name)
164
+
165
+ async def key_is_operational_wallet(
166
+ self, identity_id: int, key: Address, purpose: int
167
+ ) -> bool:
168
+ return await self._key_is_operational_wallet(identity_id, key, purpose)
169
+
170
+ async def time_until_next_epoch(self) -> int:
171
+ return await self._time_until_next_epoch()
172
+
173
+ async def epoch_length(self) -> int:
174
+ return await self._epoch_length()
175
+
176
+ async def get_stake_weighted_average_ask(self) -> int:
177
+ return await self._get_stake_weighted_average_ask()
178
+
179
+ async def get_block(self, block_identifier: str | int):
180
+ return await self._get_block(block_identifier)