Skip to content

Commit f6a89c5

Browse files
committed
Add an OgmiosV6 backend based on ogmios-python
This requires bumping the python version to 3.10+, because ogmios-python requires this. I also refactored the chain backend to not have built-in kupo support but just provide it as an extension using kupo-wrappers. This way, both OgmiosV5 and OgmiosV6 directly have support for kupo.
1 parent 80f2604 commit f6a89c5

File tree

8 files changed

+1346
-459
lines changed

8 files changed

+1346
-459
lines changed

integration-test/test/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class TestBase:
2222
# TODO: Bring back kupo test
2323
KUPO_URL = "http://localhost:1442"
2424

25-
chain_context = python_ogmios.OgmiosChainContext(
25+
chain_context = python_ogmios.OgmiosV5ChainContext(
2626
host="localhost", port=1337, network=Network.TESTNET
2727
)
2828

poetry.lock

Lines changed: 716 additions & 331 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pycardano/backend/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
from .base import *
44
from .blockfrost import *
55
from .cardano_cli import *
6-
from .ogmios import *
6+
from .ogmios_v5 import *

pycardano/backend/kupo.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
from typing import Dict, List, Optional, Union
2+
3+
import requests
4+
from cachetools import Cache, LRUCache, TTLCache
5+
6+
from pycardano.address import Address
7+
from pycardano.backend.base import (
8+
ChainContext,
9+
GenesisParameters,
10+
)
11+
from pycardano.backend.blockfrost import _try_fix_script
12+
from pycardano.hash import DatumHash
13+
from pycardano.network import Network
14+
from pycardano.plutus import ExecutionUnits, PlutusV1Script, PlutusV2Script
15+
from pycardano.serialization import RawCBOR
16+
from pycardano.transaction import (
17+
Asset,
18+
MultiAsset,
19+
TransactionInput,
20+
TransactionOutput,
21+
UTxO,
22+
Value,
23+
)
24+
25+
__all__ = ["KupoChainContextExtension"]
26+
27+
28+
class KupoChainContextExtension(ChainContext):
29+
_wrapped_backend: ChainContext
30+
_kupo_url: Optional[str]
31+
_utxo_cache: Cache
32+
_datum_cache: Cache
33+
_refetch_chain_tip_interval: int
34+
35+
def __init__(
36+
self,
37+
wrapped_backend: ChainContext,
38+
kupo_url: Optional[str] = None,
39+
refetch_chain_tip_interval: int = 10,
40+
utxo_cache_size: int = 1000,
41+
datum_cache_size: int = 1000,
42+
):
43+
self._kupo_url = kupo_url
44+
self._wrapped_backend = wrapped_backend
45+
self._refetch_chain_tip_interval = refetch_chain_tip_interval
46+
self._utxo_cache = TTLCache(
47+
ttl=self._refetch_chain_tip_interval, maxsize=utxo_cache_size
48+
)
49+
self._datum_cache = LRUCache(maxsize=datum_cache_size)
50+
51+
@property
52+
def genesis_param(self) -> GenesisParameters:
53+
"""Get chain genesis parameters"""
54+
55+
return self._wrapped_backend.genesis_param
56+
57+
@property
58+
def network(self) -> Network:
59+
"""Get current network"""
60+
return self._wrapped_backend.network
61+
62+
@property
63+
def epoch(self) -> int:
64+
"""Current epoch number"""
65+
return self._wrapped_backend.epoch
66+
67+
@property
68+
def last_block_slot(self) -> int:
69+
"""Last block slot"""
70+
return self._wrapped_backend.last_block_slot
71+
72+
def _utxos(self, address: str) -> List[UTxO]:
73+
"""Get all UTxOs associated with an address.
74+
75+
Args:
76+
address (str): An address encoded with bech32.
77+
78+
Returns:
79+
List[UTxO]: A list of UTxOs.
80+
"""
81+
key = (self.last_block_slot, address)
82+
if key in self._utxo_cache:
83+
return self._utxo_cache[key]
84+
85+
if self._kupo_url:
86+
utxos = self._utxos_kupo(address)
87+
else:
88+
utxos = self._wrapped_backend.utxos(address)
89+
90+
self._utxo_cache[key] = utxos
91+
92+
return utxos
93+
94+
def _get_datum_from_kupo(self, datum_hash: str) -> Optional[RawCBOR]:
95+
"""Get datum from Kupo.
96+
97+
Args:
98+
datum_hash (str): A datum hash.
99+
100+
Returns:
101+
Optional[RawCBOR]: A datum.
102+
"""
103+
datum = self._datum_cache.get(datum_hash, None)
104+
105+
if datum is not None:
106+
return datum
107+
108+
if self._kupo_url is None:
109+
raise AssertionError(
110+
"kupo_url object attribute has not been assigned properly."
111+
)
112+
113+
kupo_datum_url = self._kupo_url + "/datums/" + datum_hash
114+
datum_result = requests.get(kupo_datum_url).json()
115+
if datum_result and datum_result["datum"] != datum_hash:
116+
datum = RawCBOR(bytes.fromhex(datum_result["datum"]))
117+
118+
self._datum_cache[datum_hash] = datum
119+
return datum
120+
121+
def _utxos_kupo(self, address: str) -> List[UTxO]:
122+
"""Get all UTxOs associated with an address with Kupo.
123+
Since UTxO querying will be deprecated from Ogmios in next
124+
major release: https://ogmios.dev/mini-protocols/local-state-query/.
125+
126+
Args:
127+
address (str): An address encoded with bech32.
128+
129+
Returns:
130+
List[UTxO]: A list of UTxOs.
131+
"""
132+
if self._kupo_url is None:
133+
raise AssertionError(
134+
"kupo_url object attribute has not been assigned properly."
135+
)
136+
137+
kupo_utxo_url = self._kupo_url + "/matches/" + address + "?unspent"
138+
results = requests.get(kupo_utxo_url).json()
139+
140+
utxos = []
141+
142+
for result in results:
143+
tx_id = result["transaction_id"]
144+
index = result["output_index"]
145+
146+
if result["spent_at"] is None:
147+
tx_in = TransactionInput.from_primitive([tx_id, index])
148+
149+
lovelace_amount = result["value"]["coins"]
150+
151+
script = None
152+
script_hash = result.get("script_hash", None)
153+
if script_hash:
154+
kupo_script_url = self._kupo_url + "/scripts/" + script_hash
155+
script = requests.get(kupo_script_url).json()
156+
if script["language"] == "plutus:v2":
157+
script = PlutusV2Script(bytes.fromhex(script["script"]))
158+
script = _try_fix_script(script_hash, script)
159+
elif script["language"] == "plutus:v1":
160+
script = PlutusV1Script(bytes.fromhex(script["script"]))
161+
script = _try_fix_script(script_hash, script)
162+
else:
163+
raise ValueError("Unknown plutus script type")
164+
165+
datum = None
166+
datum_hash = (
167+
DatumHash.from_primitive(result["datum_hash"])
168+
if result["datum_hash"]
169+
else None
170+
)
171+
if datum_hash and result.get("datum_type", "inline"):
172+
datum = self._get_datum_from_kupo(result["datum_hash"])
173+
174+
if not result["value"]["assets"]:
175+
tx_out = TransactionOutput(
176+
Address.from_primitive(address),
177+
amount=lovelace_amount,
178+
datum_hash=datum_hash,
179+
datum=datum,
180+
script=script,
181+
)
182+
else:
183+
multi_assets = MultiAsset()
184+
185+
for asset, quantity in result["value"]["assets"].items():
186+
policy_hex, policy, asset_name_hex = self._extract_asset_info(
187+
asset
188+
)
189+
multi_assets.setdefault(policy, Asset())[
190+
asset_name_hex
191+
] = quantity
192+
193+
tx_out = TransactionOutput(
194+
Address.from_primitive(address),
195+
amount=Value(lovelace_amount, multi_assets),
196+
datum_hash=datum_hash,
197+
datum=datum,
198+
script=script,
199+
)
200+
utxos.append(UTxO(tx_in, tx_out))
201+
else:
202+
continue
203+
204+
return utxos
205+
206+
def submit_tx_cbor(self, cbor: Union[bytes, str]):
207+
"""Submit a transaction to the blockchain.
208+
209+
Args:
210+
cbor (Union[bytes, str]): The transaction to be submitted.
211+
212+
Raises:
213+
:class:`InvalidArgumentException`: When the transaction is invalid.
214+
:class:`TransactionFailedException`: When fails to submit the transaction to blockchain.
215+
"""
216+
return self._wrapped_backend.submit_tx_cbor(cbor)
217+
218+
def evaluate_tx_cbor(self, cbor: Union[bytes, str]) -> Dict[str, ExecutionUnits]:
219+
"""Evaluate execution units of a transaction.
220+
221+
Args:
222+
cbor (Union[bytes, str]): The serialized transaction to be evaluated.
223+
224+
Returns:
225+
Dict[str, ExecutionUnits]: A list of execution units calculated for each of the transaction's redeemers
226+
227+
Raises:
228+
:class:`TransactionFailedException`: When fails to evaluate the transaction.
229+
"""
230+
return self._wrapped_backend.evaluate_tx_cbor(cbor)

0 commit comments

Comments
 (0)