Skip to content

Commit 580202f

Browse files
committed
Change the default calculation of min_lovelace to post alonzo
1 parent 8082aa4 commit 580202f

File tree

10 files changed

+185
-33
lines changed

10 files changed

+185
-33
lines changed

examples/native_token.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
NETWORK = Network.TESTNET
1616

1717
chain_context = BlockFrostChainContext(
18-
project_id=BLOCK_FROST_PROJECT_ID, network=NETWORK
18+
project_id=BLOCK_FROST_PROJECT_ID,
19+
network=NETWORK,
20+
base_url="https://cardano-preprod.blockfrost.io/api",
1921
)
2022

2123
"""Preparation"""
@@ -153,7 +155,9 @@ def load_or_create_key_pair(base_dir, base_name):
153155
builder.auxiliary_data = auxiliary_data
154156

155157
# Calculate the minimum amount of lovelace that need to hold the NFT we are going to mint
156-
min_val = min_lovelace_pre_alonzo(Value(0, my_nft), chain_context)
158+
min_val = min_lovelace(
159+
chain_context, output=TransactionOutput(address, Value(0, my_nft))
160+
)
157161

158162
# Send the NFT to our own address
159163
builder.add_output(TransactionOutput(address, Value(min_val, my_nft)))
+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import pathlib
2+
import tempfile
3+
from dataclasses import dataclass
4+
5+
import cbor2
6+
import pytest
7+
from retry import retry
8+
9+
from pycardano import *
10+
11+
from .base import TEST_RETRIES, TestBase
12+
13+
14+
class TestMint(TestBase):
15+
@retry(tries=TEST_RETRIES, backoff=1.5, delay=6, jitter=(0, 4))
16+
@pytest.mark.post_alonzo
17+
def test_min_utxo(self):
18+
address = Address(self.payment_vkey.hash(), network=self.NETWORK)
19+
20+
with open("./plutus_scripts/always_succeeds.plutus", "r") as f:
21+
script_hex = f.read()
22+
anymint_script = PlutusV1Script(cbor2.loads(bytes.fromhex(script_hex)))
23+
24+
policy_id = plutus_script_hash(anymint_script)
25+
26+
my_nft = MultiAsset.from_primitive(
27+
{
28+
policy_id.payload: {
29+
b"MY_SCRIPT_NFT_1": 1, # Name of our NFT1 # Quantity of this NFT
30+
b"MY_SCRIPT_NFT_2": 1, # Name of our NFT2 # Quantity of this NFT
31+
}
32+
}
33+
)
34+
35+
metadata = {
36+
721: {
37+
policy_id.payload.hex(): {
38+
"MY_SCRIPT_NFT_1": {
39+
"description": "This is my first NFT thanks to PyCardano",
40+
"name": "PyCardano NFT example token 1",
41+
"id": 1,
42+
"image": "ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw",
43+
},
44+
"MY_SCRIPT_NFT_2": {
45+
"description": "This is my second NFT thanks to PyCardano",
46+
"name": "PyCardano NFT example token 2",
47+
"id": 2,
48+
"image": "ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw",
49+
},
50+
}
51+
}
52+
}
53+
54+
# Place metadata in AuxiliaryData, the format acceptable by a transaction.
55+
auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(metadata)))
56+
57+
# Create a transaction builder
58+
builder = TransactionBuilder(self.chain_context)
59+
60+
# Add our own address as the input address
61+
builder.add_input_address(address)
62+
63+
@dataclass
64+
class MyPlutusData(PlutusData):
65+
a: int
66+
67+
# Add minting script with an empty datum and a minting redeemer
68+
builder.add_minting_script(
69+
anymint_script, redeemer=Redeemer(RedeemerTag.MINT, MyPlutusData(a=42))
70+
)
71+
72+
# Set nft we want to mint
73+
builder.mint = my_nft
74+
75+
# Set transaction metadata
76+
builder.auxiliary_data = auxiliary_data
77+
78+
# Calculate the minimum amount of lovelace that need to hold the NFT we are going to mint
79+
min_val = min_lovelace(
80+
output=TransactionOutput(address, Value(0, my_nft)),
81+
context=self.chain_context,
82+
)
83+
84+
# Send the NFT to our own address
85+
nft_output = TransactionOutput(address, Value(min_val, my_nft))
86+
pure_ada_output = TransactionOutput(
87+
address,
88+
min_lovelace(
89+
context=self.chain_context, output=TransactionOutput(address, 0)
90+
),
91+
)
92+
builder.add_output(nft_output)
93+
builder.add_output(pure_ada_output)
94+
95+
# Build and sign transaction
96+
signed_tx = builder.build_and_sign([self.payment_skey], address)
97+
# signed_tx.transaction_witness_set.plutus_data
98+
99+
print("############### Transaction created ###############")
100+
print(signed_tx)
101+
print(signed_tx.to_cbor())
102+
103+
# Submit signed transaction to the network
104+
print("############### Submitting transaction ###############")
105+
self.chain_context.submit_tx(signed_tx.to_cbor())
106+
107+
self.assert_output(address, nft_output)

integration-test/test/test_mint.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ class MyPlutusData(PlutusData):
322322

323323
# Calculate the minimum amount of lovelace that need to hold the NFT we are going to mint
324324
min_val = min_lovelace(
325-
output=TransactionOutput(address, Value(1000000, my_nft)),
325+
output=TransactionOutput(address, Value(0, my_nft)),
326326
context=self.chain_context,
327327
)
328328

pycardano/backend/base.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
from pycardano.plutus import ExecutionUnits
1010
from pycardano.transaction import UTxO
1111

12-
__all__ = ["GenesisParameters", "ProtocolParameters", "ChainContext", "ALONZO_COINS_PER_UTXO_WORD"]
12+
__all__ = [
13+
"GenesisParameters",
14+
"ProtocolParameters",
15+
"ChainContext",
16+
"ALONZO_COINS_PER_UTXO_WORD",
17+
]
1318

1419
ALONZO_COINS_PER_UTXO_WORD = 34482
1520

pycardano/backend/blockfrost.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
from blockfrost import ApiUrls, BlockFrostApi
88

99
from pycardano.address import Address
10-
from pycardano.backend.base import ChainContext, GenesisParameters, ProtocolParameters, ALONZO_COINS_PER_UTXO_WORD
10+
from pycardano.backend.base import (
11+
ALONZO_COINS_PER_UTXO_WORD,
12+
ChainContext,
13+
GenesisParameters,
14+
ProtocolParameters,
15+
)
1116
from pycardano.exception import TransactionFailedException
1217
from pycardano.hash import SCRIPT_HASH_SIZE, DatumHash, ScriptHash
1318
from pycardano.nativescript import NativeScript

pycardano/coinselection.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import random
66
from typing import Iterable, List, Optional, Tuple
77

8+
from pycardano.address import Address
89
from pycardano.backend.base import ChainContext
910
from pycardano.exception import (
1011
InputUTxODepletedException,
@@ -13,10 +14,14 @@
1314
UTxOSelectionException,
1415
)
1516
from pycardano.transaction import TransactionOutput, UTxO, Value
16-
from pycardano.utils import max_tx_fee, min_lovelace_pre_alonzo
17+
from pycardano.utils import max_tx_fee, min_lovelace_post_alonzo
1718

1819
__all__ = ["UTxOSelector", "LargestFirstSelector", "RandomImproveMultiAsset"]
1920

21+
_FAKE_ADDR = Address.from_primitive(
22+
"addr1q8m9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwta8k2v59pcduem5uw253zwke30x9mwes62kfvqnzg38kuh6q966kg7"
23+
)
24+
2025

2126
class UTxOSelector:
2227
"""UTxOSelector defines an interface through which a subset of UTxOs should be selected from a parent set
@@ -75,9 +80,9 @@ def select(
7580
utxos: List[UTxO],
7681
outputs: List[TransactionOutput],
7782
context: ChainContext,
78-
max_input_count: int = None,
79-
include_max_fee: bool = True,
80-
respect_min_utxo: bool = True,
83+
max_input_count: Optional[int] = None,
84+
include_max_fee: Optional[bool] = True,
85+
respect_min_utxo: Optional[bool] = True,
8186
) -> Tuple[List[UTxO], Value]:
8287

8388
available: List[UTxO] = sorted(utxos, key=lambda utxo: utxo.output.lovelace)
@@ -103,7 +108,9 @@ def select(
103108

104109
if respect_min_utxo:
105110
change = selected_amount - total_requested
106-
min_change_amount = min_lovelace_pre_alonzo(change, context, False)
111+
min_change_amount = min_lovelace_post_alonzo(
112+
TransactionOutput(_FAKE_ADDR, change), context
113+
)
107114

108115
if change.coin < min_change_amount:
109116
additional, _ = self.select(
@@ -307,7 +314,9 @@ def select(
307314

308315
if respect_min_utxo:
309316
change = selected_amount - request_sum
310-
min_change_amount = min_lovelace_pre_alonzo(change, context, False)
317+
min_change_amount = min_lovelace_post_alonzo(
318+
TransactionOutput(_FAKE_ADDR, change), context
319+
)
311320

312321
if change.coin < min_change_amount:
313322
additional, _ = self.select(

pycardano/transaction.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ class TransactionOutput(CBORSerializable):
371371

372372
script: Optional[Union[NativeScript, PlutusV1Script, PlutusV2Script]] = None
373373

374+
post_alonzo: Optional[bool] = False
375+
374376
def __post_init__(self):
375377
if isinstance(self.amount, int):
376378
self.amount = Value(self.amount)
@@ -398,7 +400,7 @@ def lovelace(self) -> int:
398400
return self.amount.coin
399401

400402
def to_primitive(self) -> Primitive:
401-
if self.datum or self.script:
403+
if self.datum or self.script or self.post_alonzo:
402404
datum = (
403405
_DatumOption(self.datum_hash or self.datum)
404406
if self.datum is not None or self.datum_hash is not None

pycardano/txbuilder.py

+20-17
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,7 @@
5555
Value,
5656
Withdrawals,
5757
)
58-
from pycardano.utils import (
59-
fee,
60-
max_tx_fee,
61-
min_lovelace_post_alonzo,
62-
min_lovelace_pre_alonzo,
63-
script_data_hash,
64-
)
58+
from pycardano.utils import fee, max_tx_fee, min_lovelace_post_alonzo, script_data_hash
6559
from pycardano.witness import TransactionWitnessSet, VerificationKeyWitness
6660

6761
__all__ = ["TransactionBuilder"]
@@ -436,10 +430,12 @@ def _calc_change(
436430

437431
# when there is only ADA left, simply use remaining coin value as change
438432
if not change.multi_asset:
439-
if change.coin < min_lovelace_pre_alonzo(change, self.context):
433+
if change.coin < min_lovelace_post_alonzo(
434+
TransactionOutput(address, change), self.context
435+
):
440436
raise InsufficientUTxOBalanceException(
441437
f"Not enough ADA left for change: {change.coin} but needs "
442-
f"{min_lovelace_pre_alonzo(change, self.context)}"
438+
f"{min_lovelace_post_alonzo(TransactionOutput(address, change), self.context)}"
443439
)
444440
lovelace_change = change.coin
445441
change_output_arr.append(TransactionOutput(address, lovelace_change))
@@ -456,8 +452,8 @@ def _calc_change(
456452
# Combine remainder of provided ADA with last MultiAsset for output
457453
# There may be rare cases where adding ADA causes size exceeds limit
458454
# We will revisit if it becomes an issue
459-
if change.coin < min_lovelace_pre_alonzo(
460-
Value(0, multi_asset), self.context
455+
if change.coin < min_lovelace_post_alonzo(
456+
TransactionOutput(address, Value(0, multi_asset)), self.context
461457
):
462458
raise InsufficientUTxOBalanceException(
463459
"Not enough ADA left to cover non-ADA assets in a change address"
@@ -468,8 +464,8 @@ def _calc_change(
468464
change_value = Value(change.coin, multi_asset)
469465
else:
470466
change_value = Value(0, multi_asset)
471-
change_value.coin = min_lovelace_pre_alonzo(
472-
change_value, self.context
467+
change_value.coin = min_lovelace_post_alonzo(
468+
TransactionOutput(address, change_value), self.context
473469
)
474470

475471
change_output_arr.append(TransactionOutput(address, change_value))
@@ -560,7 +556,9 @@ def _adding_asset_make_output_overflow(
560556
attempt_amount = new_amount + current_amount
561557

562558
# Calculate minimum ada requirements for more precise value size
563-
required_lovelace = min_lovelace_pre_alonzo(attempt_amount, self.context)
559+
required_lovelace = min_lovelace_post_alonzo(
560+
TransactionOutput(output.address, attempt_amount), self.context
561+
)
564562
attempt_amount.coin = required_lovelace
565563

566564
return len(attempt_amount.to_cbor("bytes")) > max_val_size
@@ -617,7 +615,9 @@ def _pack_tokens_for_change(
617615

618616
# Calculate min lovelace required for more precise size
619617
updated_amount = deepcopy(output.amount)
620-
required_lovelace = min_lovelace_pre_alonzo(updated_amount, self.context)
618+
required_lovelace = min_lovelace_post_alonzo(
619+
TransactionOutput(change_address, updated_amount), self.context
620+
)
621621
updated_amount.coin = required_lovelace
622622

623623
if len(updated_amount.to_cbor("bytes")) > max_val_size:
@@ -903,8 +903,11 @@ def build(
903903
unfulfilled_amount.coin = max(
904904
0,
905905
unfulfilled_amount.coin
906-
+ min_lovelace_pre_alonzo(
907-
selected_amount - trimmed_selected_amount, self.context
906+
+ min_lovelace_post_alonzo(
907+
TransactionOutput(
908+
change_address, selected_amount - trimmed_selected_amount
909+
),
910+
self.context,
908911
),
909912
)
910913
else:

pycardano/utils.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,25 @@ def min_lovelace_post_alonzo(output: TransactionOutput, context: ChainContext) -
162162
int: Minimum required lovelace amount for this transaction output.
163163
"""
164164
constant_overhead = 160
165+
166+
amt = output.amount
167+
168+
# If the amount of ADA is 0, a default value of 1 ADA will be used
169+
if amt.coin == 0:
170+
amt.coin = 1000000
171+
172+
# Make sure we are using post-alonzo output
173+
tmp_out = TransactionOutput(
174+
output.address,
175+
output.amount,
176+
output.datum_hash,
177+
output.datum,
178+
output.script,
179+
True,
180+
)
181+
165182
return (
166-
constant_overhead + len(output.to_cbor("bytes"))
183+
constant_overhead + len(tmp_out.to_cbor("bytes"))
167184
) * context.protocol_param.coins_per_utxo_byte
168185

169186

test/pycardano/test_txbuilder.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -367,13 +367,13 @@ def test_tx_add_change_split_nfts(chain_context):
367367
# Change output
368368
[
369369
sender_address.to_primitive(),
370-
[1344798, {b"1111111111111111111111111111": {b"Token1": 1}}],
370+
[1034400, {b"1111111111111111111111111111": {b"Token1": 1}}],
371371
],
372372
# Second change output from split due to change size limit exceed
373373
# Fourth output as change
374374
[
375375
sender_address.to_primitive(),
376-
[2482969, {b"1111111111111111111111111111": {b"Token2": 2}}],
376+
[2793367, {b"1111111111111111111111111111": {b"Token2": 2}}],
377377
],
378378
],
379379
2: 172233,
@@ -407,7 +407,7 @@ def test_tx_add_change_split_nfts_not_enough_add(chain_context):
407407
# Add sender address as input
408408
mint = {policy_id.payload: {b"Token3": 1}}
409409
tx_builder.add_input_address(sender).add_output(
410-
TransactionOutput.from_primitive([sender, 7000000])
410+
TransactionOutput.from_primitive([sender, 8000000])
411411
)
412412
tx_builder.mint = MultiAsset.from_primitive(mint)
413413
tx_builder.native_scripts = [script]

0 commit comments

Comments
 (0)