pccontext 0.1.0__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.
pccontext/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ # flake8: noqa
2
+ __version__ = "0.1.0"
3
+ __app_name__ = "pccontext"
4
+
5
+ from .backend import *
6
+ from .enums import *
7
+ from .models import *
8
+ from .utils import *
@@ -0,0 +1,9 @@
1
+ # flake8: noqa
2
+
3
+ from .blockfrost import *
4
+ from .cardano_cli import *
5
+ from .koios import *
6
+ from .kupo import *
7
+ from .offline_transfer_file import *
8
+ from .ogmios import *
9
+ from .yaci_devkit import *
@@ -0,0 +1,296 @@
1
+ import os
2
+ import tempfile
3
+ import time
4
+ import warnings
5
+ from typing import Dict, List, Optional, Union
6
+
7
+ import cbor2
8
+ from blockfrost import ApiError, ApiUrls, BlockFrostApi
9
+ from blockfrost.utils import Namespace
10
+ from pycardano.address import Address
11
+ from pycardano.backend.base import ChainContext
12
+ from pycardano.backend.base import ProtocolParameters as PyCardanoProtocolParameters
13
+ from pycardano.exception import TransactionFailedException
14
+ from pycardano.hash import SCRIPT_HASH_SIZE, DatumHash, ScriptHash
15
+ from pycardano.nativescript import NativeScript
16
+ from pycardano.network import Network
17
+ from pycardano.plutus import (
18
+ ExecutionUnits,
19
+ PlutusV1Script,
20
+ PlutusV2Script,
21
+ PlutusV3Script,
22
+ script_hash,
23
+ )
24
+ from pycardano.serialization import RawCBOR
25
+ from pycardano.transaction import (
26
+ Asset,
27
+ AssetName,
28
+ MultiAsset,
29
+ TransactionInput,
30
+ TransactionOutput,
31
+ UTxO,
32
+ Value,
33
+ )
34
+ from pycardano.types import JsonDict
35
+
36
+ from pccontext.models import GenesisParameters, ProtocolParameters, StakeAddressInfo
37
+
38
+ __all__ = ["BlockFrostChainContext"]
39
+
40
+
41
+ def _try_fix_script(
42
+ scripth: str, script: Union[PlutusV1Script, PlutusV2Script, PlutusV3Script]
43
+ ) -> Union[PlutusV1Script, PlutusV2Script, PlutusV3Script]:
44
+ if str(script_hash(script)) == scripth:
45
+ return script
46
+ new_script = script.__class__(cbor2.loads(script))
47
+ if str(script_hash(new_script)) == scripth:
48
+ return new_script
49
+ else:
50
+ raise ValueError("Cannot recover script from hash.")
51
+
52
+
53
+ class BlockFrostChainContext(ChainContext):
54
+ """A `BlockFrost <https://blockfrost.io/>`_ API wrapper for the client code to interact with.
55
+
56
+ Args:
57
+ project_id (str): A BlockFrost project ID obtained from https://blockfrost.io.
58
+ network (Network): Network to use.
59
+ base_url (str): Base URL for the BlockFrost API. Defaults to the preprod url.
60
+ """
61
+
62
+ api: BlockFrostApi
63
+ _epoch_info: Namespace
64
+ _epoch: Optional[int] = None
65
+ _genesis_param: Optional[GenesisParameters] = None
66
+ _protocol_param: Optional[ProtocolParameters] = None
67
+
68
+ def __init__(
69
+ self,
70
+ project_id: str,
71
+ network: Optional[Network] = None,
72
+ base_url: Optional[str] = None,
73
+ ):
74
+ if network is not None:
75
+ warnings.warn(
76
+ "`network` argument will be deprecated in the future. Directly passing `base_url` is recommended."
77
+ )
78
+ self._network = network
79
+ else:
80
+ self._network = Network.TESTNET
81
+
82
+ self._project_id = project_id
83
+ self._base_url = base_url or (
84
+ ApiUrls.preprod.value
85
+ if self.network == Network.TESTNET
86
+ else ApiUrls.mainnet.value
87
+ )
88
+
89
+ # Set network value to mainnet if base_url contains "mainnet".
90
+ if "mainnet" in self._base_url:
91
+ self._network = Network.MAINNET
92
+
93
+ self.api = BlockFrostApi(project_id=self._project_id, base_url=self._base_url)
94
+ self._epoch_info = self.api.epoch_latest()
95
+ self._epoch = None
96
+ self._genesis_param = None
97
+ self._protocol_param = None
98
+
99
+ def _check_epoch_and_update(self):
100
+ if int(time.time()) < self._epoch_info.end_time:
101
+ return False
102
+ self._epoch_info = self.api.epoch_latest()
103
+ return True
104
+
105
+ @property
106
+ def network(self) -> Network:
107
+ return self._network
108
+
109
+ @property
110
+ def epoch(self) -> int:
111
+ if not self._epoch or self._check_epoch_and_update():
112
+ new_epoch: int = self.api.epoch_latest().epoch
113
+ self._epoch = new_epoch
114
+ return self._epoch
115
+
116
+ @property
117
+ def last_block_slot(self) -> int:
118
+ block = self.api.block_latest()
119
+ return block.slot
120
+
121
+ @property
122
+ def genesis_param(self) -> GenesisParameters:
123
+ if not self._genesis_param or self._check_epoch_and_update():
124
+ params = self.api.genesis(return_type="json")
125
+ self._genesis_param = GenesisParameters.from_json(params)
126
+ return self._genesis_param
127
+
128
+ @property
129
+ def protocol_param(self) -> PyCardanoProtocolParameters:
130
+ if not self._protocol_param or self._check_epoch_and_update():
131
+ params = self.api.epoch_latest_parameters(return_type="json")
132
+ self._protocol_param = ProtocolParameters.from_json(params)
133
+ return self._protocol_param.to_pycardano()
134
+
135
+ def _get_script(
136
+ self, script_hash: str
137
+ ) -> Union[PlutusV1Script, PlutusV2Script, PlutusV3Script, NativeScript]:
138
+ script_type = self.api.script(script_hash).type
139
+ if script_type == "plutusV1":
140
+ v1script = PlutusV1Script(
141
+ bytes.fromhex(self.api.script_cbor(script_hash).cbor)
142
+ )
143
+ return _try_fix_script(script_hash, v1script)
144
+ elif script_type == "plutusV2":
145
+ v2script = PlutusV2Script(
146
+ bytes.fromhex(self.api.script_cbor(script_hash).cbor)
147
+ )
148
+ return _try_fix_script(script_hash, v2script)
149
+ elif script_type == "plutusV3":
150
+ v3script = PlutusV3Script(
151
+ bytes.fromhex(self.api.script_cbor(script_hash).cbor)
152
+ )
153
+ return _try_fix_script(script_hash, v3script)
154
+ else:
155
+ script_json: JsonDict = self.api.script_json(
156
+ script_hash, return_type="json"
157
+ )["json"]
158
+ return NativeScript.from_dict(script_json)
159
+
160
+ def _utxos(self, address: str) -> List[UTxO]:
161
+ try:
162
+ results = self.api.address_utxos(address, gather_pages=True)
163
+ except ApiError as e:
164
+ if e.status_code == 404:
165
+ return []
166
+ else:
167
+ raise e
168
+
169
+ utxos = []
170
+
171
+ for result in results:
172
+ tx_in = TransactionInput.from_primitive(
173
+ [result.tx_hash, result.output_index]
174
+ )
175
+ amount = result.amount
176
+ lovelace_amount = 0
177
+ multi_assets = MultiAsset()
178
+ for item in amount:
179
+ if item.unit == "lovelace":
180
+ lovelace_amount = int(item.quantity)
181
+ else:
182
+ # The utxo contains Multi-asset
183
+ data = bytes.fromhex(item.unit)
184
+ policy_id = ScriptHash(data[:SCRIPT_HASH_SIZE])
185
+ asset_name = AssetName(data[SCRIPT_HASH_SIZE:])
186
+
187
+ if policy_id not in multi_assets:
188
+ multi_assets[policy_id] = Asset()
189
+ multi_assets[policy_id][asset_name] = int(item.quantity)
190
+
191
+ amount = Value(lovelace_amount, multi_assets)
192
+
193
+ datum_hash = (
194
+ DatumHash.from_primitive(result.data_hash)
195
+ if result.data_hash and result.inline_datum is None
196
+ else None
197
+ )
198
+
199
+ datum = None
200
+
201
+ if hasattr(result, "inline_datum") and result.inline_datum is not None:
202
+ datum = RawCBOR(bytes.fromhex(result.inline_datum))
203
+
204
+ script = None
205
+
206
+ if (
207
+ hasattr(result, "reference_script_hash")
208
+ and result.reference_script_hash
209
+ ):
210
+ script = self._get_script(result.reference_script_hash)
211
+
212
+ tx_out = TransactionOutput(
213
+ Address.from_primitive(address),
214
+ amount=amount,
215
+ datum_hash=datum_hash,
216
+ datum=datum,
217
+ script=script,
218
+ )
219
+ utxos.append(UTxO(tx_in, tx_out))
220
+
221
+ return utxos
222
+
223
+ def submit_tx_cbor(self, cbor: Union[bytes, str]) -> str:
224
+ """Submit a transaction.
225
+
226
+ Args:
227
+ cbor (Union[bytes, str]): The serialized transaction to be submitted.
228
+
229
+ Returns:
230
+ str: The transaction hash.
231
+
232
+ Raises:
233
+ :class:`TransactionFailedException`: When fails to submit the transaction.
234
+ """
235
+ if isinstance(cbor, str):
236
+ cbor = bytes.fromhex(cbor)
237
+ with tempfile.NamedTemporaryFile(delete=False) as f:
238
+ f.write(cbor)
239
+ try:
240
+ response = self.api.transaction_submit(f.name)
241
+ except ApiError as e:
242
+ os.remove(f.name)
243
+ raise TransactionFailedException(
244
+ f"Failed to submit transaction. Error code: {e.status_code}. Error message: {e.message}"
245
+ ) from e
246
+ os.remove(f.name)
247
+ return response
248
+
249
+ def evaluate_tx_cbor(self, cbor: Union[bytes, str]) -> Dict[str, ExecutionUnits]:
250
+ """Evaluate execution units of a transaction.
251
+
252
+ Args:
253
+ cbor (Union[bytes, str]): The serialized transaction to be evaluated.
254
+
255
+ Returns:
256
+ Dict[str, ExecutionUnits]: A list of execution units calculated for each of the transaction's redeemers
257
+
258
+ Raises:
259
+ :class:`TransactionFailedException`: When fails to evaluate the transaction.
260
+ """
261
+ if isinstance(cbor, bytes):
262
+ cbor = cbor.hex()
263
+ with tempfile.NamedTemporaryFile(delete=False, mode="w") as f:
264
+ f.write(cbor)
265
+ result = self.api.transaction_evaluate(f.name).result
266
+ os.remove(f.name)
267
+ return_val = {}
268
+ if not hasattr(result, "EvaluationResult"):
269
+ raise TransactionFailedException(result)
270
+ else:
271
+ for k in vars(result.EvaluationResult):
272
+ return_val[k] = ExecutionUnits(
273
+ getattr(result.EvaluationResult, k).memory,
274
+ getattr(result.EvaluationResult, k).steps,
275
+ )
276
+ return return_val
277
+
278
+ def stake_address_info(self, stake_address: str) -> List[StakeAddressInfo]:
279
+ """Get the stake address information.
280
+
281
+ Args:
282
+ stake_address (str): The stake address.
283
+
284
+ Returns:
285
+ List[StakeAddressInfo]: The stake address information.
286
+ """
287
+ rewards_state = self.api.accounts(stake_address)
288
+
289
+ return [
290
+ StakeAddressInfo(
291
+ address=rewards_state.get("stake_address", None),
292
+ stake_delegation=rewards_state.get("pool_id", None),
293
+ reward_account_balance=rewards_state.get("withdrawable_amount", None),
294
+ delegate_representative=rewards_state.get("drep_id", None),
295
+ )
296
+ ]