From 580202f23fe60b9591b19b61a4709cfabc4c5855 Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 30 Sep 2022 23:36:40 -0700 Subject: [PATCH] Change the default calculation of min_lovelace to post alonzo --- examples/native_token.py | 8 +- integration-test/test/test_min_utxo.py | 107 +++++++++++++++++++++++++ integration-test/test/test_mint.py | 2 +- pycardano/backend/base.py | 7 +- pycardano/backend/blockfrost.py | 7 +- pycardano/coinselection.py | 21 +++-- pycardano/transaction.py | 4 +- pycardano/txbuilder.py | 37 +++++---- pycardano/utils.py | 19 ++++- test/pycardano/test_txbuilder.py | 6 +- 10 files changed, 185 insertions(+), 33 deletions(-) create mode 100644 integration-test/test/test_min_utxo.py diff --git a/examples/native_token.py b/examples/native_token.py index 56a92aa8..99508aba 100644 --- a/examples/native_token.py +++ b/examples/native_token.py @@ -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""" @@ -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))) diff --git a/integration-test/test/test_min_utxo.py b/integration-test/test/test_min_utxo.py new file mode 100644 index 00000000..da431e5a --- /dev/null +++ b/integration-test/test/test_min_utxo.py @@ -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) diff --git a/integration-test/test/test_mint.py b/integration-test/test/test_mint.py index fd5534f7..7cda39a7 100644 --- a/integration-test/test/test_mint.py +++ b/integration-test/test/test_mint.py @@ -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, ) diff --git a/pycardano/backend/base.py b/pycardano/backend/base.py index 90ec837d..2a66e7ce 100644 --- a/pycardano/backend/base.py +++ b/pycardano/backend/base.py @@ -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 diff --git a/pycardano/backend/blockfrost.py b/pycardano/backend/blockfrost.py index 875b2f58..2ec0ad83 100644 --- a/pycardano/backend/blockfrost.py +++ b/pycardano/backend/blockfrost.py @@ -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 diff --git a/pycardano/coinselection.py b/pycardano/coinselection.py index 7bc9e324..77201218 100644 --- a/pycardano/coinselection.py +++ b/pycardano/coinselection.py @@ -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, @@ -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 @@ -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) @@ -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( @@ -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( diff --git a/pycardano/transaction.py b/pycardano/transaction.py index c85002d7..0a512365 100644 --- a/pycardano/transaction.py +++ b/pycardano/transaction.py @@ -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) @@ -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 diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 2186de67..6877f369 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -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"] @@ -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)) @@ -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" @@ -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)) @@ -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 @@ -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: @@ -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: diff --git a/pycardano/utils.py b/pycardano/utils.py index 22564fa6..eeaf3d82 100644 --- a/pycardano/utils.py +++ b/pycardano/utils.py @@ -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 diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 653f372a..e299d9cd 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -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, @@ -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]