Skip to content

Add an OgmiosV6 backend based on ogmios-python #368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
7 changes: 6 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python-version: [3.8, 3.9, '3.10', '3.11']
python-version: ['3.8', '3.9', '3.10', '3.11']

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -59,6 +59,11 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: 'poetry'

- name: Setup docker-compose
uses: KengoTODA/[email protected]
with:
version: '2.14.2'

- name: Run integration tests
run: |
cd integration-test && ./run_tests.sh
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
cov_html
docs/build
dist
.mypy_cache

# IDE
.idea
Expand Down
3 changes: 1 addition & 2 deletions integration-test/test/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import os

import ogmios as python_ogmios
from retry import retry

from pycardano import *
Expand All @@ -22,7 +21,7 @@ class TestBase:
# TODO: Bring back kupo test
KUPO_URL = "http://localhost:1442"

chain_context = python_ogmios.OgmiosChainContext(
chain_context = OgmiosV6ChainContext(
host="localhost", port=1337, network=Network.TESTNET
)

Expand Down
963 changes: 702 additions & 261 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pycardano/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
from .base import *
from .blockfrost import *
from .cardano_cli import *
from .ogmios import *
from .ogmios_v5 import *
from .ogmios_v6 import *
245 changes: 245 additions & 0 deletions pycardano/backend/kupo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
from typing import Dict, List, Optional, Union, Tuple

import requests
from cachetools import Cache, LRUCache, TTLCache

from pycardano.address import Address
from pycardano.backend.base import (
ChainContext,
GenesisParameters,
)
from pycardano.backend.blockfrost import _try_fix_script
from pycardano.hash import DatumHash, ScriptHash
from pycardano.network import Network
from pycardano.plutus import ExecutionUnits, PlutusV1Script, PlutusV2Script
from pycardano.serialization import RawCBOR
from pycardano.transaction import (
Asset,
MultiAsset,
TransactionInput,
TransactionOutput,
UTxO,
Value,
AssetName,
)

__all__ = ["KupoChainContextExtension"]


def extract_asset_info(asset_hash: str) -> Tuple[str, ScriptHash, AssetName]:
split_result = asset_hash.split(".")

if len(split_result) == 1:
policy_hex, asset_name_hex = split_result[0], ""
elif len(split_result) == 2:
policy_hex, asset_name_hex = split_result
else:
raise ValueError(f"Unable to parse asset hash: {asset_hash}")

Check warning on line 37 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L37

Added line #L37 was not covered by tests

policy = ScriptHash.from_primitive(policy_hex)
asset_name = AssetName.from_primitive(asset_name_hex)

return policy_hex, policy, asset_name


class KupoChainContextExtension(ChainContext):
_wrapped_backend: ChainContext
_kupo_url: Optional[str]
_utxo_cache: Cache
_datum_cache: Cache
_refetch_chain_tip_interval: int

def __init__(
self,
wrapped_backend: ChainContext,
kupo_url: Optional[str] = None,
refetch_chain_tip_interval: int = 10,
utxo_cache_size: int = 1000,
datum_cache_size: int = 1000,
):
self._kupo_url = kupo_url
self._wrapped_backend = wrapped_backend
self._refetch_chain_tip_interval = refetch_chain_tip_interval
self._utxo_cache = TTLCache(

Check warning on line 63 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L60-L63

Added lines #L60 - L63 were not covered by tests
ttl=self._refetch_chain_tip_interval, maxsize=utxo_cache_size
)
self._datum_cache = LRUCache(maxsize=datum_cache_size)

Check warning on line 66 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L66

Added line #L66 was not covered by tests

@property
def genesis_param(self) -> GenesisParameters:
"""Get chain genesis parameters"""

return self._wrapped_backend.genesis_param

Check warning on line 72 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L72

Added line #L72 was not covered by tests

@property
def network(self) -> Network:
"""Get current network"""
return self._wrapped_backend.network

Check warning on line 77 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L77

Added line #L77 was not covered by tests

@property
def epoch(self) -> int:
"""Current epoch number"""
return self._wrapped_backend.epoch

Check warning on line 82 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L82

Added line #L82 was not covered by tests

@property
def last_block_slot(self) -> int:
"""Last block slot"""
return self._wrapped_backend.last_block_slot

Check warning on line 87 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L87

Added line #L87 was not covered by tests

def _utxos(self, address: str) -> List[UTxO]:
"""Get all UTxOs associated with an address.

Args:
address (str): An address encoded with bech32.

Returns:
List[UTxO]: A list of UTxOs.
"""
key = (self.last_block_slot, address)

Check warning on line 98 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L98

Added line #L98 was not covered by tests
if key in self._utxo_cache:
return self._utxo_cache[key]

Check warning on line 100 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L100

Added line #L100 was not covered by tests

if self._kupo_url:
utxos = self._utxos_kupo(address)

Check warning on line 103 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L103

Added line #L103 was not covered by tests
else:
utxos = self._wrapped_backend.utxos(address)

Check warning on line 105 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L105

Added line #L105 was not covered by tests

self._utxo_cache[key] = utxos

Check warning on line 107 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L107

Added line #L107 was not covered by tests

return utxos

Check warning on line 109 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L109

Added line #L109 was not covered by tests

def _get_datum_from_kupo(self, datum_hash: str) -> Optional[RawCBOR]:
"""Get datum from Kupo.

Args:
datum_hash (str): A datum hash.

Returns:
Optional[RawCBOR]: A datum.
"""
datum = self._datum_cache.get(datum_hash, None)

Check warning on line 120 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L120

Added line #L120 was not covered by tests

if datum is not None:
return datum

Check warning on line 123 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L123

Added line #L123 was not covered by tests

if self._kupo_url is None:

Check warning on line 125 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L125

Added line #L125 was not covered by tests
raise AssertionError(
"kupo_url object attribute has not been assigned properly."
)

kupo_datum_url = self._kupo_url + "/datums/" + datum_hash
datum_result = requests.get(kupo_datum_url).json()

Check warning on line 131 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L130-L131

Added lines #L130 - L131 were not covered by tests
if datum_result and datum_result["datum"] != datum_hash:
datum = RawCBOR(bytes.fromhex(datum_result["datum"]))

Check warning on line 133 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L133

Added line #L133 was not covered by tests

self._datum_cache[datum_hash] = datum
return datum

Check warning on line 136 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L135-L136

Added lines #L135 - L136 were not covered by tests

def _utxos_kupo(self, address: str) -> List[UTxO]:
"""Get all UTxOs associated with an address with Kupo.
Since UTxO querying will be deprecated from Ogmios in next
major release: https://ogmios.dev/mini-protocols/local-state-query/.

Args:
address (str): An address encoded with bech32.

Returns:
List[UTxO]: A list of UTxOs.
"""
if self._kupo_url is None:

Check warning on line 149 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L149

Added line #L149 was not covered by tests
raise AssertionError(
"kupo_url object attribute has not been assigned properly."
)

kupo_utxo_url = self._kupo_url + "/matches/" + address + "?unspent"
results = requests.get(kupo_utxo_url).json()

Check warning on line 155 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L154-L155

Added lines #L154 - L155 were not covered by tests

utxos = []

Check warning on line 157 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L157

Added line #L157 was not covered by tests

for result in results:
tx_id = result["transaction_id"]
index = result["output_index"]

Check warning on line 161 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L160-L161

Added lines #L160 - L161 were not covered by tests

if result["spent_at"] is None:
tx_in = TransactionInput.from_primitive([tx_id, index])

Check warning on line 164 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L164

Added line #L164 was not covered by tests

lovelace_amount = result["value"]["coins"]

Check warning on line 166 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L166

Added line #L166 was not covered by tests

script = None
script_hash = result.get("script_hash", None)

Check warning on line 169 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L168-L169

Added lines #L168 - L169 were not covered by tests
if script_hash:
kupo_script_url = self._kupo_url + "/scripts/" + script_hash
script = requests.get(kupo_script_url).json()

Check warning on line 172 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L171-L172

Added lines #L171 - L172 were not covered by tests
if script["language"] == "plutus:v2":
script = PlutusV2Script(bytes.fromhex(script["script"]))
script = _try_fix_script(script_hash, script)

Check warning on line 175 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L174-L175

Added lines #L174 - L175 were not covered by tests
elif script["language"] == "plutus:v1":
script = PlutusV1Script(bytes.fromhex(script["script"]))
script = _try_fix_script(script_hash, script)

Check warning on line 178 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L177-L178

Added lines #L177 - L178 were not covered by tests
else:
raise ValueError("Unknown plutus script type")

Check warning on line 180 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L180

Added line #L180 was not covered by tests

datum = None
datum_hash = (

Check warning on line 183 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L182-L183

Added lines #L182 - L183 were not covered by tests
DatumHash.from_primitive(result["datum_hash"])
if result["datum_hash"]
else None
)
if datum_hash and result.get("datum_type", "inline"):
datum = self._get_datum_from_kupo(result["datum_hash"])

Check warning on line 189 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L189

Added line #L189 was not covered by tests

if not result["value"]["assets"]:
tx_out = TransactionOutput(

Check warning on line 192 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L192

Added line #L192 was not covered by tests
Address.from_primitive(address),
amount=lovelace_amount,
datum_hash=datum_hash,
datum=datum,
script=script,
)
else:
multi_assets = MultiAsset()

Check warning on line 200 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L200

Added line #L200 was not covered by tests

for asset, quantity in result["value"]["assets"].items():
policy_hex, policy, asset_name_hex = extract_asset_info(asset)
multi_assets.setdefault(policy, Asset())[

Check warning on line 204 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L203-L204

Added lines #L203 - L204 were not covered by tests
asset_name_hex
] = quantity

tx_out = TransactionOutput(

Check warning on line 208 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L208

Added line #L208 was not covered by tests
Address.from_primitive(address),
amount=Value(lovelace_amount, multi_assets),
datum_hash=datum_hash,
datum=datum,
script=script,
)
utxos.append(UTxO(tx_in, tx_out))

Check warning on line 215 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L215

Added line #L215 was not covered by tests
else:
continue

Check warning on line 217 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L217

Added line #L217 was not covered by tests

return utxos

Check warning on line 219 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L219

Added line #L219 was not covered by tests

def submit_tx_cbor(self, cbor: Union[bytes, str]):
"""Submit a transaction to the blockchain.

Args:
cbor (Union[bytes, str]): The transaction to be submitted.

Raises:
:class:`InvalidArgumentException`: When the transaction is invalid.
:class:`TransactionFailedException`: When fails to submit the transaction to blockchain.
"""
return self._wrapped_backend.submit_tx_cbor(cbor)

Check warning on line 231 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L231

Added line #L231 was not covered by tests

def evaluate_tx_cbor(self, cbor: Union[bytes, str]) -> Dict[str, ExecutionUnits]:
"""Evaluate execution units of a transaction.

Args:
cbor (Union[bytes, str]): The serialized transaction to be evaluated.

Returns:
Dict[str, ExecutionUnits]: A list of execution units calculated for each of the transaction's redeemers

Raises:
:class:`TransactionFailedException`: When fails to evaluate the transaction.
"""
return self._wrapped_backend.evaluate_tx_cbor(cbor)

Check warning on line 245 in pycardano/backend/kupo.py

View check run for this annotation

Codecov / codecov/patch

pycardano/backend/kupo.py#L245

Added line #L245 was not covered by tests
Loading
Loading