Skip to content

Commit

Permalink
Merge pull request #1544 from Plutonomicon/dshuiski/purs15/blockfrost…
Browse files Browse the repository at this point in the history
…-additional-utxos

(Purs15) Provide additional utxos to Blockfrost evaluateTx, Handle AdditionalUtxoOverlap error
  • Loading branch information
errfrom authored Oct 3, 2023
2 parents 2d1ee4e + e9c0f04 commit 5a560eb
Show file tree
Hide file tree
Showing 22 changed files with 545 additions and 117 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [HD wallet support](./doc/key-management.md) with mnemonic seed phrases ([#1498](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1498))
- Ogmios-specific functions for Local TX Monitor Ouroboros Mini-Protocol in `Contract.Backend.Ogmios` ([#1508](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1508/))
- New `mustSendChangeWithDatum` balancer constraint that adds datum to all change outputs ([#1510](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1510/))
- Full additional utxos support for Blockfrost backend ([#1537](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1537))

### Changed

Expand All @@ -91,6 +92,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Do not require light wallet collateral for all interactions ([#1477](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1477))
- Removed re-exports of wallet-related functions from `Contract.Utxos` and `Contract.Address` (use `Contract.Wallet`) ([#1477](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1477))
- `ownPaymentPubKeysHashes` renamed to `ownPaymentPubKeyHashes`, `ownStakePubKeysHashes` renamed to `ownStakePubKeyHashes` and both moved to `Contract.Wallet` ([#1477](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1477))
- UTxO lists and combined input/output/mint/fee values are now being pretty-printed instead of logged using `Show` instance (in the balancer) ([#1531](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1531))

### Fixed

Expand All @@ -107,6 +109,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Add a single-slot wait at Plutip startup before attempting to query any wallet UTxOs ([#1470](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1470))
- Index `Reward` redeemers properly ([#1419](https://github.com/Plutonomicon/cardano-transaction-lib/issues/1419), [#1462](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1462))
- A problem with collateral selection not respecting `mustNotSpendUtxosWithOutRefs` ([#1509](https://github.com/Plutonomicon/cardano-transaction-lib/issues/1509))
- A problem with too many change UTxOs being generated ([#1530](https://github.com/Plutonomicon/cardano-transaction-lib/issues/1530))
- A problem where tx evaluation with additional utxos failed with an Ogmios `AdditionalUtxoOverlap` exception if some additional utxos got confirmed in the meantime ([#1537](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1537))

### Removed

Expand Down
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Support is planned for the following light wallets:
- [x] [Lode](https://lodewallet.io/)
- [x] [Eternl (formerly CCvault)](https://eternl.io/)
- [x] [NuFi](https://nu.fi/)
- [ ] [Lace](https://www.lace.io/)
- [x] [Lace](https://www.lace.io/)
- [ ] [Typhon](https://typhonwallet.io/)
- [ ] [Yoroi](https://yoroi-wallet.com/)

Expand All @@ -79,8 +79,8 @@ Support is planned for the following light wallets:
- [x] **Stage 3** Once we have a simple working transaction, we will seek to build a Plutus smart contract transaction with datum from scratch
- [x] **Stage 4** Once we can construct Plutus smart contract transactions, we will seek to build a library/DSL/interface such that transactions can be built using constraints and lookups - as close as possible to a cut-and-paste solution from Plutus' `Contract` monad code in Haskell (but with no guarantee that code changes are not necessary)
- [x] **Stage 4.1** Investigate supporting compatibility with the Vasil hardfork and improvements to our initial `Contract` API
- [ ] **Stage 5** Once we have a basic `Contract`-style API, we will further refine its public interface, expand wallet support (see [below](#light-wallet-support)), expose a test interface (**DONE** - see [here](doc/plutip-testing.md)), provide a more ergonomic JS/TS API, support stake validators (**DONE**), and support CIP workflows on the public testnet (**In progress**)
- [ ] **Stage 6** Once CTL's `Contract` interface has been stabilized, we will add support for even more wallets and attempt to deprecate CTL's currently required Haskell server (**DONE**)
- [x] **Stage 5** Once we have a basic `Contract`-style API, we will further refine its public interface, expand wallet support (see [below](#light-wallet-support)), expose a test interface (**DONE** - see [here](doc/plutip-testing.md)), provide a more ergonomic JS/TS API, support stake validators (**DONE**), and support CIP workflows on the public testnet (**In progress**)
- [x] **Stage 6** Once CTL's `Contract` interface has been stabilized, we will add support for even more wallets and attempt to deprecate CTL's currently required Haskell server (**DONE**)

## Architecture

Expand All @@ -90,12 +90,10 @@ CTL is directly inspired by the Plutus Application Backend (PAB). Unlike PAB, ho
- This is handled by `cardano-serialization-lib`, a Rust library available as WASM
2. How do we query the chain?
- This has been solved using Ogmios & Kupo
- We [will support](https://cardano.ideascale.com/c/idea/420791) an alternative [BlockFrost](https://blockfrost.io/) backend as well in the future
3. How do we query for datums (i.e. the datums themselves and not just their hashes)?
- `Kupo` solves this problem
4. How do we get wallet data?
- Thanks to [Catalyst](https://cardano.ideascale.com/c/idea/420791), we now support an alternative [BlockFrost](https://blockfrost.io/) backend as well
3. How do we get wallet data?
- This is done via browser-based light wallet integration in the browser based on CIP-30
5. How closely should we follow Plutus' `Contract` API?
4. How closely should we follow Plutus' `Contract` API?
- CTL's `Contract` model is **significantly** less restrictive than Plutus' and allows for arbitrary effects within the `Contract` monad
- Certain features cannot be directly translated into Purescript from Haskell due to differences between the two languages
- Some of the Plutus conventions do not make sense for us, due to differences between on-chain and off-chain
Expand Down
133 changes: 133 additions & 0 deletions examples/AdditionalUtxos.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
module Ctl.Examples.AdditionalUtxos
( contract
, main
) where

import Contract.Prelude

import Contract.Address (scriptHashAddress)
import Contract.BalanceTxConstraints (BalanceTxConstraintsBuilder)
import Contract.BalanceTxConstraints (mustUseAdditionalUtxos) as BalancerConstraints
import Contract.Config (ContractParams, testnetNamiConfig)
import Contract.Log (logInfo')
import Contract.Monad (Contract, launchAff_, liftedE, runContract)
import Contract.PlutusData (Datum, PlutusData(Integer), unitRedeemer)
import Contract.ScriptLookups (ScriptLookups, UnbalancedTx, mkUnbalancedTx)
import Contract.ScriptLookups (datum, unspentOutputs, validator) as Lookups
import Contract.Scripts (Validator, ValidatorHash, validatorHash)
import Contract.Sync (withoutSync)
import Contract.Transaction
( ScriptRef(NativeScriptRef)
, TransactionInput
, awaitTxConfirmed
, balanceTxWithConstraints
, createAdditionalUtxos
, signTransaction
, submit
, withBalancedTx
)
import Contract.TxConstraints
( DatumPresence(DatumInline, DatumWitness)
, TxConstraints
)
import Contract.TxConstraints
( mustPayToScript
, mustPayToScriptWithScriptRef
, mustSpendPubKeyOutput
, mustSpendScriptOutput
) as Constraints
import Contract.Utxos (UtxoMap)
import Contract.Value (Value)
import Contract.Value (lovelaceValueOf) as Value
import Ctl.Examples.PlutusV2.Scripts.AlwaysSucceeds (alwaysSucceedsScriptV2)
import Data.Array (fromFoldable) as Array
import Data.BigInt (fromInt) as BigInt
import Data.Map (difference, filter, keys) as Map
import Test.QuickCheck (arbitrary)
import Test.QuickCheck.Gen (randomSampleOne)

main :: Effect Unit
main = example testnetNamiConfig

example :: ContractParams -> Effect Unit
example contractParams =
launchAff_ $ runContract contractParams $ contract false

contract :: Boolean -> Contract Unit
contract testAdditionalUtxoOverlap = withoutSync do
logInfo' "Running Examples.AdditionalUtxos"
validator <- alwaysSucceedsScriptV2
let vhash = validatorHash validator
{ unbalancedTx, datum } <- payToValidator vhash
withBalancedTx unbalancedTx \balancedTx -> do
balancedSignedTx <- signTransaction balancedTx
txHash <- submit balancedSignedTx
when testAdditionalUtxoOverlap $ awaitTxConfirmed txHash
logInfo' "Successfully locked two outputs at the validator address."

additionalUtxos <- createAdditionalUtxos balancedSignedTx
spendFromValidator validator additionalUtxos datum

payToValidator
:: ValidatorHash -> Contract { unbalancedTx :: UnbalancedTx, datum :: Datum }
payToValidator vhash = do
scriptRef <- liftEffect (NativeScriptRef <$> randomSampleOne arbitrary)
let
value :: Value
value = Value.lovelaceValueOf $ BigInt.fromInt 2_000_000

datum :: Datum
datum = wrap $ Integer $ BigInt.fromInt 42

constraints :: TxConstraints Unit Unit
constraints =
Constraints.mustPayToScript vhash datum DatumWitness value
<> Constraints.mustPayToScriptWithScriptRef vhash datum DatumInline
scriptRef
value

lookups :: ScriptLookups PlutusData
lookups = Lookups.datum datum

unbalancedTx <- liftedE $ mkUnbalancedTx lookups constraints
pure { unbalancedTx, datum }

spendFromValidator :: Validator -> UtxoMap -> Datum -> Contract Unit
spendFromValidator validator additionalUtxos datum = do
let
scriptUtxos :: UtxoMap
scriptUtxos =
additionalUtxos # Map.filter \out ->
(unwrap (unwrap out).output).address
== scriptHashAddress (validatorHash validator) Nothing

scriptOrefs :: Array TransactionInput
scriptOrefs = Array.fromFoldable $ Map.keys scriptUtxos

pubKeyOrefs :: Array TransactionInput
pubKeyOrefs =
Array.fromFoldable $ Map.keys $ Map.difference additionalUtxos scriptUtxos

constraints :: TxConstraints Unit Unit
constraints =
foldMap (flip Constraints.mustSpendScriptOutput unitRedeemer) scriptOrefs
<> foldMap Constraints.mustSpendPubKeyOutput pubKeyOrefs

lookups :: ScriptLookups PlutusData
lookups =
Lookups.validator validator
<> Lookups.unspentOutputs additionalUtxos
<> Lookups.datum datum

balancerConstraints :: BalanceTxConstraintsBuilder
balancerConstraints =
BalancerConstraints.mustUseAdditionalUtxos additionalUtxos

unbalancedTx <- liftedE $ mkUnbalancedTx lookups constraints
balancedTx <- liftedE $ balanceTxWithConstraints unbalancedTx
balancerConstraints
balancedSignedTx <- signTransaction balancedTx
txHash <- submit balancedSignedTx

awaitTxConfirmed txHash
logInfo' "Successfully spent additional utxos from the validator address."
11 changes: 10 additions & 1 deletion examples/ByUrl.purs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ import Contract.Config
)
import Contract.Monad (Contract)
import Contract.Test.E2E (E2EConfigName, E2ETestName, addLinks, route)
import Ctl.Examples.AdditionalUtxos as AdditionalUtxos
import Ctl.Examples.AlwaysMints as AlwaysMints
import Ctl.Examples.AlwaysSucceeds as AlwaysSucceeds
import Ctl.Examples.ChangeGeneration as ChangeGeneration
import Ctl.Examples.Cip30 as Cip30
import Ctl.Examples.Datums as Datums
import Ctl.Examples.DropTokens as DropTokens
Expand Down Expand Up @@ -191,7 +193,8 @@ mkBlockfrostPreprodConfig apiKey =

examples :: Map E2ETestName (Contract Unit)
examples = Map.fromFoldable
[ "AlwaysMints" /\ AlwaysMints.contract
[ "AdditionalUtxos" /\ AdditionalUtxos.contract false
, "AlwaysMints" /\ AlwaysMints.contract
, "NativeScriptMints" /\ NativeScriptMints.contract
, "AlwaysSucceeds" /\ AlwaysSucceeds.contract
, "AlwaysSucceedsV2" /\ AlwaysSucceedsV2.contract
Expand All @@ -214,4 +217,10 @@ examples = Map.fromFoldable
, "ECDSA" /\ ECDSA.contract
, "PaysWithDatum" /\ PaysWithDatum.contract
, "DropTokens" /\ DropTokens.contract
, "ChangeGeneration1-1" /\
ChangeGeneration.checkChangeOutputsDistribution 1 1 3
, "ChangeGeneration3-1" /\
ChangeGeneration.checkChangeOutputsDistribution 3 1 5
, "ChangeGeneration1-3" /\
ChangeGeneration.checkChangeOutputsDistribution 1 3 7
]
77 changes: 77 additions & 0 deletions examples/ChangeGeneration.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module Ctl.Examples.ChangeGeneration (checkChangeOutputsDistribution) where

import Prelude

import Contract.BalanceTxConstraints (mustSendChangeWithDatum)
import Contract.Monad (Contract, liftedE)
import Contract.PlutusData
( Datum(Datum)
, OutputDatum(OutputDatum)
, PlutusData(Integer)
, unitDatum
)
import Contract.ScriptLookups as Lookups
import Contract.Scripts (validatorHash)
import Contract.Transaction
( _body
, _outputs
, awaitTxConfirmed
, balanceTxWithConstraints
, signTransaction
, submit
)
import Contract.TxConstraints (TxConstraints)
import Contract.TxConstraints as Constraints
import Contract.UnbalancedTx (mkUnbalancedTx)
import Contract.Value as Value
import Contract.Wallet (ownPaymentPubKeyHashes, ownStakePubKeyHashes)
import Ctl.Examples.AlwaysSucceeds as AlwaysSucceeds
import Data.Array (fold, length, replicate, take, zip)
import Data.BigInt (fromInt) as BigInt
import Data.Lens (to, (^.))
import Data.Maybe (Maybe(Just, Nothing))
import Data.Newtype (unwrap)
import Data.Tuple (Tuple(Tuple))
import Test.Spec.Assertions (shouldEqual)

-- | A contract that creates `outputsToScript` number of outputs at a script address,
-- | `outputsToSelf` outputs going to own address, and asserts that the number of change
-- | outputs is equal to `expectedOutputs`.
checkChangeOutputsDistribution :: Int -> Int -> Int -> Contract Unit
checkChangeOutputsDistribution outputsToScript outputsToSelf expectedOutputs =
do
pkhs <- ownPaymentPubKeyHashes
skhs <- ownStakePubKeyHashes
validator <- AlwaysSucceeds.alwaysSucceedsScript
let
vhash = validatorHash validator
value = Value.lovelaceValueOf $ BigInt.fromInt 1000001

constraintsToSelf :: TxConstraints Unit Unit
constraintsToSelf = fold <<< take outputsToSelf <<< fold
$ replicate outputsToSelf
$ zip pkhs skhs <#> \(Tuple pkh mbSkh) -> case mbSkh of
Nothing -> Constraints.mustPayToPubKey pkh value
Just skh -> Constraints.mustPayToPubKeyAddress pkh skh value

constraintsToScripts :: TxConstraints Unit Unit
constraintsToScripts = fold $ replicate outputsToScript
$ Constraints.mustPayToScript vhash unitDatum
Constraints.DatumWitness
value

constraints = constraintsToSelf <> constraintsToScripts

lookups :: Lookups.ScriptLookups PlutusData
lookups = mempty
unbalancedTx <- liftedE $ mkUnbalancedTx lookups constraints
balancedTx <- liftedE $ balanceTxWithConstraints unbalancedTx
-- just to check that attaching datums works
( mustSendChangeWithDatum $ OutputDatum $ Datum $ Integer $ BigInt.fromInt
1000
)
balancedSignedTx <- signTransaction balancedTx
let outputs = balancedTx ^. to unwrap <<< _body <<< _outputs
length outputs `shouldEqual` expectedOutputs
txHash <- submit balancedSignedTx
awaitTxConfirmed txHash
2 changes: 1 addition & 1 deletion packages.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ let additions =
, "node-fs-aff"
]
, repo = "https://github.com/mlabs-haskell/purescript-toppokki"
, version = "b043e9342463df76972d05981ac4ec25316834bf"
, version = "f90f92f0ddf0eecc73705c1675db37918d18cbcb"
}
, noble-secp256k1 =
{ dependencies =
Expand Down
8 changes: 4 additions & 4 deletions spago-packages.nix

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5a560eb

Please sign in to comment.