Skip to content

Commit b0d79c1

Browse files
henryyuanheng-wangYuanheng WangYuanheng Wangdependabot[bot]
authored
Enable UTxO query with Kupo (#39)
* incorporate split change logic * incorporate split change logic * incorporate split change logic * minor modification to change combine logic * minor modification to change combine logic * minor modification to change combine logic * integrate diff changes * integrate diff changes and remove unused functions * add change split test case. Modify txbuilder logic in handeling splits. * Bump pytest from 7.0.1 to 7.1.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](pytest-dev/pytest@7.0.1...7.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> * replace max fee with more accurately estimated fees * replace max fee with more precise fee estimation * remove max fee import * modify format to be consistent with code base * supports utxo queries using kupo * support utxo query with Kupo * reformat * query utxo with Kupo when kupo url is provided * remove the hardcoded genesis utxo in test_all file Co-authored-by: Yuanheng Wang <[email protected]> Co-authored-by: Yuanheng Wang <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent c95a3db commit b0d79c1

File tree

6 files changed

+143
-11
lines changed

6 files changed

+143
-11
lines changed

integration-test/docker-compose.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,25 @@ services:
5858
max-size: "200k"
5959
max-file: "10"
6060

61+
kupo:
62+
image: cardanosolutions/kupo
63+
environment:
64+
NETWORK: "${NETWORK:-local}"
65+
66+
command: [
67+
"--node-socket", "/ipc/node.socket",
68+
"--node-config", "/code/configs/${NETWORK:-local}/config.json",
69+
"--host", "0.0.0.0",
70+
"--since", "origin",
71+
"--match", "*",
72+
"--in-memory"
73+
]
74+
volumes:
75+
- .:/code
76+
- node-ipc:/ipc
77+
ports:
78+
- ${KUPO_PORT:-1442}:1442
79+
6180
volumes:
6281
node-db:
6382
node-ipc:

integration-test/run_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ sleep 8
2626
poetry run pytest -s -n 4 "$ROOT"/test
2727

2828
# Cleanup
29-
docker-compose down --volumes --remove-orphans
29+
docker-compose down --volumes --remove-orphans

integration-test/test/test_all.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ class TestAll:
2222

2323
OGMIOS_WS = "ws://localhost:1337"
2424

25-
chain_context = OgmiosChainContext(OGMIOS_WS, Network.TESTNET)
25+
KUPO_URL = "http://localhost:1442/v1/matches"
26+
27+
chain_context = OgmiosChainContext(OGMIOS_WS, Network.TESTNET, kupo_url=KUPO_URL)
2628

2729
check_chain_context(chain_context)
2830

@@ -214,7 +216,7 @@ def test_plutus(self):
214216

215217
# ----------- Giver give ---------------
216218

217-
with open("plutus_scripts/fortytwo.plutus", "r") as f:
219+
with open("./plutus_scripts/fortytwo.plutus", "r") as f:
218220
script_hex = f.read()
219221
forty_two_script = cbor2.loads(bytes.fromhex(script_hex))
220222

pycardano/backend/ogmios.py

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44
from typing import Dict, List, Union
55

6+
import requests
67
import websocket
78

89
from pycardano.address import Address
@@ -25,10 +26,17 @@
2526

2627

2728
class OgmiosChainContext(ChainContext):
28-
def __init__(self, ws_url: str, network: Network, compact_result=True):
29+
def __init__(
30+
self,
31+
ws_url: str,
32+
network: Network,
33+
compact_result=True,
34+
kupo_url=None,
35+
):
2936
self._ws_url = ws_url
3037
self._network = network
3138
self._service_name = "ogmios.v1:compact" if compact_result else "ogmios"
39+
self._kupo_url = kupo_url
3240
self._last_known_block_slot = 0
3341
self._genesis_param = None
3442
self._protocol_param = None
@@ -152,15 +160,103 @@ def last_block_slot(self) -> int:
152160
args = {"query": "chainTip"}
153161
return self._request(method, args)["slot"]
154162

155-
def utxos(self, address: str) -> List[UTxO]:
156-
"""Get all UTxOs associated with an address.
163+
def _extract_asset_info(self, asset_hash: str):
164+
policy_hex, asset_name_hex = asset_hash.split(".")
165+
policy = ScriptHash.from_primitive(policy_hex)
166+
asset_name_hex = AssetName.from_primitive(asset_name_hex)
167+
168+
return policy_hex, policy, asset_name_hex
169+
170+
def _check_utxo_unspent(self, tx_id: str, index: int) -> bool:
171+
"""Check whether an UTxO is unspent with Ogmios.
172+
173+
Args:
174+
tx_id (str): transaction id.
175+
index (int): transaction index.
176+
"""
177+
178+
method = "Query"
179+
args = {"query": {"utxo": [{"txId": tx_id, "index": index}]}}
180+
results = self._request(method, args)
181+
182+
if results:
183+
return True
184+
else:
185+
return False
186+
187+
def _utxos_kupo(self, address: str) -> List[UTxO]:
188+
"""Get all UTxOs associated with an address with Kupo.
189+
Since UTxO querying will be deprecated from Ogmios in next
190+
major release: https://ogmios.dev/mini-protocols/local-state-query/.
191+
192+
Args:
193+
address (str): An address encoded with bech32.
194+
195+
Returns:
196+
List[UTxO]: A list of UTxOs.
197+
"""
198+
address_url = self._kupo_url + "/" + address
199+
results = requests.get(address_url).json()
200+
201+
utxos = []
202+
203+
for result in results:
204+
tx_id = result["transaction_id"]
205+
index = result["output_index"]
206+
207+
# Right now, all UTxOs of the address will be returned with Kupo, which requires Ogmios to
208+
# validate if the UTxOs are spent with output reference. This feature is being considered to
209+
# be added to Kupo to avoid extra API calls.
210+
# See discussion here: https://github.com/CardanoSolutions/kupo/discussions/19.
211+
if self._check_utxo_unspent(tx_id, index):
212+
tx_in = TransactionInput.from_primitive([tx_id, index])
213+
214+
lovelace_amount = result["value"]["coins"]
215+
216+
datum_hash = (
217+
DatumHash.from_primitive(result["datum_hash"])
218+
if result["datum_hash"]
219+
else None
220+
)
221+
222+
if not result["value"]["assets"]:
223+
tx_out = TransactionOutput(
224+
Address.from_primitive(address),
225+
amount=lovelace_amount,
226+
datum_hash=datum_hash,
227+
)
228+
else:
229+
multi_assets = MultiAsset()
230+
231+
for asset, quantity in result["value"]["assets"].items():
232+
policy_hex, policy, asset_name_hex = self._extract_asset_info(
233+
asset
234+
)
235+
multi_assets.setdefault(policy, Asset())[
236+
asset_name_hex
237+
] = quantity
238+
239+
tx_out = TransactionOutput(
240+
Address.from_primitive(address),
241+
amount=Value(lovelace_amount, multi_assets),
242+
datum_hash=datum_hash,
243+
)
244+
utxos.append(UTxO(tx_in, tx_out))
245+
else:
246+
continue
247+
248+
return utxos
249+
250+
def _utxos_ogmios(self, address: str) -> List[UTxO]:
251+
"""Get all UTxOs associated with an address with Ogmios.
157252
158253
Args:
159254
address (str): An address encoded with bech32.
160255
161256
Returns:
162257
List[UTxO]: A list of UTxOs.
163258
"""
259+
164260
method = "Query"
165261
args = {"query": {"utxo": [address]}}
166262
results = self._request(method, args)
@@ -187,11 +283,8 @@ def utxos(self, address: str) -> List[UTxO]:
187283
else:
188284
multi_assets = MultiAsset()
189285

190-
for asset in output["value"]["assets"]:
191-
quantity = output["value"]["assets"][asset]
192-
policy_hex, asset_name_hex = asset.split(".")
193-
policy = ScriptHash.from_primitive(policy_hex)
194-
asset_name_hex = AssetName.from_primitive(asset_name_hex)
286+
for asset, quantity in output["value"]["assets"].items():
287+
policy_hex, policy, asset_name_hex = self._extract_asset_info(asset)
195288
multi_assets.setdefault(policy, Asset())[asset_name_hex] = quantity
196289

197290
tx_out = TransactionOutput(
@@ -203,6 +296,22 @@ def utxos(self, address: str) -> List[UTxO]:
203296

204297
return utxos
205298

299+
def utxos(self, address: str) -> List[UTxO]:
300+
"""Get all UTxOs associated with an address.
301+
302+
Args:
303+
address (str): An address encoded with bech32.
304+
305+
Returns:
306+
List[UTxO]: A list of UTxOs.
307+
"""
308+
if self._kupo_url:
309+
utxos = self._utxos_kupo(address)
310+
else:
311+
utxos = self._utxos_ogmios(address)
312+
313+
return utxos
314+
206315
def submit_tx(self, cbor: Union[bytes, str]):
207316
"""Submit a transaction to the blockchain.
208317

pycardano/txbuilder.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ def build(
738738
)
739739
else:
740740
unfulfilled_amount.coin = max(0, unfulfilled_amount.coin)
741+
741742
# Clean up all non-positive assets
742743
unfulfilled_amount.multi_asset = unfulfilled_amount.multi_asset.filter(
743744
lambda p, n, v: v > 0

test/pycardano/test_txbuilder.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def test_tx_builder_raises_utxo_selection(chain_context):
175175

176176
with pytest.raises(UTxOSelectionException) as e:
177177
tx_body = tx_builder.build(change_address=sender_address)
178+
178179
# The unfulfilled amount includes requested (991000000) and estimated fees (161277)
179180
assert "Unfulfilled amount:\n {'coin': 991161277" in e.value.args[0]
180181
assert "{AssetName(b'NewToken'): 1}" in e.value.args[0]

0 commit comments

Comments
 (0)