Skip to content

Commit

Permalink
Change the default calculation of min_lovelace to post alonzo
Browse files Browse the repository at this point in the history
  • Loading branch information
cffls committed Oct 1, 2022
1 parent 8082aa4 commit 580202f
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 33 deletions.
8 changes: 6 additions & 2 deletions examples/native_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
NETWORK = Network.TESTNET

chain_context = BlockFrostChainContext(
project_id=BLOCK_FROST_PROJECT_ID, network=NETWORK
project_id=BLOCK_FROST_PROJECT_ID,
network=NETWORK,
base_url="https://cardano-preprod.blockfrost.io/api",
)

"""Preparation"""
Expand Down Expand Up @@ -153,7 +155,9 @@ def load_or_create_key_pair(base_dir, base_name):
builder.auxiliary_data = auxiliary_data

# Calculate the minimum amount of lovelace that need to hold the NFT we are going to mint
min_val = min_lovelace_pre_alonzo(Value(0, my_nft), chain_context)
min_val = min_lovelace(
chain_context, output=TransactionOutput(address, Value(0, my_nft))
)

# Send the NFT to our own address
builder.add_output(TransactionOutput(address, Value(min_val, my_nft)))
Expand Down
107 changes: 107 additions & 0 deletions integration-test/test/test_min_utxo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import pathlib
import tempfile
from dataclasses import dataclass

import cbor2
import pytest
from retry import retry

from pycardano import *

from .base import TEST_RETRIES, TestBase


class TestMint(TestBase):
@retry(tries=TEST_RETRIES, backoff=1.5, delay=6, jitter=(0, 4))
@pytest.mark.post_alonzo
def test_min_utxo(self):
address = Address(self.payment_vkey.hash(), network=self.NETWORK)

with open("./plutus_scripts/always_succeeds.plutus", "r") as f:
script_hex = f.read()
anymint_script = PlutusV1Script(cbor2.loads(bytes.fromhex(script_hex)))

policy_id = plutus_script_hash(anymint_script)

my_nft = MultiAsset.from_primitive(
{
policy_id.payload: {
b"MY_SCRIPT_NFT_1": 1, # Name of our NFT1 # Quantity of this NFT
b"MY_SCRIPT_NFT_2": 1, # Name of our NFT2 # Quantity of this NFT
}
}
)

metadata = {
721: {
policy_id.payload.hex(): {
"MY_SCRIPT_NFT_1": {
"description": "This is my first NFT thanks to PyCardano",
"name": "PyCardano NFT example token 1",
"id": 1,
"image": "ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw",
},
"MY_SCRIPT_NFT_2": {
"description": "This is my second NFT thanks to PyCardano",
"name": "PyCardano NFT example token 2",
"id": 2,
"image": "ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw",
},
}
}
}

# Place metadata in AuxiliaryData, the format acceptable by a transaction.
auxiliary_data = AuxiliaryData(AlonzoMetadata(metadata=Metadata(metadata)))

# Create a transaction builder
builder = TransactionBuilder(self.chain_context)

# Add our own address as the input address
builder.add_input_address(address)

@dataclass
class MyPlutusData(PlutusData):
a: int

# Add minting script with an empty datum and a minting redeemer
builder.add_minting_script(
anymint_script, redeemer=Redeemer(RedeemerTag.MINT, MyPlutusData(a=42))
)

# Set nft we want to mint
builder.mint = my_nft

# Set transaction metadata
builder.auxiliary_data = auxiliary_data

# Calculate the minimum amount of lovelace that need to hold the NFT we are going to mint
min_val = min_lovelace(
output=TransactionOutput(address, Value(0, my_nft)),
context=self.chain_context,
)

# Send the NFT to our own address
nft_output = TransactionOutput(address, Value(min_val, my_nft))
pure_ada_output = TransactionOutput(
address,
min_lovelace(
context=self.chain_context, output=TransactionOutput(address, 0)
),
)
builder.add_output(nft_output)
builder.add_output(pure_ada_output)

# Build and sign transaction
signed_tx = builder.build_and_sign([self.payment_skey], address)
# signed_tx.transaction_witness_set.plutus_data

print("############### Transaction created ###############")
print(signed_tx)
print(signed_tx.to_cbor())

# Submit signed transaction to the network
print("############### Submitting transaction ###############")
self.chain_context.submit_tx(signed_tx.to_cbor())

self.assert_output(address, nft_output)
2 changes: 1 addition & 1 deletion integration-test/test/test_mint.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ class MyPlutusData(PlutusData):

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

Expand Down
7 changes: 6 additions & 1 deletion pycardano/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
from pycardano.plutus import ExecutionUnits
from pycardano.transaction import UTxO

__all__ = ["GenesisParameters", "ProtocolParameters", "ChainContext", "ALONZO_COINS_PER_UTXO_WORD"]
__all__ = [
"GenesisParameters",
"ProtocolParameters",
"ChainContext",
"ALONZO_COINS_PER_UTXO_WORD",
]

ALONZO_COINS_PER_UTXO_WORD = 34482

Expand Down
7 changes: 6 additions & 1 deletion pycardano/backend/blockfrost.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from blockfrost import ApiUrls, BlockFrostApi

from pycardano.address import Address
from pycardano.backend.base import ChainContext, GenesisParameters, ProtocolParameters, ALONZO_COINS_PER_UTXO_WORD
from pycardano.backend.base import (
ALONZO_COINS_PER_UTXO_WORD,
ChainContext,
GenesisParameters,
ProtocolParameters,
)
from pycardano.exception import TransactionFailedException
from pycardano.hash import SCRIPT_HASH_SIZE, DatumHash, ScriptHash
from pycardano.nativescript import NativeScript
Expand Down
21 changes: 15 additions & 6 deletions pycardano/coinselection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import random
from typing import Iterable, List, Optional, Tuple

from pycardano.address import Address
from pycardano.backend.base import ChainContext
from pycardano.exception import (
InputUTxODepletedException,
Expand All @@ -13,10 +14,14 @@
UTxOSelectionException,
)
from pycardano.transaction import TransactionOutput, UTxO, Value
from pycardano.utils import max_tx_fee, min_lovelace_pre_alonzo
from pycardano.utils import max_tx_fee, min_lovelace_post_alonzo

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

_FAKE_ADDR = Address.from_primitive(
"addr1q8m9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwta8k2v59pcduem5uw253zwke30x9mwes62kfvqnzg38kuh6q966kg7"
)


class UTxOSelector:
"""UTxOSelector defines an interface through which a subset of UTxOs should be selected from a parent set
Expand Down Expand Up @@ -75,9 +80,9 @@ def select(
utxos: List[UTxO],
outputs: List[TransactionOutput],
context: ChainContext,
max_input_count: int = None,
include_max_fee: bool = True,
respect_min_utxo: bool = True,
max_input_count: Optional[int] = None,
include_max_fee: Optional[bool] = True,
respect_min_utxo: Optional[bool] = True,
) -> Tuple[List[UTxO], Value]:

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

if respect_min_utxo:
change = selected_amount - total_requested
min_change_amount = min_lovelace_pre_alonzo(change, context, False)
min_change_amount = min_lovelace_post_alonzo(
TransactionOutput(_FAKE_ADDR, change), context
)

if change.coin < min_change_amount:
additional, _ = self.select(
Expand Down Expand Up @@ -307,7 +314,9 @@ def select(

if respect_min_utxo:
change = selected_amount - request_sum
min_change_amount = min_lovelace_pre_alonzo(change, context, False)
min_change_amount = min_lovelace_post_alonzo(
TransactionOutput(_FAKE_ADDR, change), context
)

if change.coin < min_change_amount:
additional, _ = self.select(
Expand Down
4 changes: 3 additions & 1 deletion pycardano/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ class TransactionOutput(CBORSerializable):

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

post_alonzo: Optional[bool] = False

def __post_init__(self):
if isinstance(self.amount, int):
self.amount = Value(self.amount)
Expand Down Expand Up @@ -398,7 +400,7 @@ def lovelace(self) -> int:
return self.amount.coin

def to_primitive(self) -> Primitive:
if self.datum or self.script:
if self.datum or self.script or self.post_alonzo:
datum = (
_DatumOption(self.datum_hash or self.datum)
if self.datum is not None or self.datum_hash is not None
Expand Down
37 changes: 20 additions & 17 deletions pycardano/txbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,7 @@
Value,
Withdrawals,
)
from pycardano.utils import (
fee,
max_tx_fee,
min_lovelace_post_alonzo,
min_lovelace_pre_alonzo,
script_data_hash,
)
from pycardano.utils import fee, max_tx_fee, min_lovelace_post_alonzo, script_data_hash
from pycardano.witness import TransactionWitnessSet, VerificationKeyWitness

__all__ = ["TransactionBuilder"]
Expand Down Expand Up @@ -436,10 +430,12 @@ def _calc_change(

# when there is only ADA left, simply use remaining coin value as change
if not change.multi_asset:
if change.coin < min_lovelace_pre_alonzo(change, self.context):
if change.coin < min_lovelace_post_alonzo(
TransactionOutput(address, change), self.context
):
raise InsufficientUTxOBalanceException(
f"Not enough ADA left for change: {change.coin} but needs "
f"{min_lovelace_pre_alonzo(change, self.context)}"
f"{min_lovelace_post_alonzo(TransactionOutput(address, change), self.context)}"
)
lovelace_change = change.coin
change_output_arr.append(TransactionOutput(address, lovelace_change))
Expand All @@ -456,8 +452,8 @@ def _calc_change(
# Combine remainder of provided ADA with last MultiAsset for output
# There may be rare cases where adding ADA causes size exceeds limit
# We will revisit if it becomes an issue
if change.coin < min_lovelace_pre_alonzo(
Value(0, multi_asset), self.context
if change.coin < min_lovelace_post_alonzo(
TransactionOutput(address, Value(0, multi_asset)), self.context
):
raise InsufficientUTxOBalanceException(
"Not enough ADA left to cover non-ADA assets in a change address"
Expand All @@ -468,8 +464,8 @@ def _calc_change(
change_value = Value(change.coin, multi_asset)
else:
change_value = Value(0, multi_asset)
change_value.coin = min_lovelace_pre_alonzo(
change_value, self.context
change_value.coin = min_lovelace_post_alonzo(
TransactionOutput(address, change_value), self.context
)

change_output_arr.append(TransactionOutput(address, change_value))
Expand Down Expand Up @@ -560,7 +556,9 @@ def _adding_asset_make_output_overflow(
attempt_amount = new_amount + current_amount

# Calculate minimum ada requirements for more precise value size
required_lovelace = min_lovelace_pre_alonzo(attempt_amount, self.context)
required_lovelace = min_lovelace_post_alonzo(
TransactionOutput(output.address, attempt_amount), self.context
)
attempt_amount.coin = required_lovelace

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

# Calculate min lovelace required for more precise size
updated_amount = deepcopy(output.amount)
required_lovelace = min_lovelace_pre_alonzo(updated_amount, self.context)
required_lovelace = min_lovelace_post_alonzo(
TransactionOutput(change_address, updated_amount), self.context
)
updated_amount.coin = required_lovelace

if len(updated_amount.to_cbor("bytes")) > max_val_size:
Expand Down Expand Up @@ -903,8 +903,11 @@ def build(
unfulfilled_amount.coin = max(
0,
unfulfilled_amount.coin
+ min_lovelace_pre_alonzo(
selected_amount - trimmed_selected_amount, self.context
+ min_lovelace_post_alonzo(
TransactionOutput(
change_address, selected_amount - trimmed_selected_amount
),
self.context,
),
)
else:
Expand Down
19 changes: 18 additions & 1 deletion pycardano/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,25 @@ def min_lovelace_post_alonzo(output: TransactionOutput, context: ChainContext) -
int: Minimum required lovelace amount for this transaction output.
"""
constant_overhead = 160

amt = output.amount

# If the amount of ADA is 0, a default value of 1 ADA will be used
if amt.coin == 0:
amt.coin = 1000000

# Make sure we are using post-alonzo output
tmp_out = TransactionOutput(
output.address,
output.amount,
output.datum_hash,
output.datum,
output.script,
True,
)

return (
constant_overhead + len(output.to_cbor("bytes"))
constant_overhead + len(tmp_out.to_cbor("bytes"))
) * context.protocol_param.coins_per_utxo_byte


Expand Down
6 changes: 3 additions & 3 deletions test/pycardano/test_txbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,13 +367,13 @@ def test_tx_add_change_split_nfts(chain_context):
# Change output
[
sender_address.to_primitive(),
[1344798, {b"1111111111111111111111111111": {b"Token1": 1}}],
[1034400, {b"1111111111111111111111111111": {b"Token1": 1}}],
],
# Second change output from split due to change size limit exceed
# Fourth output as change
[
sender_address.to_primitive(),
[2482969, {b"1111111111111111111111111111": {b"Token2": 2}}],
[2793367, {b"1111111111111111111111111111": {b"Token2": 2}}],
],
],
2: 172233,
Expand Down Expand Up @@ -407,7 +407,7 @@ def test_tx_add_change_split_nfts_not_enough_add(chain_context):
# Add sender address as input
mint = {policy_id.payload: {b"Token3": 1}}
tx_builder.add_input_address(sender).add_output(
TransactionOutput.from_primitive([sender, 7000000])
TransactionOutput.from_primitive([sender, 8000000])
)
tx_builder.mint = MultiAsset.from_primitive(mint)
tx_builder.native_scripts = [script]
Expand Down

0 comments on commit 580202f

Please sign in to comment.