dkg 0.1.0b1__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 (62) hide show
  1. dkg/__init__.py +3 -0
  2. dkg/asset.py +781 -0
  3. dkg/constants.py +39 -0
  4. dkg/data/interfaces/Assertion.json +131 -0
  5. dkg/data/interfaces/AssertionStorage.json +229 -0
  6. dkg/data/interfaces/CommitManagerV1.json +534 -0
  7. dkg/data/interfaces/CommitManagerV1U1.json +720 -0
  8. dkg/data/interfaces/ContentAsset.json +671 -0
  9. dkg/data/interfaces/ContentAssetStorage.json +706 -0
  10. dkg/data/interfaces/HashingProxy.json +227 -0
  11. dkg/data/interfaces/Hub.json +356 -0
  12. dkg/data/interfaces/Identity.json +193 -0
  13. dkg/data/interfaces/IdentityStorage.json +342 -0
  14. dkg/data/interfaces/ParametersStorage.json +468 -0
  15. dkg/data/interfaces/Profile.json +292 -0
  16. dkg/data/interfaces/ProfileStorage.json +596 -0
  17. dkg/data/interfaces/ProofManagerV1.json +525 -0
  18. dkg/data/interfaces/ProofManagerV1U1.json +546 -0
  19. dkg/data/interfaces/ScoringProxy.json +242 -0
  20. dkg/data/interfaces/ServiceAgreementStorageProxy.json +1299 -0
  21. dkg/data/interfaces/ServiceAgreementStorageV1.json +901 -0
  22. dkg/data/interfaces/ServiceAgreementStorageV1U1.json +1097 -0
  23. dkg/data/interfaces/ServiceAgreementV1.json +741 -0
  24. dkg/data/interfaces/ShardingTable.json +268 -0
  25. dkg/data/interfaces/ShardingTableStorage.json +317 -0
  26. dkg/data/interfaces/Staking.json +456 -0
  27. dkg/data/interfaces/StakingStorage.json +407 -0
  28. dkg/data/interfaces/Token.json +544 -0
  29. dkg/data/interfaces/UnfinalizedStateStorage.json +171 -0
  30. dkg/data/interfaces/WhitelistStorage.json +124 -0
  31. dkg/dataclasses.py +45 -0
  32. dkg/exceptions.py +161 -0
  33. dkg/graph.py +63 -0
  34. dkg/main.py +74 -0
  35. dkg/manager.py +64 -0
  36. dkg/method.py +131 -0
  37. dkg/module.py +63 -0
  38. dkg/node.py +54 -0
  39. dkg/providers/__init__.py +2 -0
  40. dkg/providers/blockchain.py +181 -0
  41. dkg/providers/node_http.py +62 -0
  42. dkg/types/__init__.py +8 -0
  43. dkg/types/blockchain.py +58 -0
  44. dkg/types/dkg_node.py +20 -0
  45. dkg/types/encoding.py +22 -0
  46. dkg/types/evm.py +25 -0
  47. dkg/types/generics.py +21 -0
  48. dkg/types/network.py +20 -0
  49. dkg/types/rdf.py +21 -0
  50. dkg/utils/__init__.py +0 -0
  51. dkg/utils/blockchain_request.py +159 -0
  52. dkg/utils/decorators.py +46 -0
  53. dkg/utils/merkle.py +173 -0
  54. dkg/utils/metadata.py +50 -0
  55. dkg/utils/node_request.py +197 -0
  56. dkg/utils/rdf.py +51 -0
  57. dkg/utils/string_transformations.py +22 -0
  58. dkg/utils/ual.py +41 -0
  59. dkg-0.1.0b1.dist-info/LICENSE +202 -0
  60. dkg-0.1.0b1.dist-info/METADATA +453 -0
  61. dkg-0.1.0b1.dist-info/RECORD +62 -0
  62. dkg-0.1.0b1.dist-info/WHEEL +4 -0
dkg/asset.py ADDED
@@ -0,0 +1,781 @@
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 math
19
+ import re
20
+ from typing import Literal, Type
21
+
22
+ from pyld import jsonld
23
+ from web3 import Web3
24
+ from web3.constants import HASH_ZERO
25
+ from web3.exceptions import ContractLogicError
26
+
27
+ from dkg.constants import BLOCKCHAINS, PRIVATE_ASSERTION_PREDICATE
28
+ from dkg.dataclasses import (KnowledgeAssetContentVisibility,
29
+ KnowledgeAssetEnumStates, NodeResponseDict)
30
+ from dkg.exceptions import (DatasetOutputFormatNotSupported,
31
+ InvalidKnowledgeAsset, InvalidStateOption,
32
+ InvalidTokenAmount, MissingKnowledgeAssetState,
33
+ OperationNotFinished)
34
+ from dkg.manager import DefaultRequestManager
35
+ from dkg.method import Method
36
+ from dkg.module import Module
37
+ from dkg.types import JSONLD, UAL, Address, AgreementData, HexStr, NQuads
38
+ from dkg.utils.blockchain_request import BlockchainRequest
39
+ from dkg.utils.decorators import retry
40
+ from dkg.utils.merkle import MerkleTree, hash_assertion_with_indexes
41
+ from dkg.utils.metadata import (generate_agreement_id,
42
+ generate_assertion_metadata, generate_keyword)
43
+ from dkg.utils.node_request import (NodeRequest, StoreTypes,
44
+ validate_operation_status)
45
+ from dkg.utils.rdf import normalize_dataset
46
+ from dkg.utils.ual import format_ual, parse_ual
47
+
48
+
49
+ class ContentAsset(Module):
50
+ DEFAULT_HASH_FUNCTION_ID = 1
51
+ DEFAULT_SCORE_FUNCTION_ID = 1
52
+ PRIVATE_HISTORICAL_REPOSITORY = "privateHistory"
53
+ PRIVATE_CURRENT_REPOSITORY = "privateCurrent"
54
+
55
+ def __init__(self, manager: DefaultRequestManager):
56
+ self.manager = manager
57
+
58
+ _chain_id = Method(BlockchainRequest.chain_id)
59
+
60
+ _get_contract_address = Method(BlockchainRequest.get_contract_address)
61
+ _get_asset_storage_address = Method(BlockchainRequest.get_asset_storage_address)
62
+ _increase_allowance = Method(BlockchainRequest.increase_allowance)
63
+ _decrease_allowance = Method(BlockchainRequest.decrease_allowance)
64
+ _create = Method(BlockchainRequest.create_asset)
65
+
66
+ _get_bid_suggestion = Method(NodeRequest.bid_suggestion)
67
+ _local_store = Method(NodeRequest.local_store)
68
+ _publish = Method(NodeRequest.publish)
69
+
70
+ def create(
71
+ self,
72
+ content: dict[Literal["public", "private"], JSONLD],
73
+ epochs_number: int,
74
+ token_amount: int | None = None,
75
+ immutable: bool = False,
76
+ content_type: Literal["JSON-LD", "N-Quads"] = "JSON-LD",
77
+ ) -> dict[str, HexStr | dict[str, str]]:
78
+ assertions = self._process_content(content, content_type)
79
+
80
+ chain_name = BLOCKCHAINS[self._chain_id()]["name"]
81
+ content_asset_storage_address = self._get_asset_storage_address(
82
+ "ContentAssetStorage"
83
+ )
84
+
85
+ if token_amount is None:
86
+ token_amount = int(
87
+ self._get_bid_suggestion(
88
+ chain_name,
89
+ epochs_number,
90
+ assertions["public"]["size"],
91
+ content_asset_storage_address,
92
+ assertions["public"]["id"],
93
+ self.DEFAULT_HASH_FUNCTION_ID,
94
+ )["bidSuggestion"]
95
+ )
96
+
97
+ service_agreement_v1_address = str(
98
+ self._get_contract_address("ServiceAgreementV1")
99
+ )
100
+ self._increase_allowance(service_agreement_v1_address, token_amount)
101
+
102
+ try:
103
+ receipt = self._create(
104
+ {
105
+ "assertionId": Web3.to_bytes(hexstr=assertions["public"]["id"]),
106
+ "size": assertions["public"]["size"],
107
+ "triplesNumber": assertions["public"]["triples_number"],
108
+ "chunksNumber": assertions["public"]["chunks_number"],
109
+ "tokenAmount": token_amount,
110
+ "epochsNumber": epochs_number,
111
+ "scoreFunctionId": self.DEFAULT_SCORE_FUNCTION_ID,
112
+ "immutable_": immutable,
113
+ }
114
+ )
115
+ except ContractLogicError as err:
116
+ self._decrease_allowance(service_agreement_v1_address, token_amount)
117
+ raise err
118
+
119
+ events = self.manager.blockchain_provider.decode_logs_event(
120
+ receipt,
121
+ "ContentAsset",
122
+ "AssetMinted",
123
+ )
124
+ token_id = events[0].args["tokenId"]
125
+
126
+ assertions_list = [
127
+ {
128
+ "blockchain": chain_name,
129
+ "contract": content_asset_storage_address,
130
+ "tokenId": token_id,
131
+ "assertionId": assertions["public"]["id"],
132
+ "assertion": assertions["public"]["content"],
133
+ "storeType": StoreTypes.TRIPLE.value,
134
+ }
135
+ ]
136
+
137
+ if content.get("private", None):
138
+ assertions_list.append(
139
+ {
140
+ "blockchain": chain_name,
141
+ "contract": content_asset_storage_address,
142
+ "tokenId": token_id,
143
+ "assertionId": assertions["private"]["id"],
144
+ "assertion": assertions["private"]["content"],
145
+ "storeType": StoreTypes.TRIPLE.value,
146
+ }
147
+ )
148
+
149
+ operation_id = self._local_store(assertions_list)["operationId"]
150
+ self.get_operation_result(operation_id, "local-store")
151
+
152
+ operation_id = self._publish(
153
+ assertions["public"]["id"],
154
+ assertions["public"]["content"],
155
+ chain_name,
156
+ content_asset_storage_address,
157
+ token_id,
158
+ self.DEFAULT_HASH_FUNCTION_ID,
159
+ )["operationId"]
160
+ operation_result = self.get_operation_result(operation_id, "publish")
161
+
162
+ return {
163
+ "UAL": format_ual(chain_name, content_asset_storage_address, token_id),
164
+ "publicAssertionId": assertions["public"]["id"],
165
+ "operation": {
166
+ "operationId": operation_id,
167
+ "status": operation_result["status"],
168
+ },
169
+ }
170
+
171
+ _transfer = Method(BlockchainRequest.transfer_asset)
172
+
173
+ def transfer(
174
+ self,
175
+ ual: UAL,
176
+ new_owner: Address,
177
+ ) -> dict[str, UAL | Address | dict[str, str]]:
178
+ token_id = parse_ual(ual)["token_id"]
179
+
180
+ self._transfer(
181
+ self.manager.blockchain_provider.account,
182
+ new_owner,
183
+ token_id,
184
+ )
185
+
186
+ return {
187
+ "UAL": ual,
188
+ "owner": new_owner,
189
+ "operation": {"status": "COMPLETED"},
190
+ }
191
+
192
+ _update = Method(NodeRequest.update)
193
+
194
+ _get_block = Method(BlockchainRequest.get_block)
195
+
196
+ _get_service_agreement_data = Method(BlockchainRequest.get_service_agreement_data)
197
+ _update_asset_state = Method(BlockchainRequest.update_asset_state)
198
+
199
+ def update(
200
+ self,
201
+ ual: UAL,
202
+ content: dict[Literal["public", "private"], JSONLD],
203
+ token_amount: int | None = None,
204
+ content_type: Literal["JSON-LD", "N-Quads"] = "JSON-LD",
205
+ ) -> dict[str, HexStr | dict[str, str]]:
206
+ parsed_ual = parse_ual(ual)
207
+ content_asset_storage_address, token_id = (
208
+ parsed_ual["contract_address"],
209
+ parsed_ual["token_id"],
210
+ )
211
+
212
+ assertions = self._process_content(content, content_type)
213
+
214
+ chain_name = BLOCKCHAINS[self._chain_id()]["name"]
215
+
216
+ if token_amount is None:
217
+ agreement_id = self.get_agreement_id(
218
+ content_asset_storage_address, token_id
219
+ )
220
+ # TODO: Dynamic types for namedtuples?
221
+ agreement_data: Type[AgreementData] = self._get_service_agreement_data(
222
+ agreement_id
223
+ )
224
+
225
+ timestamp_now = self._get_block("latest")["timestamp"]
226
+ current_epoch = math.floor(
227
+ (timestamp_now - agreement_data.startTime) / agreement_data.epochLength
228
+ )
229
+ epochs_left = agreement_data.epochsNumber - current_epoch
230
+
231
+ token_amount = int(
232
+ self._get_bid_suggestion(
233
+ chain_name,
234
+ epochs_left,
235
+ assertions["public"]["size"],
236
+ content_asset_storage_address,
237
+ assertions["public"]["id"],
238
+ self.DEFAULT_HASH_FUNCTION_ID,
239
+ )["bidSuggestion"]
240
+ )
241
+
242
+ token_amount -= agreement_data.tokensInfo[0]
243
+ token_amount = token_amount if token_amount > 0 else 0
244
+
245
+ self._update_asset_state(
246
+ token_id=token_id,
247
+ assertion_id=assertions["public"]["id"],
248
+ size=assertions["public"]["size"],
249
+ triples_number=assertions["public"]["triples_number"],
250
+ chunks_number=assertions["public"]["chunks_number"],
251
+ update_token_amount=token_amount,
252
+ )
253
+
254
+ assertions_list = [
255
+ {
256
+ "blockchain": chain_name,
257
+ "contract": content_asset_storage_address,
258
+ "tokenId": token_id,
259
+ "assertionId": assertions["public"]["id"],
260
+ "assertion": assertions["public"]["content"],
261
+ "storeType": StoreTypes.PENDING.value,
262
+ }
263
+ ]
264
+
265
+ if content.get("private", None):
266
+ assertions_list.append(
267
+ {
268
+ "blockchain": chain_name,
269
+ "contract": content_asset_storage_address,
270
+ "tokenId": token_id,
271
+ "assertionId": assertions["private"]["id"],
272
+ "assertion": assertions["private"]["content"],
273
+ "storeType": StoreTypes.PENDING.value,
274
+ }
275
+ )
276
+
277
+ operation_id = self._local_store(assertions_list)["operationId"]
278
+ self.get_operation_result(operation_id, "local-store")
279
+
280
+ operation_id = self._update(
281
+ assertions["public"]["id"],
282
+ assertions["public"]["content"],
283
+ chain_name,
284
+ content_asset_storage_address,
285
+ token_id,
286
+ self.DEFAULT_HASH_FUNCTION_ID,
287
+ )["operationId"]
288
+ operation_result = self.get_operation_result(operation_id, "update")
289
+
290
+ return {
291
+ "UAL": format_ual(chain_name, content_asset_storage_address, token_id),
292
+ "publicAssertionId": assertions["public"]["id"],
293
+ "operation": {
294
+ "operationId": operation_id,
295
+ "status": operation_result["status"],
296
+ },
297
+ }
298
+
299
+ _cancel_update = Method(BlockchainRequest.cancel_asset_state_update)
300
+
301
+ def cancel_update(self, ual: UAL) -> dict[str, UAL | dict[str, str]]:
302
+ token_id = parse_ual(ual)["token_id"]
303
+
304
+ self._cancel_update(token_id)
305
+
306
+ return {
307
+ "UAL": ual,
308
+ "operation": {"status": "COMPLETED"},
309
+ }
310
+
311
+ _burn_asset = Method(BlockchainRequest.burn_asset)
312
+
313
+ def burn(self, ual: UAL) -> dict[str, UAL | dict[str, str]]:
314
+ token_id = parse_ual(ual)["token_id"]
315
+
316
+ self._burn_asset(token_id)
317
+
318
+ return {"UAL": ual, "operation": {"status": "COMPLETED"}}
319
+
320
+ _get_assertion_ids = Method(BlockchainRequest.get_assertion_ids)
321
+ _get_latest_assertion_id = Method(BlockchainRequest.get_latest_assertion_id)
322
+ _get_unfinalized_state = Method(BlockchainRequest.get_unfinalized_state)
323
+
324
+ _get = Method(NodeRequest.get)
325
+ _query = Method(NodeRequest.query)
326
+
327
+ def get(
328
+ self,
329
+ ual: UAL,
330
+ state: str | HexStr | int = KnowledgeAssetEnumStates.LATEST.value,
331
+ content_visibility: str = KnowledgeAssetContentVisibility.ALL.value,
332
+ output_format: Literal["JSON-LD", "N-Quads"] = "JSON-LD",
333
+ validate: bool = True,
334
+ ) -> dict[str, HexStr | list[JSONLD] | dict[str, str]]:
335
+ state = (
336
+ state.upper()
337
+ if (isinstance(state, str) and not re.match(r"^0x[a-fA-F0-9]{64}$", state))
338
+ else state
339
+ )
340
+ content_visibility = content_visibility.upper()
341
+ output_format = output_format.upper()
342
+
343
+ token_id = parse_ual(ual)["token_id"]
344
+
345
+ def handle_latest_state(token_id: int) -> tuple[HexStr, bool]:
346
+ unfinalized_state = Web3.to_hex(self._get_unfinalized_state(token_id))
347
+
348
+ if unfinalized_state and unfinalized_state != HASH_ZERO:
349
+ return unfinalized_state, False
350
+ else:
351
+ return handle_latest_finalized_state(token_id)
352
+
353
+ def handle_latest_finalized_state(token_id: int) -> tuple[HexStr, bool]:
354
+ return Web3.to_hex(self._get_latest_assertion_id(token_id)), True
355
+
356
+ is_state_finalized = False
357
+
358
+ match state:
359
+ case KnowledgeAssetEnumStates.LATEST.value:
360
+ public_assertion_id, is_state_finalized = handle_latest_state(token_id)
361
+
362
+ case KnowledgeAssetEnumStates.LATEST_FINALIZED.value:
363
+ public_assertion_id, is_state_finalized = handle_latest_finalized_state(
364
+ token_id
365
+ )
366
+
367
+ case _ if isinstance(state, int):
368
+ assertion_ids = [
369
+ Web3.to_hex(assertion_id)
370
+ for assertion_id in self._get_assertion_ids(token_id)
371
+ ]
372
+ if 0 <= state < (states_number := len(assertion_ids)):
373
+ public_assertion_id = assertion_ids[state]
374
+
375
+ if state == states_number - 1:
376
+ is_state_finalized = True
377
+ else:
378
+ raise InvalidStateOption(f"State index {state} is out of range.")
379
+
380
+ case _ if isinstance(state, str) and re.match(
381
+ r"^0x[a-fA-F0-9]{64}$", state
382
+ ):
383
+ assertion_ids = [
384
+ Web3.to_hex(assertion_id)
385
+ for assertion_id in self._get_assertion_ids(token_id)
386
+ ]
387
+
388
+ if state in assertion_ids:
389
+ public_assertion_id = state
390
+
391
+ if state == assertion_ids[-1]:
392
+ is_state_finalized = True
393
+ else:
394
+ raise InvalidStateOption(
395
+ f"Given state hash: {state} is not a part of the KA."
396
+ )
397
+
398
+ case _:
399
+ raise InvalidStateOption(f"Invalid state option: {state}.")
400
+
401
+ get_public_operation_id: NodeResponseDict = self._get(
402
+ ual, public_assertion_id, hashFunctionId=1
403
+ )["operationId"]
404
+
405
+ get_public_operation_result = self.get_operation_result(
406
+ get_public_operation_id, "get"
407
+ )
408
+ public_assertion = get_public_operation_result["data"].get("assertion", None)
409
+
410
+ if public_assertion is None:
411
+ raise MissingKnowledgeAssetState("Unable to find state on the network!")
412
+
413
+ if validate:
414
+ root = MerkleTree(
415
+ hash_assertion_with_indexes(public_assertion), sort_pairs=True
416
+ ).root
417
+ if root != public_assertion_id:
418
+ raise InvalidKnowledgeAsset(
419
+ f"State: {public_assertion_id}. " f"Merkle Tree Root: {root}"
420
+ )
421
+
422
+ result = {"operation": {}}
423
+ if content_visibility != KnowledgeAssetContentVisibility.PRIVATE.value:
424
+ formatted_public_assertion = public_assertion
425
+
426
+ match output_format:
427
+ case "NQUADS" | "N-QUADS":
428
+ formatted_public_assertion: list[JSONLD] = jsonld.from_rdf(
429
+ "\n".join(public_assertion),
430
+ {"algorithm": "URDNA2015", "format": "application/n-quads"},
431
+ )
432
+ case "JSONLD" | "JSON-LD":
433
+ formatted_public_assertion = "\n".join(public_assertion)
434
+
435
+ case _:
436
+ raise DatasetOutputFormatNotSupported(
437
+ f"{output_format} isn't supported!"
438
+ )
439
+
440
+ if content_visibility == KnowledgeAssetContentVisibility.PUBLIC.value:
441
+ result = {
442
+ **result,
443
+ "asertion": formatted_public_assertion,
444
+ "assertionId": public_assertion_id,
445
+ }
446
+ else:
447
+ result["public"] = {
448
+ "assertion": formatted_public_assertion,
449
+ "assertionId": public_assertion_id,
450
+ }
451
+
452
+ result["operation"]["publicGet"] = {
453
+ "operationId": get_public_operation_id,
454
+ "status": get_public_operation_result["status"],
455
+ }
456
+
457
+ if content_visibility != KnowledgeAssetContentVisibility.PUBLIC.value:
458
+ private_assertion_link_triples = list(
459
+ filter(
460
+ lambda element: PRIVATE_ASSERTION_PREDICATE in element,
461
+ public_assertion,
462
+ )
463
+ )
464
+
465
+ if private_assertion_link_triples:
466
+ private_assertion_id = re.search(
467
+ r'"(.*?)"', private_assertion_link_triples[0]
468
+ ).group(1)
469
+
470
+ private_assertion = get_public_operation_result["data"].get(
471
+ "privateAssertion", None
472
+ )
473
+
474
+ query_private_operation_id: NodeResponseDict | None = None
475
+ if private_assertion is None:
476
+ query = f"""
477
+ CONSTRUCT {{ ?s ?p ?o }}
478
+ WHERE {{
479
+ {{
480
+ GRAPH <assertion:{private_assertion_id}>
481
+ {{
482
+ ?s ?p ?o .
483
+ }}
484
+ }}
485
+ }}
486
+ """
487
+
488
+ query_private_operation_id = self._query(
489
+ query,
490
+ "CONSTRUCT",
491
+ self.PRIVATE_CURRENT_REPOSITORY
492
+ if is_state_finalized
493
+ else self.PRIVATE_HISTORICAL_REPOSITORY,
494
+ )["operationId"]
495
+
496
+ query_private_operation_result = self.get_operation_result(
497
+ query_private_operation_id, "query"
498
+ )
499
+
500
+ private_assertion = normalize_dataset(
501
+ query_private_operation_result["data"],
502
+ "N-Quads",
503
+ )
504
+
505
+ if validate:
506
+ root = MerkleTree(
507
+ hash_assertion_with_indexes(private_assertion),
508
+ sort_pairs=True,
509
+ ).root
510
+ if root != private_assertion_id:
511
+ raise InvalidKnowledgeAsset(
512
+ f"State: {private_assertion_id}. "
513
+ f"Merkle Tree Root: {root}"
514
+ )
515
+
516
+ match output_format:
517
+ case "NQUADS" | "N-QUADS":
518
+ formatted_private_assertion: list[JSONLD] = jsonld.from_rdf(
519
+ "\n".join(private_assertion),
520
+ {
521
+ "algorithm": "URDNA2015",
522
+ "format": "application/n-quads",
523
+ },
524
+ )
525
+ case "JSONLD" | "JSON-LD":
526
+ formatted_private_assertion = "\n".join(private_assertion)
527
+
528
+ case _:
529
+ raise DatasetOutputFormatNotSupported(
530
+ f"{output_format} isn't supported!"
531
+ )
532
+
533
+ if content_visibility == KnowledgeAssetContentVisibility.PRIVATE:
534
+ result = {
535
+ **result,
536
+ "assertion": formatted_private_assertion,
537
+ "assertionId": private_assertion_id,
538
+ }
539
+ else:
540
+ result["private"] = {
541
+ "assertion": formatted_private_assertion,
542
+ "assertionId": private_assertion_id,
543
+ }
544
+
545
+ if query_private_operation_id is not None:
546
+ result["operation"]["queryPrivate"] = {
547
+ "operationId": query_private_operation_id,
548
+ "status": query_private_operation_result["status"],
549
+ }
550
+
551
+ return result
552
+
553
+ _extend_storing_period = Method(BlockchainRequest.extend_asset_storing_period)
554
+
555
+ def extend_storing_period(
556
+ self,
557
+ ual: UAL,
558
+ additional_epochs: int,
559
+ token_amount: int | None = None,
560
+ ) -> dict[str, UAL | dict[str, str]]:
561
+ parsed_ual = parse_ual(ual)
562
+ content_asset_storage_address, token_id = (
563
+ parsed_ual["contract_address"],
564
+ parsed_ual["token_id"],
565
+ )
566
+
567
+ if token_amount is None:
568
+ chain_name = BLOCKCHAINS[self._chain_id()]["name"]
569
+
570
+ latest_finalized_state = self._get_latest_assertion_id(token_id)
571
+ latest_finalized_state_size = self._get_assertion_size(
572
+ latest_finalized_state
573
+ )
574
+
575
+ token_amount = int(
576
+ self._get_bid_suggestion(
577
+ chain_name,
578
+ additional_epochs,
579
+ latest_finalized_state_size,
580
+ content_asset_storage_address,
581
+ latest_finalized_state,
582
+ self.DEFAULT_HASH_FUNCTION_ID,
583
+ )["bidSuggestion"]
584
+ )
585
+
586
+ self._extend_storing_period(token_id, additional_epochs, token_amount)
587
+
588
+ return {
589
+ "UAL": ual,
590
+ "operation": {"status": "COMPLETED"},
591
+ }
592
+
593
+ _get_assertion_size = Method(BlockchainRequest.get_assertion_size)
594
+ _add_tokens = Method(BlockchainRequest.increase_asset_token_amount)
595
+
596
+ def add_tokens(
597
+ self,
598
+ ual: UAL,
599
+ token_amount: int | None = None,
600
+ ) -> dict[str, UAL | dict[str, str]]:
601
+ parsed_ual = parse_ual(ual)
602
+ content_asset_storage_address, token_id = (
603
+ parsed_ual["contract_address"],
604
+ parsed_ual["token_id"],
605
+ )
606
+
607
+ if token_amount is None:
608
+ chain_name = BLOCKCHAINS[self._chain_id()]["name"]
609
+
610
+ agreement_id = self.get_agreement_id(
611
+ content_asset_storage_address, token_id
612
+ )
613
+ # TODO: Dynamic types for namedtuples?
614
+ agreement_data: Type[AgreementData] = self._get_service_agreement_data(
615
+ agreement_id
616
+ )
617
+
618
+ timestamp_now = self._get_block("latest")["timestamp"]
619
+ current_epoch = math.floor(
620
+ (timestamp_now - agreement_data.startTime) / agreement_data.epochLength
621
+ )
622
+ epochs_left = agreement_data.epochsNumber - current_epoch
623
+
624
+ latest_finalized_state = self._get_latest_assertion_id(token_id)
625
+ latest_finalized_state_size = self._get_assertion_size(
626
+ latest_finalized_state
627
+ )
628
+
629
+ token_amount = int(
630
+ self._get_bid_suggestion(
631
+ chain_name,
632
+ epochs_left,
633
+ latest_finalized_state_size,
634
+ content_asset_storage_address,
635
+ latest_finalized_state,
636
+ self.DEFAULT_HASH_FUNCTION_ID,
637
+ )["bidSuggestion"]
638
+ ) - sum(agreement_data.tokensInfo)
639
+
640
+ if token_amount <= 0:
641
+ raise InvalidTokenAmount(
642
+ "Token amount is bigger than default suggested amount, "
643
+ "please specify exact token_amount if you still want to add "
644
+ "more tokens!"
645
+ )
646
+
647
+ self._add_tokens(token_id, token_amount)
648
+
649
+ return {
650
+ "UAL": ual,
651
+ "operation": {"status": "COMPLETED"},
652
+ }
653
+
654
+ _add_update_tokens = Method(BlockchainRequest.increase_asset_update_token_amount)
655
+
656
+ def add_update_tokens(
657
+ self,
658
+ ual: UAL,
659
+ token_amount: int | None = None,
660
+ ) -> dict[str, UAL | dict[str, str]]:
661
+ parsed_ual = parse_ual(ual)
662
+ content_asset_storage_address, token_id = (
663
+ parsed_ual["contract_address"],
664
+ parsed_ual["token_id"],
665
+ )
666
+
667
+ if token_amount is None:
668
+ chain_name = BLOCKCHAINS[self._chain_id()]["name"]
669
+
670
+ agreement_id = self.get_agreement_id(
671
+ content_asset_storage_address, token_id
672
+ )
673
+ # TODO: Dynamic types for namedtuples?
674
+ agreement_data: Type[AgreementData] = self._get_service_agreement_data(
675
+ agreement_id
676
+ )
677
+
678
+ timestamp_now = self._get_block("latest")["timestamp"]
679
+ current_epoch = math.floor(
680
+ (timestamp_now - agreement_data.startTime) / agreement_data.epochLength
681
+ )
682
+ epochs_left = agreement_data.epochsNumber - current_epoch
683
+
684
+ unfinalized_state = self._get_latest_assertion_id(token_id)
685
+ unfinalized_state_size = self._get_assertion_size(unfinalized_state)
686
+
687
+ token_amount = int(
688
+ self._get_bid_suggestion(
689
+ chain_name,
690
+ epochs_left,
691
+ unfinalized_state_size,
692
+ content_asset_storage_address,
693
+ unfinalized_state,
694
+ self.DEFAULT_HASH_FUNCTION_ID,
695
+ )["bidSuggestion"]
696
+ ) - sum(agreement_data.tokensInfo)
697
+
698
+ if token_amount <= 0:
699
+ raise InvalidTokenAmount(
700
+ "Token amount is bigger than default suggested amount, "
701
+ "please specify exact token_amount if you still want to add "
702
+ "more update tokens!"
703
+ )
704
+
705
+ self._add_update_tokens(token_id, token_amount)
706
+
707
+ return {
708
+ "UAL": ual,
709
+ "operation": {"status": "COMPLETED"},
710
+ }
711
+
712
+ _owner = Method(BlockchainRequest.owner_of)
713
+
714
+ def get_owner(self, ual: UAL) -> Address:
715
+ token_id = parse_ual(ual)["token_id"]
716
+
717
+ return self._owner(token_id)
718
+
719
+ def _process_content(
720
+ self,
721
+ content: dict[Literal["public", "private"], JSONLD],
722
+ type: Literal["JSON-LD", "N-Quads"] = "JSON-LD",
723
+ ) -> dict[str, dict[str, HexStr | NQuads | int]]:
724
+ public_graph = {"@graph": []}
725
+
726
+ if content.get("public", None):
727
+ public_graph["@graph"].append(content["public"])
728
+
729
+ if content.get("private", None):
730
+ private_assertion = normalize_dataset(content["private"], type)
731
+ private_assertion_id = MerkleTree(
732
+ hash_assertion_with_indexes(private_assertion),
733
+ sort_pairs=True,
734
+ ).root
735
+
736
+ public_graph["@graph"].append(
737
+ {PRIVATE_ASSERTION_PREDICATE: private_assertion_id}
738
+ )
739
+
740
+ public_assertion = normalize_dataset(public_graph, type)
741
+ public_assertion_id = MerkleTree(
742
+ hash_assertion_with_indexes(public_assertion),
743
+ sort_pairs=True,
744
+ ).root
745
+ public_assertion_metadata = generate_assertion_metadata(public_assertion)
746
+
747
+ return {
748
+ "public": {
749
+ "id": public_assertion_id,
750
+ "content": public_assertion,
751
+ **public_assertion_metadata,
752
+ },
753
+ "private": {
754
+ "id": private_assertion_id,
755
+ "content": private_assertion,
756
+ }
757
+ if content.get("private", None)
758
+ else {},
759
+ }
760
+
761
+ _get_assertion_id_by_index = Method(BlockchainRequest.get_assertion_id_by_index)
762
+
763
+ def get_agreement_id(self, contract_address: Address, token_id: int) -> HexStr:
764
+ first_assertion_id = self._get_assertion_id_by_index(token_id, 0)
765
+ keyword = generate_keyword(contract_address, first_assertion_id)
766
+ return generate_agreement_id(contract_address, token_id, keyword)
767
+
768
+ _get_operation_result = Method(NodeRequest.get_operation_result)
769
+
770
+ @retry(catch=OperationNotFinished, max_retries=5, base_delay=1, backoff=2)
771
+ def get_operation_result(
772
+ self, operation_id: str, operation: str
773
+ ) -> NodeResponseDict:
774
+ operation_result = self._get_operation_result(
775
+ operation_id=operation_id,
776
+ operation=operation,
777
+ )
778
+
779
+ validate_operation_status(operation_result)
780
+
781
+ return operation_result