diff --git a/cardano-api/cardano-api.cabal b/cardano-api/cardano-api.cabal index a2d21fb32..899d8c6a1 100644 --- a/cardano-api/cardano-api.cabal +++ b/cardano-api/cardano-api.cabal @@ -359,6 +359,7 @@ test-suite cardano-api-test Test.Cardano.Api.Envelope Test.Cardano.Api.EpochLeadership Test.Cardano.Api.Eras + Test.Cardano.Api.Experimental Test.Cardano.Api.Genesis Test.Cardano.Api.IO Test.Cardano.Api.Json diff --git a/cardano-api/internal/Cardano/Api/Experimental/Tx.hs b/cardano-api/internal/Cardano/Api/Experimental/Tx.hs index 9f4dfe972..f5c7491a3 100644 --- a/cardano-api/internal/Cardano/Api/Experimental/Tx.hs +++ b/cardano-api/internal/Cardano/Api/Experimental/Tx.hs @@ -10,7 +10,142 @@ {-# LANGUAGE UndecidableInstances #-} module Cardano.Api.Experimental.Tx - ( UnsignedTx (..) + ( -- * Creating transactions using the new and the old API + + -- | + -- Both the old and the new API can be used to create transactions, and + -- it is possible to transform a transaction created in one format to the other + -- since they have the same representation underneath. But we will be moving + -- towards using the new API and deprecating the old way, since the latter is + -- simpler, closer to the ledger, and easier to maintain. + -- + -- In both the new and the old API, in order to construct a transaction, + -- we need to construct a 'TxBodyContent', and we will need at least a + -- witness (for example, a 'ShelleyWitnessSigningKey'), to sign the transaction. + -- This hasn't changed. + -- + -- In the following examples, we are using the following qualified modules: + -- + -- @ + -- import qualified Cardano.Api as Api -- the general `cardano-api` exports (including the old API) + -- import qualified Cardano.Api.Script as Script -- types related to scripts (Plutus and native) + -- import qualified Cardano.Api.Ledger as Ledger -- cardano-ledger re-exports + -- import qualified Cardano.Api.Experimental as Exp -- the experimental API + -- @ + -- + -- You can find a compilable version of these examples in "Test.Cardano.Api.Experimental". + + -- ** Creating a 'TxBodyContent' + + -- | + -- Independently of whether we use the Experimental or the traditoinal API, we need to create a 'TxBodyContent'. + -- + -- You can see how to do this in the documentation of the "Cardano.Api.Tx.Body" module. + + -- ** Balancing a transaction + + -- | + -- If we have a UTxO with exactly 12 ADA, and 2 ADA is enough for the fees, we could just construct + -- the transaction like in the previous section directly, and it could be a valid transaction. But still, + -- even if we have exactly 12 ADA, it is unlikely the fees required are exactly 2 ADA, so either we made + -- an invalid transaction, or we are wasting ADA on fees. Also, we may not always want to send all the ADA + -- in the UTxO. So that is were transaction balancing comes in. + -- + -- You can see how to balance a transaction in the documentation of the "Cardano.Api.Fees" module. + + -- ** Creating a 'ShelleyWitnessSigningKey' + + -- | + -- To sign the transaction, we need a witness. For example, a 'ShelleyWitnessSigningKey'. + -- + -- There are several ways of doing this, and several ways of representing a signing key. But let us assume + -- we have the bech32 representation of the signing key. In that case we can use the 'deserialiseFromBech32' function + -- as follows: + -- + -- @ + -- let (Right signingKey) = Api.deserialiseFromBech32 (Api.AsSigningKey Api.AsPaymentKey) "addr_sk1648253w4tf6fv5fk28dc7crsjsaw7d9ymhztd4favg3cwkhz7x8sl5u3ms" + -- @ + -- + -- Then we simply wrap the signing key in a 'ShelleyWitnessSigningKey' value: + -- + -- @ + -- let witness = Api.WitnessPaymentKey signingKey + -- @ + -- + -- We could do it analogously if we wanted to use an extended key, for example, using 'AsPaymentExtendedKey' and 'WitnessPaymentExtendedKey'. + + -- ** Creating a transaction using the old API + + -- | + -- Now that we have a 'TxBodyContent' and a 'ShelleyWitnessSigningKey', we can create a transaction using the old API + -- easily. First, we create a transaction body using the 'createTransactionBody' function and the 'ShelleyBasedEra' witness + -- that we defined earlier. + -- + -- We create the transaction body using the 'TransactionBodyContent' that we created earlier: + -- + -- @ + -- let (Right txBody) = Api.createTransactionBody sbe txBodyContent + -- @ + -- + -- Then, we sign the transaction using the 'signShelleyTransaction' function and the witness: + -- + -- @ + -- let oldApiSignedTx :: Api.Tx Api.ConwayEra = Api.signShelleyTransaction sbe txBody [witness] + -- @ + -- + -- And that is it. We have a signed transaction. + + -- ** Creating a transaction using the new API + + -- | + -- Now, let's see how we can create a transaction using the new API. First, we create an 'UnsignedTx' using the 'makeUnsignedTx' + -- function and the 'Era' and 'TxBodyContent' that we defined earlier: + -- + -- @ + -- let (Right unsignedTx) = Exp.makeUnsignedTx era txBodyContent + -- @ + -- + -- Then we use the key witness to witness the current unsigned transaction using the 'makeKeyWitness' function: + -- + -- @ + -- let transactionWitness = Exp.makeKeyWitness era unsignedTx (Api.WitnessPaymentKey signingKey) + -- @ + -- + -- Finally, we sign the transaction using the 'signTx' function: + -- + -- @ + -- let newApiSignedTx :: Ledger.Tx (Exp.LedgerEra Exp.ConwayEra) = Exp.signTx era [] [transactionWitness] unsignedTx + -- @ + -- + -- Where the empty list is for the bootstrap witnesses, which, in this case, we don't have any. + -- + -- And that is it. We have a signed transaction. + + -- ** Converting a transaction from the new API to the old API + + -- | + -- If we have a transaction created using the new API, we can convert it to the old API very easily by + -- just wrapping it using the 'ShelleyTx' constructor: + -- + -- @ + -- let oldStyleTx :: Api.Tx Api.ConwayEra = ShelleyTx sbe newApiSignedTx + -- @ + + -- ** Inspecting transactions + + -- | + -- For deconstructing an old style 'TxBody' into a 'TxBodyContent', we can also use the + -- 'TxBody' pattern, but this cannot be used for constructing. For that we use 'ShelleyTxBody' + -- or 'createTransactionBody', like in the example. + -- + -- For extracting the 'TxBody' and the 'KeyWitness'es from an old style 'Tx', we can use + -- the lenses 'getTxBody' and 'getTxWitnesses' respectively, from "Cardano.Api". + -- + -- When using a 'Tx' created using the experimental API, you can extract the 'TxBody' and + -- 'TxWits' using the lenses 'txBody' and 'txWits' respectively, from "Cardano.Api.Ledger". + + -- * Contents + UnsignedTx (..) , UnsignedTxError (..) , makeUnsignedTx , makeKeyWitness diff --git a/cardano-api/internal/Cardano/Api/Fees.hs b/cardano-api/internal/Cardano/Api/Fees.hs index 61a4daf5b..47651a0fb 100644 --- a/cardano-api/internal/Cardano/Api/Fees.hs +++ b/cardano-api/internal/Cardano/Api/Fees.hs @@ -12,21 +12,306 @@ -- | Fee calculation module Cardano.Api.Fees - ( -- * Transaction fees + ( -- * Introduction + + -- + + -- | + -- The documentation of the "Cardano.Api.Tx.Body" module shows how to create a 'TxBodyContent' for a + -- transaction that takes 12 ADA and sends 10 ADA to an address, and spends 2 ADA on fees. If we have a + -- UTxO with exactly 12 ADA, and 2 ADA is enough for the fees, this could be a valid transaction. But still, + -- even if we have exactly 12 ADA, it is unlikely that the fees required are exactly 2 ADA, so either we + -- made an invalid transaction, or we are wasting ADA on fees. + -- + -- On the other hand, we don't always want to spend all the ADA from a UTxO. Balancing a transaction allows + -- us to send the amount we want to send, pay only the necessary fees (or little more), and it + -- allows us to calculate the amount of extra currency that we can send back to a change address. + -- + -- Because changes to the transaction body will potentially change the amount of fees needed by a transaction, + -- finding this balance can be quite tricky. But fortunately, we have functions that help achieve this. + -- + -- This module provides several different ways of calculating the fees for a transaction or altogether + -- balancing a draft transaciton, which require different amounts of information, and provide different levels + -- of accuracy and automation. + -- + -- Next, we explore three examples of different and representative ways of calculating the fees for a transaction. + -- There are some other ways, but they have similar requirements. + -- + -- In these examples, we are using the following qualified modules: + -- + -- @ + -- import qualified Cardano.Api as Api -- the general `cardano-api` exports + -- @ + + -- ** Example 1: Simple fee calculation and manual balancing + + -- | + -- This method requires very little information, and it requires a little manual + -- work. + -- + -- It simply calculates the fees for a transaction, and then we need to do the + -- balancing ourselves. + -- + -- We just need to know: + -- + -- 1. The shelley based era witness for the current era. We can get it by using 'shelleyBasedEra'. + -- + -- @ + -- let sbe :: Api.ShelleyBasedEra Api.ConwayEra = Api.shelleyBasedEra + -- @ + -- + -- 2. The protocol parameters for the current era. We can get them by using the 'QueryProtocolParameters' + -- query defined in "Cardano.Api.Query". Let's assume we have them in the @exampleProtocolParams@ variable. + -- + -- 3. The draft transaction body. We can get it by using 'createTransactionBody' defined in "Cardano.Api.Tx.Body": + -- + -- @ + -- let (Right txBody) = Api.createTransactionBody sbe txBodyContent + -- @ + -- + -- 4. The number of Byron and Shelley key witnesses. This is just the number of keys that need + -- to sign the transaction. We can get an estimate by using 'estimateTransactionKeyWitnessCount' + -- but for a simple transaction that is using a single UTxO locked by a single modern Shelley key, + -- we would have @0@ Byron witnesses and @1@ Shelley witness. + -- + -- 5. Lastly, the size of the reference scripts in bytes, if any. For simple transactions, + -- this would be @0@. + -- + -- With this in mind we could estimate the fees required by our transaction by calling + -- 'evaluateTransactionFee' as follows: + -- + -- @ + -- let fees = Api.evaluateTransactionFee sbe exampleProtocolParams txBody 0 1 0 + -- @ + -- + -- Once we know the amount of fees required, we could balance the transaction by + -- substracting it from the total value of the UTxO we are spending. + -- + -- For example, if we have a UTxO with 12 ADA, the fees are 0.2 ADA, and we want + -- to send 10 ADA to an address. We could have a transaction with: + -- + -- * 1 input of 12 ADA (from the UTxO) + -- * 1 output of 10 ADA (to the address we want to pay) + -- * 1 output of 1.8 ADA (to the change address) + -- * 0.2 ADA in fees + -- + -- So, we would have to update our 'TxBodyContent' accordingly, and continue with the + -- transaction building as we demonstrate in the 'Cardano.Api.Experimental.Tx' module + + -- ** Example 2: Automated balancing without chain info (no UTxO, no ledger state) + + -- | + -- This method requires a bit more of information, but it does more work for us + -- automatically and we have to guess less. It also works with a 'TxBodyContent', + -- instead of a full 'TxBody'. + -- + -- We need the following info: + -- + -- 1. The MaryEraOnwards witness for the current era. In this case we are using the + -- one for 'Conway' era: + -- + -- @ + -- let meo = Api.MaryEraOnwardsConway + -- @ + -- + -- 2. The draft 'TxBodyContent' for the transaction we want to balance. In "Cardano.Api.Tx.Body" we show + -- how to create one. We assume we have it in the @txBodyContent@ variable. + -- + -- 3. The protocol parameters for the current era. We can get them by using the 'QueryProtocolParameters' + -- query defined in "Cardano.Api.Query". Let's assume we have them in the @exampleProtocolParams@ variable. + -- + -- 4. For stake pool and gov actions, we will also need: + -- + -- * The set of registered stake pools, that are being unregistered in this transaction. + -- * The map of all deposits for stake credentials that are being unregistered in this transaction. + -- * The map of all deposits for drep credentials that are being unregistered in this transaction. + -- * Plutus script execution units for all script witness we are used in the transaction. + -- + -- For this example, we are assuming we are doing nothing like that and using not scripts, + -- so we will use 'mempty' for the four. + -- + -- 5. Counts for: + -- + -- * The collateral amount we are depositing. This is only required for transactions that + -- have scripts in them. So we are assuming this will be @0@. + -- * The number of Shelley key witnesses still to be added to the transaction. We can get an + -- estimate by using 'estimateTransactionKeyWitnessCount' but for a simple transaction that is using + -- a single UTxO locked by a single modern Shelley key, we would have @1@ Shelley witness. + -- * The number of Byron key witnesses still to be added to the transaction. We are assuming + -- this will be @0@ for our transaction. + -- * Size of all reference scripts in bytes. Again, we are not using any reference scripts, so + -- this would be @0@. + -- + -- 6. The address where we want to send the change. We can deserialise it from its bech32 representation + -- by using the 'deserialiseAddress' function defined in "Cardano.Api.Address": + -- + -- @ + -- let Just exampleChangeAddress = + -- Api.deserialiseAddress + -- (Api.AsAddressInEra eraAsType) + -- "addr_test1vzpfxhjyjdlgk5c0xt8xw26avqxs52rtf69993j4tajehpcue4v2v" + -- @ + -- + -- Or we can get it from our signing key + -- + -- @ + -- let exampleChangeAddress = + -- Api.shelleyAddressInEra sbe $ + -- Api.makeShelleyAddress + -- (Api.Testnet $ Api.NetworkMagic 2) -- The magic for the network we are using + -- (Api.PaymentCredentialByKey + -- $ Api.verificationKeyHash + -- $ Api.getVerificationKey + -- signingKey) + -- Api.NoStakeAddress -- Potentially, the stake credential if we want to use one + -- @ + -- + -- 7. Finally, we need to know the total amount in the UTxOs we are spending. In our example, + -- we are spending 12 ADA, so we would have: + -- + -- @ + -- let totalUTxOValue = Api.lovelaceToValue 12_000_000 + -- @ + -- + -- With all this information, we could get a balanced transaction by calling 'estimateBalancedTxBody' + -- as follows: + -- + -- @ + -- let (Right (Api.BalancedTxBody + -- updatedTxBodyContent + -- updatedTxBody + -- changeOutput + -- fees)) = + -- Api.estimateBalancedTxBody + -- meo + -- txBodyContent + -- exampleProtocolParams + -- mempty + -- mempty + -- mempty + -- mempty + -- 0 + -- 1 + -- 0 + -- 0 + -- exampleChangeAddress + -- @ + -- + -- This will give us a balanced transaction, with the fees calculated, and the change output. + -- Now we can just proceed with transaction singing and submission. + + -- ** Example 3: Full automated balancing with chain info (requires UTxO and ledger state info) + + -- | + -- In the previous example, we had to provide a lot of information manually, which was + -- easy because our transaction was simple. But, for more complex transactions, that may have + -- scripts or other advanced functions (like governance actions), there is an approach that + -- will do many of the calculations for us in exchange for some general information about + -- the ledger. + -- + -- We need the following info: + -- + -- 1. The shelley based era witness for the current era. We can get it by using 'shelleyBasedEra'. + -- + -- @ + -- let sbe :: Api.ShelleyBasedEra Api.ConwayEra = Api.shelleyBasedEra + -- @ + -- + -- 2. The time the network started. We can obtain it using the 'QuerySystemStart' query + -- defined in "Cardano.Api.Query". Let's assume we have it in the @exampleSystemStart@ variable. + -- + -- 3. The ledger epoch info. We can obtain it by applying the 'toLedgerEpochInfo' function + -- to the 'EraHistory' that we can obtain using the 'QueryEraHistory' query defined in + -- "Cardano.Api.Query". Let's assume we have it in the @exampleLedgerEpochInfo@ variable. + -- + -- 4. The protocol parameters for the current era. We can get them by using the 'QueryProtocolParameters' + -- query defined in "Cardano.Api.Query". Let's assume we have them in the @exampleProtocolParams@ variable. + -- + -- 5. For stake pool and gov actions, we will also need: + -- + -- * The set of registered stake pools, that are being unregistered in this transaction. + -- * The map of all deposits for stake credentials that are being unregistered in this transaction. + -- * The map of all deposits for drep credentials that are being unregistered in this transaction. + -- + -- For this example, I am assuming we are doing nothing like that, so we will use 'mempty' for the three. + -- + -- 6. The part of the UTxO that we are spending. We can obtain the whole UTxO using the 'QueryUTxO' query + -- defined in "Cardano.Api.Query". Let's assume we have it in the @utxoToUse@ variable. + -- + -- 7. The draft 'TxBodyContent' for the transaction we want to balance. In "Cardano.Api.Tx.Body" we show + -- how to create one. + -- + -- 8. The address where we want to send the change. We can deserialise it from its bech32 representation + -- by using the 'deserialiseAddress' function defined in "Cardano.Api.Address": + -- + -- @ + -- let (Just exampleChangeAddress) = + -- Api.deserialiseAddress + -- (Api.AsAddressInEra eraAsType) + -- "addr_test1vzpfxhjyjdlgk5c0xt8xw26avqxs52rtf69993j4tajehpcue4v2v" + -- @ + -- + -- Or we can get it from our signing key + -- + -- @ + -- let exampleChangeAddress = + -- Api.shelleyAddressInEra sbe $ + -- Api.makeShelleyAddress + -- (Api.Testnet $ Api.NetworkMagic 2) -- The magic for the network we are using + -- (Api.PaymentCredentialByKey + -- $ Api.verificationKeyHash + -- $ Api.getVerificationKey + -- signingKey) + -- Api.NoStakeAddress -- Potentially, the stake credential if we want to use one + -- @ + -- + -- + -- 9. Finally, we have the opportunity to override the number of key witnesses that the transaction + -- will require. But the function will calculate them automatically if we set this to 'Nothing'. + -- + -- With all this information, we could get a balanced transaction by calling 'makeTransactionBodyAutoBalance' + -- as follows: + -- + -- @ + -- let Right (Api.BalancedTxBody + -- updatedTxBodyContent + -- updatedTxBody + -- changeOutput + -- fees) = + -- Api.makeTransactionBodyAutoBalance + -- sbe + -- exampleSystemStart + -- exampleLedgerEpochInfo + -- (Api.LedgerProtocolParameters exampleProtocolParams) + -- mempty + -- mempty + -- mempty + -- utxoToUse + -- txBodyContent + -- exampleChangeAddress + -- Nothing + -- @ + -- + -- This will give us a balanced transaction, with the fees calculated, and the change output. + -- Now we can just proceed with transaction singing and submission. + + -- * Contents + + -- ** Transaction fees evaluateTransactionFee , calculateMinTxFee , estimateTransactionKeyWitnessCount - -- * Script execution units + -- ** Script execution units , evaluateTransactionExecutionUnits , evaluateTransactionExecutionUnitsShelley , ScriptExecutionError (..) , TransactionValidityError (..) - -- * Transaction balance + -- ** Transaction balance , evaluateTransactionBalance - -- * Automated transaction building + -- ** Automated transaction building , estimateBalancedTxBody , estimateOrCalculateBalancedTxBody , makeTransactionBodyAutoBalance @@ -40,10 +325,10 @@ module Cardano.Api.Fees , TxBodyErrorAutoBalance (..) , TxFeeEstimationError (..) - -- * Minimum UTxO calculation + -- ** Minimum UTxO calculation , calculateMinimumUTxO - -- * Internal helpers + -- ** Internal helpers , ResolvablePointers (..) ) where diff --git a/cardano-api/internal/Cardano/Api/Tx/Body.hs b/cardano-api/internal/Cardano/Api/Tx/Body.hs index 753d4c1b5..338088e4a 100644 --- a/cardano-api/internal/Cardano/Api/Tx/Body.hs +++ b/cardano-api/internal/Cardano/Api/Tx/Body.hs @@ -16,20 +16,120 @@ {-# LANGUAGE TypeOperators #-} {-# LANGUAGE ViewPatterns #-} --- | Transaction bodies module Cardano.Api.Tx.Body - ( parseTxId - - -- * Transaction bodies + ( -- * Creating a 'TxBodyContent' + + -- | + -- + -- In order to create a transaction, we first need to define the contents of its body. In this section, + -- we will see how to create a 'TxBodyContent' for a simple transaction using the API. + -- + -- In this example, we are using the following qualified modules: + -- + -- @ + -- import qualified Cardano.Api as Api -- the general `cardano-api` exports + -- import qualified Cardano.Api.Script as Script -- types related to scripts (Plutus and native) + -- import qualified Cardano.Api.Experimental as Exp -- the experimental API + -- @ + -- + -- 'TxBodyContent' datatype provides lots of fields, because transactions can serve multiple + -- purposes, but the function 'defaultTxBodyContent' exported from 'Cardano.Api' already provides + -- a base 'TxBodyContent' with all fields set to their default values that we can use as a starting point + -- in order not to have to set all fields manually. + -- + -- The 'defaultTxBodyContent' takes, as the only parameter, the 'ShelleyBasedEra' witness for the era + -- we are working with. For example, if we are working with the 'ConwayEra', we can use 'shelleyBasedEra' + -- available in 'Cardano.Api', as follows: + -- + -- @ + -- let sbe :: Api.ShelleyBasedEra Api.ConwayEra = Api.shelleyBasedEra + -- @ + -- + -- We can also derive it from 'ConwayEra' from 'Cardano.Api.Experimental' by using the 'convert' function: + -- + -- @ + -- let era = Exp.ConwayEra + -- let sbe = Api.convert era + -- @ + -- + -- Let's see what creating a simple 'TxBodyContent' would look like. + -- + -- First, we choose a transaction output to spend (a UTxO). We specify which UTxO to spend by + -- providing the transaction id and the index of the output in that transaction that we want + -- to spend. + -- + -- To specify the transaction id, we can use the 'deserialiseFromRawBytesHex' function on the + -- hexadecimal representation of the hash of the transaction. For example: + -- + -- @ + -- let (Right srcTxId) = Api.deserialiseFromRawBytesHex Api.AsTxId "be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd978" + -- @ + -- + -- Of course, in real code, the failure case should be handled properly. + -- + -- To specify the transaction index, we can use the 'TxIx' constructor. For example: + -- + -- @ + -- let srcTxIx = Api.TxIx 0 + -- @ + -- + -- Now we combine both to create a 'TxIn' value and we pair it with a witness requirement using 'BuildTxWith' : + -- + -- @ + -- let txIn = ( Api.TxIn srcTxId srcTxIx + -- , Api.BuildTxWith (Api.KeyWitness Api.KeyWitnessForSpending) + -- ) + -- @ + -- + -- Next, let's specify the address of the recipient of the transaction. If we have the Bech32 representation, + -- we can use the 'deserialiseAddress' function to get the 'AddressInEra' type. For example: + -- + -- @ + -- let (Just destAddress) = Api.deserialiseAddress (Api.AsAddressInEra eraAsType) "addr_test1vzpfxhjyjdlgk5c0xt8xw26avqxs52rtf69993j4tajehpcue4v2v" + -- @ + -- + -- Now we can create a 'TxOut' value. For simplicity, we will assume the output is a simple payment output + -- of 10 ADA, with no datum or reference script attached to it: + -- + -- @ + -- let txOut = Api.TxOut + -- destAddress + -- (Api.lovelaceToTxOutValue sbe 10_000_000) + -- Api.TxOutDatumNone + -- Script.ReferenceScriptNone + -- @ + -- + -- We must also set the fee for the transaction. For example, let's set it to 2 ADA: + -- + -- @ + -- let txFee = Api.TxFeeExplicit sbe 2_000_000 + -- @ + -- + -- Finally, we can create the 'TxBodyContent' by using the 'defaultTxBodyContent' function and + -- putting everything together: + -- + -- @ + -- let txBodyContent = Api.defaultTxBodyContent sbe + -- & Api.setTxIns [txIn] + -- & Api.setTxOuts [txOut] + -- & Api.setTxFee txFee + -- @ + -- + -- The 'txBodyContent' can now be used to create a transaction either using the old or the new API. + + -- * Contents + parseTxId + + -- ** Transaction bodies , TxBody (.., TxBody) , createTransactionBody , createAndValidateTransactionBody , TxBodyContent (..) - -- * Byron only + -- ** Byron only , makeByronTransactionBody - -- ** Transaction body builders + -- *** Transaction body builders , defaultTxBodyContent , defaultTxFee , defaultTxValidityUpperBound @@ -68,20 +168,20 @@ module Cardano.Api.Tx.Body , txScriptValidityToIsValid , txScriptValidityToScriptValidity - -- * Transaction Ids + -- ** Transaction Ids , TxId (..) , getTxId , getTxIdByron , getTxIdShelley - -- * Transaction inputs + -- ** Transaction inputs , TxIn (..) , TxIns , TxIx (..) , genesisUTxOPseudoTxIn , getReferenceInputsSizeForTxIds - -- * Transaction outputs + -- ** Transaction outputs , CtxTx , CtxUTxO , TxOut (..) @@ -96,7 +196,7 @@ module Cardano.Api.Tx.Body , TxOutInAnyEra (..) , txOutInAnyEra - -- * Other transaction body types + -- ** Other transaction body types , TxInsCollateral (..) , TxInsReference (..) , TxReturnCollateral (..) @@ -119,23 +219,23 @@ module Cardano.Api.Tx.Body , mkTxProposalProcedures , convProposalProcedures - -- ** Building vs viewing transactions + -- *** Building vs viewing transactions , BuildTxWith (..) , BuildTx , ViewTx , buildTxWithToMaybe - -- * Inspecting 'ScriptWitness'es + -- ** Inspecting 'ScriptWitness'es , AnyScriptWitness (..) , ScriptWitnessIndex (..) , renderScriptWitnessIndex , collectTxBodyScriptWitnesses , toScriptIndex - -- * Conversion to inline data + -- ** Conversion to inline data , scriptDataToInlineDatum - -- * Internal conversion functions & types + -- ** Internal conversion functions & types , convCertificates , convCollateralTxIns , convExtraKeyWitnesses @@ -170,12 +270,12 @@ module Cardano.Api.Tx.Body , fromLedgerTxOuts , renderTxIn - -- * Misc helpers + -- ** Misc helpers , calculateExecutionUnitsLovelace , orderStakeAddrs , orderTxIns - -- * Data family instances + -- ** Data family instances , AsType (AsTxId, AsTxBody, AsByronTxBody, AsShelleyTxBody, AsMaryTxBody) , getTxBodyContent -- Temp diff --git a/cardano-api/src/Cardano/Api/Experimental.hs b/cardano-api/src/Cardano/Api/Experimental.hs index 79984396e..c1abc1acd 100644 --- a/cardano-api/src/Cardano/Api/Experimental.hs +++ b/cardano-api/src/Cardano/Api/Experimental.hs @@ -1,7 +1,16 @@ --- | This module provides an experimental library interface that is intended --- to replace the existing api. It is subject to dramatic changes so use with caution. +-- | +-- This module provides an experimental library interface that is intended +-- to replace the existing API. It is subject to dramatic changes, so use with caution. module Cardano.Api.Experimental - ( -- * Tx related + ( -- * Creating transactions + + -- | + -- For information and an example on how to create a transaction using the new experimental API, + -- check the documentation of the module "Cardano.Api.Experimental.Tx". + + -- * Contents + + -- ** Tx related UnsignedTx (..) , UnsignedTxError (..) , makeUnsignedTx @@ -12,7 +21,8 @@ module Cardano.Api.Experimental , obtainCommonConstraints , hashTxBody , evaluateTransactionExecutionUnitsShelley - -- Era related + + -- ** Era related , BabbageEra , ConwayEra , Era (..) diff --git a/cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs b/cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs new file mode 100644 index 000000000..3abadbd85 --- /dev/null +++ b/cardano-api/test/cardano-api-test/Test/Cardano/Api/Experimental.hs @@ -0,0 +1,285 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeFamilies #-} + +module Test.Cardano.Api.Experimental + ( tests + ) +where + +import qualified Cardano.Api as Api +import Cardano.Api.Eon.ShelleyBasedEra (ShelleyBasedEraConstraints) +import qualified Cardano.Api.Experimental as Exp +import qualified Cardano.Api.Genesis as Genesis +import qualified Cardano.Api.Ledger as Ledger +import qualified Cardano.Api.ProtocolParameters as Api +import qualified Cardano.Api.Script as Script +import Cardano.Api.Tx.Sign (Tx (ShelleyTx)) + +import qualified Cardano.Ledger.Alonzo.Scripts as UnexportedLedger +import qualified Cardano.Ledger.Api as UnexportedLedger +import qualified Cardano.Slotting.EpochInfo as Slotting +import qualified Cardano.Slotting.Slot as Slotting +import qualified Cardano.Slotting.Time as Slotting + +import Control.Monad.Identity (Identity) +import Data.Map (fromList) +import Data.Maybe (fromMaybe) +import Data.Ratio ((%)) +import qualified Data.Time as Time +import qualified Data.Time.Clock.POSIX as Time +import Lens.Micro ((&)) + +import Hedgehog (Property) +import qualified Hedgehog as H +import qualified Hedgehog.Extras as H +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.Hedgehog (testProperty) + +-- | Tests in this module can be run by themselves by writing: +-- ```bash +-- cabal test cardano-api-test --test-options="--pattern=Test.Cardano.Api.Experimental" +-- ``` +-- +-- IMPORTANT NOTE: If this file requires changes, please update the examples in the +-- documentation in 'cardano-api/src/Cardano/Api/Experimental.hs' too. +tests :: TestTree +tests = + testGroup + "Test.Cardano.Api.Experimental" + [ testProperty + "Created transaction with traditional and experimental APIs are equivalent" + prop_created_transaction_with_both_apis_are_the_same + , testProperty + "Check two methods of balancing transaction are equivalent" + prop_balance_transaction_two_ways + ] + +prop_created_transaction_with_both_apis_are_the_same :: Property +prop_created_transaction_with_both_apis_are_the_same = H.propertyOnce $ do + let era = Exp.ConwayEra + let sbe = Api.convert era + + signedTxTraditional <- exampleTransacitonTraditionalWay sbe + signedTxExperimental <- exampleTransactionExperimentalWay era sbe + + let oldStyleTx :: Api.Tx Api.ConwayEra = ShelleyTx sbe signedTxExperimental + + oldStyleTx H.=== signedTxTraditional + where + exampleTransacitonTraditionalWay + :: H.MonadTest m => Api.ShelleyBasedEra Exp.ConwayEra -> m (Tx Exp.ConwayEra) + exampleTransacitonTraditionalWay sbe = do + txBodyContent <- exampleTxBodyContent Api.AsConwayEra sbe + signingKey <- exampleSigningKey + + txBody <- H.evalEither $ Api.createTransactionBody sbe txBodyContent + + let signedTx :: Api.Tx Api.ConwayEra = Api.signShelleyTransaction sbe txBody [Api.WitnessPaymentKey signingKey] + + return signedTx + + exampleTransactionExperimentalWay + :: H.MonadTest m + => Exp.Era Exp.ConwayEra + -> Api.ShelleyBasedEra Exp.ConwayEra + -> m (Ledger.Tx (Exp.LedgerEra Exp.ConwayEra)) + exampleTransactionExperimentalWay era sbe = do + txBodyContent <- exampleTxBodyContent Api.AsConwayEra sbe + signingKey <- exampleSigningKey + + unsignedTx <- H.evalEither $ Exp.makeUnsignedTx era txBodyContent + let witness = Exp.makeKeyWitness era unsignedTx (Api.WitnessPaymentKey signingKey) + + let bootstrapWitnesses = [] + keyWitnesses = [witness] + + let signedTx :: Ledger.Tx (Exp.LedgerEra Exp.ConwayEra) = Exp.signTx era bootstrapWitnesses keyWitnesses unsignedTx + return signedTx + +prop_balance_transaction_two_ways :: Property +prop_balance_transaction_two_ways = H.propertyOnce $ do + let era = Exp.ConwayEra + let sbe = Api.convert era + let meo = Api.MaryEraOnwardsConway + + changeAddress <- getExampleChangeAddress sbe + + txBodyContent <- exampleTxBodyContent Api.AsConwayEra sbe + txBody <- H.evalEither $ Api.createTransactionBody sbe txBodyContent + + -- Simple way (fee calculation) + let fees = Api.evaluateTransactionFee sbe exampleProtocolParams txBody 0 1 0 + H.note_ $ "Fees 1: " <> show fees + + -- Balance without ledger context (other that protocol parameters) + Api.BalancedTxBody + _txBodyContent2 + _txBody2 + _changeOutput2 + fees2 <- + H.evalEither + $ Api.estimateBalancedTxBody + meo + txBodyContent + exampleProtocolParams + mempty + mempty + mempty + mempty + 0 + 1 + 0 + 0 + changeAddress + $ Api.lovelaceToValue 12_000_000 + + H.note_ $ "Fees 2: " <> show fees2 + -- H.note_ $ "New TxBody 2: " <> show txBody2 + -- H.note_ $ "New TxBodyContent 2: " <> show txBodyContent2 + -- H.note_ $ "Change output 2: " <> show changeOutput2 + + -- Automatically balance the transaction (with ledger context) + currTime <- Api.liftIO Time.getCurrentTime + srcTxId <- getExampleSrcTxId + let startTime = Time.posixSecondsToUTCTime (Time.utcTimeToPOSIXSeconds currTime - Time.nominalDay) + let epochInfo = + Api.LedgerEpochInfo $ Slotting.fixedEpochInfo (Slotting.EpochSize 100) (Slotting.mkSlotLength 1000) + let utxoToUse = + Api.UTxO + ( fromList + [ + ( srcTxId + , Api.TxOut + changeAddress + (Api.lovelaceToTxOutValue sbe 12_000_000) + Api.TxOutDatumNone + Script.ReferenceScriptNone + ) + ] + ) + Api.BalancedTxBody + _txBodyContent3 + _txBody3 + _changeOutput3 + fees3 <- + H.evalEither $ + Api.makeTransactionBodyAutoBalance + sbe + (Api.SystemStart startTime) + epochInfo + (Api.LedgerProtocolParameters exampleProtocolParams) + mempty + mempty + mempty + utxoToUse + txBodyContent + changeAddress + Nothing + + H.note_ $ "Fees 3: " <> show fees3 + -- H.note_ $ "TxBody 3: " <> show txBody3 + -- H.note_ $ "TxBodyContent 3: " <> show txBodyContent3 + -- H.note_ $ "Change output 3: " <> show changeOutput3 + + H.success + +exampleProtocolParams :: Ledger.PParams (UnexportedLedger.ConwayEra Ledger.StandardCrypto) +exampleProtocolParams = + UnexportedLedger.upgradePParams conwayUpgrade $ + UnexportedLedger.upgradePParams () $ + UnexportedLedger.upgradePParams alonzoUpgrade $ + UnexportedLedger.upgradePParams () $ + UnexportedLedger.upgradePParams () $ + Genesis.sgProtocolParams Genesis.shelleyGenesisDefaults + where + conwayUpgrade :: Ledger.UpgradeConwayPParams Identity + conwayUpgrade = Ledger.cgUpgradePParams Genesis.conwayGenesisDefaults + + alonzoUpgrade :: UnexportedLedger.UpgradeAlonzoPParams Identity + alonzoUpgrade = + UnexportedLedger.UpgradeAlonzoPParams + { UnexportedLedger.uappCoinsPerUTxOWord = Ledger.CoinPerWord $ Ledger.Coin 34_482 + , UnexportedLedger.uappCostModels = UnexportedLedger.emptyCostModels -- We are not using scripts for this tests, so this is fine for now + , UnexportedLedger.uappPrices = + Ledger.Prices + { Ledger.prSteps = fromMaybe maxBound $ Ledger.boundRational $ 721 % 10_000_000 + , Ledger.prMem = fromMaybe maxBound $ Ledger.boundRational $ 577 % 10_000 + } + , UnexportedLedger.uappMaxTxExUnits = + Ledger.ExUnits + { Ledger.exUnitsMem = 140_000_000 + , Ledger.exUnitsSteps = 10_000_000_000 + } + , UnexportedLedger.uappMaxBlockExUnits = + Ledger.ExUnits + { Ledger.exUnitsMem = 62_000_000 + , Ledger.exUnitsSteps = 20_000_000_000 + } + , UnexportedLedger.uappMaxValSize = 5000 + , UnexportedLedger.uappCollateralPercentage = 150 + , UnexportedLedger.uappMaxCollateralInputs = 3 + } + +getExampleSrcTxId :: H.MonadTest m => m Api.TxIn +getExampleSrcTxId = do + srcTxId <- + H.evalEither $ + Api.deserialiseFromRawBytesHex + Api.AsTxId + "be6efd42a3d7b9a00d09d77a5d41e55ceaf0bd093a8aa8a893ce70d9caafd978" + let srcTxIx = Api.TxIx 0 + return $ Api.TxIn srcTxId srcTxIx + +getExampleDestAddress + :: (H.MonadTest m, Api.IsCardanoEra era) => Script.AsType era -> m (Api.AddressInEra era) +getExampleDestAddress eraAsType = do + H.evalMaybe $ + Api.deserialiseAddress + (Api.AsAddressInEra eraAsType) + "addr_test1vzpfxhjyjdlgk5c0xt8xw26avqxs52rtf69993j4tajehpcue4v2v" + +getExampleChangeAddress :: H.MonadTest m => Api.ShelleyBasedEra era -> m (Api.AddressInEra era) +getExampleChangeAddress sbe = do + signingKey <- exampleSigningKey + return $ + Api.shelleyAddressInEra sbe $ + Api.makeShelleyAddress + (Api.Testnet $ Api.NetworkMagic 2) + (Api.PaymentCredentialByKey $ Api.verificationKeyHash $ Api.getVerificationKey signingKey) + Api.NoStakeAddress + +exampleTxBodyContent + :: (ShelleyBasedEraConstraints era, H.MonadTest m) + => Api.AsType era + -> Api.ShelleyBasedEra era + -> m (Api.TxBodyContent Api.BuildTx era) +exampleTxBodyContent eraAsType sbe = do + srcTxIn <- getExampleSrcTxId + destAddress <- getExampleDestAddress eraAsType + let txBodyContent = + Api.defaultTxBodyContent sbe + & Api.setTxIns + [ + ( srcTxIn + , Api.BuildTxWith (Api.KeyWitness Api.KeyWitnessForSpending) + ) + ] + & Api.setTxOuts + [ Api.TxOut + destAddress + (Api.lovelaceToTxOutValue sbe 10_000_000) + Api.TxOutDatumNone + Script.ReferenceScriptNone + ] + & Api.setTxFee (Api.TxFeeExplicit sbe 2_000_000) + + return txBodyContent + +exampleSigningKey :: H.MonadTest m => m (Api.SigningKey Api.PaymentKey) +exampleSigningKey = + H.evalEither $ + Api.deserialiseFromBech32 + (Api.AsSigningKey Api.AsPaymentKey) + "addr_sk1648253w4tf6fv5fk28dc7crsjsaw7d9ymhztd4favg3cwkhz7x8sl5u3ms" diff --git a/cardano-api/test/cardano-api-test/cardano-api-test.hs b/cardano-api/test/cardano-api-test/cardano-api-test.hs index 3b977fb1c..431a1fa35 100644 --- a/cardano-api/test/cardano-api-test/cardano-api-test.hs +++ b/cardano-api/test/cardano-api-test/cardano-api-test.hs @@ -13,6 +13,7 @@ import qualified Test.Cardano.Api.Crypto import qualified Test.Cardano.Api.Envelope import qualified Test.Cardano.Api.EpochLeadership import qualified Test.Cardano.Api.Eras +import qualified Test.Cardano.Api.Experimental import qualified Test.Cardano.Api.Genesis import qualified Test.Cardano.Api.IO import qualified Test.Cardano.Api.Json @@ -48,6 +49,7 @@ tests = , Test.Cardano.Api.Envelope.tests , Test.Cardano.Api.EpochLeadership.tests , Test.Cardano.Api.Eras.tests + , Test.Cardano.Api.Experimental.tests , Test.Cardano.Api.Genesis.tests , Test.Cardano.Api.IO.tests , Test.Cardano.Api.Json.tests