Skip to content

Commit 9f5af3e

Browse files
committed
Merge branch 'main' into fix/minted_amount_requested
2 parents 6f4f549 + 1e0f19b commit 9f5af3e

File tree

11 files changed

+342
-26
lines changed

11 files changed

+342
-26
lines changed

integration-test/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ services:
5252
max-file: "10"
5353

5454
ogmios:
55-
image: cardanosolutions/ogmios:latest
55+
image: cardanosolutions/ogmios:v5.6.0-mainnet
5656
environment:
5757
NETWORK: "${NETWORK:-local-alonzo}"
5858

integration-test/test/test_plutus.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_plutus_v1(self):
2626

2727
builder = TransactionBuilder(self.chain_context)
2828
builder.add_input_address(giver_address)
29-
datum = PlutusData() # A Unit type "()" in Haskell
29+
datum = Unit() # A Unit type "()" in Haskell
3030
builder.add_output(
3131
TransactionOutput(script_address, 50000000, datum_hash=datum_hash(datum))
3232
)
@@ -311,3 +311,63 @@ def test_plutus_v2_ref_script(self):
311311
self.chain_context.submit_tx(signed_tx)
312312

313313
self.assert_output(taker_address, take_output)
314+
315+
@retry(tries=TEST_RETRIES, backoff=1.5, delay=6, jitter=(0, 4))
316+
@pytest.mark.post_alonzo
317+
def test_transaction_chaining(self):
318+
giver_address = Address(self.payment_vkey.hash(), network=self.NETWORK)
319+
builder = TransactionBuilder(self.chain_context)
320+
builder.add_input_address(giver_address)
321+
builder.add_output(TransactionOutput(giver_address, 50000000))
322+
tx1 = builder.build_and_sign([self.payment_skey], giver_address)
323+
324+
utxo_to_spend = UTxO(
325+
TransactionInput(tx1.id, 0), tx1.transaction_body.outputs[0]
326+
)
327+
328+
builder = TransactionBuilder(self.chain_context)
329+
builder.add_input(utxo_to_spend)
330+
builder.add_output(TransactionOutput(giver_address, 25000000))
331+
tx2 = builder.build_and_sign([self.payment_skey], giver_address)
332+
333+
self.chain_context.submit_tx(tx1)
334+
self.chain_context.submit_tx(tx2)
335+
336+
@retry(tries=TEST_RETRIES, backoff=1.5, delay=6, jitter=(0, 4))
337+
@pytest.mark.post_alonzo
338+
def test_get_plutus_script(self):
339+
# ----------- Giver give ---------------
340+
with open("./plutus_scripts/fortytwoV2.plutus", "r") as f:
341+
script_hex = f.read()
342+
forty_two_script = PlutusV2Script(cbor2.loads(bytes.fromhex(script_hex)))
343+
344+
script_hash = plutus_script_hash(forty_two_script)
345+
346+
script_address = Address(script_hash, network=self.NETWORK)
347+
348+
giver_address = Address(self.payment_vkey.hash(), network=self.NETWORK)
349+
350+
builder = TransactionBuilder(self.chain_context)
351+
builder.add_input_address(giver_address)
352+
builder.add_output(
353+
TransactionOutput(script_address, 50000000, script=forty_two_script)
354+
)
355+
356+
signed_tx = builder.build_and_sign([self.payment_skey], giver_address)
357+
358+
print("############### Transaction created ###############")
359+
print(signed_tx)
360+
print(signed_tx.to_cbor_hex())
361+
print("############### Submitting transaction ###############")
362+
self.chain_context.submit_tx(signed_tx)
363+
time.sleep(3)
364+
365+
utxos = self.chain_context.utxos(script_address)
366+
367+
assert utxos[0].output.script == forty_two_script
368+
369+
370+
class TestPlutusOgmiosOnly(TestPlutus):
371+
@classmethod
372+
def setup_class(cls):
373+
cls.chain_context._kupo_url = None

pycardano/backend/ogmios.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from enum import Enum
55
from typing import Any, Dict, List, Optional, Tuple, Union
66

7-
import cbor2
87
import requests
98
import websocket
109
from cachetools import Cache, LRUCache, TTLCache, func
@@ -466,9 +465,9 @@ def _utxo_from_ogmios_result(self, result) -> UTxO:
466465
script = output.get("script", None)
467466
if script:
468467
if "plutus:v2" in script:
469-
script = PlutusV2Script(cbor2.loads(bytes.fromhex(script["plutus:v2"])))
468+
script = PlutusV2Script(bytes.fromhex(script["plutus:v2"]))
470469
elif "plutus:v1" in script:
471-
script = PlutusV1Script(cbor2.loads(bytes.fromhex(script["plutus:v1"])))
470+
script = PlutusV1Script(bytes.fromhex(script["plutus:v1"]))
472471
else:
473472
raise ValueError("Unknown plutus script type")
474473
datum_hash = (

pycardano/cip/cip8.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Optional, Union
22

3+
from cbor2 import CBORTag, dumps
34
from cose.algorithms import EdDSA
45
from cose.headers import KID, Algorithm
56
from cose.keys import CoseKey
@@ -10,13 +11,15 @@
1011
from cose.messages import CoseMessage, Sign1Message
1112

1213
from pycardano.address import Address
14+
from pycardano.crypto import BIP32ED25519PublicKey
1315
from pycardano.key import (
16+
ExtendedSigningKey,
17+
ExtendedVerificationKey,
1418
PaymentVerificationKey,
1519
SigningKey,
1620
StakeExtendedSigningKey,
1721
StakeSigningKey,
1822
StakeVerificationKey,
19-
VerificationKey,
2023
)
2124
from pycardano.network import Network
2225

@@ -25,7 +28,7 @@
2528

2629
def sign(
2730
message: str,
28-
signing_key: SigningKey,
31+
signing_key: Union[ExtendedSigningKey, SigningKey],
2932
attach_cose_key: bool = False,
3033
network: Network = Network.MAINNET,
3134
) -> Union[str, dict]:
@@ -45,7 +48,9 @@ def sign(
4548
"""
4649

4750
# derive the verification key
48-
verification_key = VerificationKey.from_signing_key(signing_key)
51+
verification_key = signing_key.to_verification_key()
52+
if isinstance(verification_key, ExtendedVerificationKey):
53+
verification_key = verification_key.to_non_extended()
4954

5055
if isinstance(signing_key, StakeSigningKey) or isinstance(
5156
signing_key, StakeExtendedSigningKey
@@ -85,7 +90,20 @@ def sign(
8590

8691
msg.key = cose_key # attach the key to the message
8792

88-
encoded = msg.encode()
93+
if isinstance(signing_key, ExtendedSigningKey):
94+
_message = [
95+
msg.phdr_encoded,
96+
msg.uhdr_encoded,
97+
msg.payload,
98+
signing_key.sign(msg._sig_structure),
99+
]
100+
101+
encoded = dumps(
102+
CBORTag(msg.cbor_tag, _message), default=msg._custom_cbor_encoder
103+
)
104+
105+
else:
106+
encoded = msg.encode()
89107

90108
# turn the enocded message into a hex string and remove the first byte
91109
# which is always "d2"
@@ -108,7 +126,8 @@ def sign(
108126

109127

110128
def verify(
111-
signed_message: Union[str, dict], attach_cose_key: Optional[bool] = None
129+
signed_message: Union[str, dict],
130+
attach_cose_key: Optional[bool] = None,
112131
) -> dict:
113132
"""Verify the signature of a COSESign1 message and decode its contents following CIP-0008.
114133
Supports messages signed by browser wallets or `Message.sign()`.
@@ -175,7 +194,16 @@ def verify(
175194
# attach the key to the decoded message
176195
decoded_message.key = cose_key
177196

178-
signature_verified = decoded_message.verify_signature()
197+
if len(verification_key) > 32:
198+
vk = BIP32ED25519PublicKey(
199+
public_key=verification_key[:32], chain_code=verification_key[32:]
200+
)
201+
vk.verify(
202+
signature=decoded_message.signature, message=decoded_message._sig_structure
203+
)
204+
signature_verified = True
205+
else:
206+
signature_verified = decoded_message.verify_signature()
179207

180208
message = decoded_message.payload.decode("utf-8")
181209

pycardano/metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def _validate_type_and_size(data):
4141
if len(data) > self.MAX_ITEM_SIZE:
4242
raise InvalidArgumentException(
4343
f"The size of {data} exceeds {self.MAX_ITEM_SIZE} bytes."
44+
"Use pycardano.serialization.ByteString for long bytes."
4445
)
4546
elif isinstance(data, str):
4647
if len(data.encode("utf-8")) > self.MAX_ITEM_SIZE:

pycardano/plutus.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@
66
import json
77
from dataclasses import dataclass, field, fields
88
from enum import Enum
9-
from typing import Any, ClassVar, Optional, Type, Union
9+
from hashlib import sha256
10+
from typing import Any, Optional, Type, Union
1011

1112
import cbor2
1213
from cbor2 import CBORTag
1314
from nacl.encoding import RawEncoder
1415
from nacl.hash import blake2b
1516

16-
from pycardano.exception import DeserializeException
17+
from pycardano.exception import DeserializeException, InvalidArgumentException
1718
from pycardano.hash import DATUM_HASH_SIZE, SCRIPT_HASH_SIZE, DatumHash, ScriptHash
1819
from pycardano.nativescript import NativeScript
1920
from pycardano.serialization import (
2021
ArrayCBORSerializable,
22+
ByteString,
2123
CBORSerializable,
2224
DictCBORSerializable,
2325
IndefiniteList,
@@ -44,9 +46,16 @@
4446
"datum_hash",
4547
"plutus_script_hash",
4648
"script_hash",
49+
"Unit",
4750
]
4851

4952

53+
# taken from https://stackoverflow.com/a/13624858
54+
class classproperty(property):
55+
def __get__(self, owner_self, owner_cls):
56+
return self.fget(owner_cls)
57+
58+
5059
class CostModels(DictCBORSerializable):
5160
KEY_TYPE = int
5261
VALUE_TYPE = dict
@@ -460,18 +469,43 @@ class will reduce the complexity of serialization and deserialization tremendous
460469
>>> assert test == Test.from_cbor("d87a9f187b43333231ff")
461470
"""
462471

463-
CONSTR_ID: ClassVar[int] = 0
464-
"""Constructor ID of this plutus data.
465-
It is primarily used by Plutus core to reconstruct a data structure from serialized CBOR bytes."""
472+
MAX_BYTES_SIZE = 64
473+
474+
@classproperty
475+
def CONSTR_ID(cls):
476+
"""
477+
Constructor ID of this plutus data.
478+
It is primarily used by Plutus core to reconstruct a data structure from serialized CBOR bytes.
479+
The default implementation is an almost unique, deterministic constructor ID in the range 1 - 2^32 based
480+
on class attributes, types and class name.
481+
"""
482+
k = f"_CONSTR_ID_{cls.__name__}"
483+
if not hasattr(cls, k):
484+
det_string = (
485+
cls.__name__
486+
+ "*"
487+
+ "*".join([f"{f.name}~{f.type}" for f in fields(cls)])
488+
)
489+
det_hash = sha256(det_string.encode("utf8")).hexdigest()
490+
setattr(cls, k, int(det_hash, 16) % 2**32)
491+
492+
return getattr(cls, k)
466493

467494
def __post_init__(self):
468-
valid_types = (PlutusData, dict, IndefiniteList, int, bytes)
495+
valid_types = (PlutusData, dict, IndefiniteList, int, ByteString, bytes)
469496
for f in fields(self):
470497
if inspect.isclass(f.type) and not issubclass(f.type, valid_types):
471498
raise TypeError(
472499
f"Invalid field type: {f.type}. A field in PlutusData should be one of {valid_types}"
473500
)
474501

502+
data = getattr(self, f.name)
503+
if isinstance(data, bytes) and len(data) > 64:
504+
raise InvalidArgumentException(
505+
f"The size of {data} exceeds {self.MAX_BYTES_SIZE} bytes. "
506+
"Use pycardano.serialization.ByteString for long bytes."
507+
)
508+
475509
def to_shallow_primitive(self) -> CBORTag:
476510
primitives: Primitive = super().to_shallow_primitive()
477511
if primitives:
@@ -529,6 +563,8 @@ def _dfs(obj):
529563
return {"int": obj}
530564
elif isinstance(obj, bytes):
531565
return {"bytes": obj.hex()}
566+
elif isinstance(obj, ByteString):
567+
return {"bytes": obj.value.hex()}
532568
elif isinstance(obj, IndefiniteList) or isinstance(obj, list):
533569
return {"list": [_dfs(item) for item in obj]}
534570
elif isinstance(obj, dict):
@@ -643,7 +679,10 @@ def _dfs(obj):
643679
elif "int" in obj:
644680
return obj["int"]
645681
elif "bytes" in obj:
646-
return bytes.fromhex(obj["bytes"])
682+
if len(obj["bytes"]) > 64:
683+
return ByteString(bytes.fromhex(obj["bytes"]))
684+
else:
685+
return bytes.fromhex(obj["bytes"])
647686
elif "list" in obj:
648687
return IndefiniteList([_dfs(item) for item in obj["list"]])
649688
else:
@@ -820,3 +859,10 @@ def script_hash(script: ScriptType) -> ScriptHash:
820859
)
821860
else:
822861
raise TypeError(f"Unexpected script type: {type(script)}")
862+
863+
864+
@dataclass
865+
class Unit(PlutusData):
866+
"""The default "Unit type" with a 0 constructor ID"""
867+
868+
CONSTR_ID = 0

pycardano/serialization.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ class IndefiniteFrozenList(FrozenList, IndefiniteList): # type: ignore
6060
pass
6161

6262

63+
@dataclass
64+
class ByteString:
65+
value: bytes
66+
67+
def __hash__(self):
68+
return hash(self.value)
69+
70+
def __eq__(self, other: object):
71+
if isinstance(other, ByteString):
72+
return self.value == other.value
73+
elif isinstance(other, bytes):
74+
return self.value == other
75+
else:
76+
return False
77+
78+
6379
@dataclass
6480
class RawCBOR:
6581
"""A wrapper class for bytes that represents a CBOR value."""
@@ -160,6 +176,7 @@ def default_encoder(
160176
assert isinstance(
161177
value,
162178
(
179+
ByteString,
163180
CBORSerializable,
164181
IndefiniteList,
165182
RawCBOR,
@@ -178,6 +195,15 @@ def default_encoder(
178195
for item in value:
179196
encoder.encode(item)
180197
encoder.write(b"\xff")
198+
elif isinstance(value, ByteString):
199+
if len(value.value) > 64:
200+
encoder.write(b"\x5f")
201+
for i in range(0, len(value.value), 64):
202+
imax = min(i + 64, len(value.value))
203+
encoder.encode(value.value[i:imax])
204+
encoder.write(b"\xff")
205+
else:
206+
encoder.encode(value.value)
181207
elif isinstance(value, RawCBOR):
182208
encoder.write(value.cbor)
183209
elif isinstance(value, FrozenList):

pycardano/txbuilder.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,13 @@ def _add_collateral_input(cur_total, candidate_inputs):
11481148
)
11491149
_add_collateral_input(tmp_val, sorted_inputs)
11501150

1151+
if tmp_val.coin < collateral_amount:
1152+
sorted_inputs = sorted(
1153+
self.potential_inputs,
1154+
key=lambda i: (len(i.output.to_cbor_hex()), -i.output.amount.coin),
1155+
)
1156+
_add_collateral_input(tmp_val, sorted_inputs)
1157+
11511158
if tmp_val.coin < collateral_amount:
11521159
sorted_inputs = sorted(
11531160
self.context.utxos(collateral_return_address),

0 commit comments

Comments
 (0)