Skip to content

Commit aad4b64

Browse files
authored
Merge pull request #1513 from Plutonomicon/klntsky/use-additional-addresses-as-collateral
Add `mustUseCollateralUtxos` balancer constraint
2 parents 199f6de + 3149cb8 commit aad4b64

File tree

21 files changed

+471
-225
lines changed

21 files changed

+471
-225
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
8383
- Support for generic CIP-30 wallets by name ([#1524](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1524))
8484
- Full additional utxos support for Blockfrost backend ([#1537](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1537))
8585
- New `submitTxE`, an error returning variant of `submitTx`
86+
- Allow providing a custom set of UTxOs for collateral selection, overriding the wallet (`mustUseCollateralUtxos` balancer constraint) ([#1513](https://github.com/Plutonomicon/cardano-transaction-lib/pull/1513))
8687

8788
### Changed
8889

doc/balancing.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ Transaction balancing in Cardano is the process of finding a set of inputs and o
1818

1919
CTL allows tweaking the default balancer behavior by letting the user impose constraints on the UTxO set that is used in the process (`balanceTxWithConstraints`):
2020

21-
- providing additional UTxOs to use: `mustUseUtxosAtAddresses` / `mustUseUtxosAtAddress` / `mustUseAdditionalUtxos`
22-
- overriding change address: `mustSendChangeToAddress`
23-
- prevent certain UTxOs from being spent: `mustNotSpendUtxosWithOutRefs` / `mustNotSpendUtxoWithOutRef`
24-
- distribute token outputs equally between change UTxOs: `mustGenChangeOutsWithMaxTokenQuantity`
21+
- Using arbitrary address as user's own (for transaction balancing): `mustUseUtxosAtAddresses` / `mustUseUtxosAtAddress`
22+
- Providing additional UTxOs to use: `mustUseAdditionalUtxos`
23+
- Bypassing wallet's collateral selection and selecting collateral UTxOs from a given set: `mustUseCollateralUtxos`
24+
- Overriding change address: `mustSendChangeToAddress`
25+
- Preventing certain UTxOs from being spent: `mustNotSpendUtxosWithOutRefs` / `mustNotSpendUtxoWithOutRef`
26+
- Distributing token outputs equally between change UTxOs: `mustGenChangeOutsWithMaxTokenQuantity`
2527

2628
## Concurrent spending
2729

@@ -31,11 +33,11 @@ Obviously, the number of available UTxOs must be greater than the number of tran
3133

3234
## Balancing a Tx for other wallet
3335

34-
Setting `mustUseUtxosAtAddress` and `mustSendChangeToAddress` at the same time allows to build a transaction without any connection to the current wallet. For example, it's possible to balance it on server-side and send to the user to sign, or balance a Tx on one user's side while leaving fees at the expense of some other user.
36+
Setting `mustUseUtxosAtAddress`, `mustSendChangeToAddress` and `mustUseCollateralUtxos` at the same time allows to build a transaction without any connection to the current wallet. For example, it's possible to balance it on server-side and send to the user to sign, or balance a Tx on one user's side while leaving fees at the expense of some other user.
3537

3638
## Synchronization
3739

38-
Before balancing, CTL synchronizes the wallet with the query layer, i.e. waits until all UTxOs that the wallet returns are visible in the query layer. Thus the situation when the query layer refuses to validate a Tx (either during ex-units evaluation or on Tx submission) is only possible due to a rollback. Please see [our docs for query layer synchronization](./query-layers.md).
40+
Before balancing, CTL tries to synchronize the wallet state with the query layer, i.e. waits until all UTxOs that the wallet returns are visible in the query layer. Thus the situation when the query layer refuses to validate a Tx (either during ex-units evaluation or on Tx submission) is only possible due to a rollback or a synchronization timeout. Please see [our docs for query layer synchronization](./query-layers.md).
3941

4042
## Balancing process limitations
4143

examples/BalanceTxConstraints.purs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ module Ctl.Examples.BalanceTxConstraints
55

66
import Contract.Prelude
77

8-
import Contract.Address
9-
( Address
10-
)
8+
import Contract.Address (Address)
119
import Contract.BalanceTxConstraints
1210
( BalanceTxConstraintsBuilder
1311
, mustGenChangeOutsWithMaxTokenQuantity
1412
, mustNotSpendUtxoWithOutRef
1513
, mustSendChangeToAddress
14+
, mustUseCollateralUtxos
1615
, mustUseUtxosAtAddress
1716
) as BalanceTxConstraints
1817
import Contract.Log (logInfo')
@@ -43,6 +42,7 @@ import Contract.Value (singleton, valueOf) as Value
4342
import Contract.Wallet
4443
( KeyWallet
4544
, getWalletAddressesWithNetworkTag
45+
, getWalletCollateral
4646
, ownPaymentPubKeyHashes
4747
, withKeyWallet
4848
)
@@ -52,7 +52,7 @@ import Ctl.Examples.Helpers (mkCurrencySymbol, mkTokenName) as Helpers
5252
import Data.Array (head)
5353
import Data.Array (sort) as Array
5454
import Data.BigInt (BigInt, fromInt)
55-
import Data.Map (keys, member) as Map
55+
import Data.Map (fromFoldable, keys, member) as Map
5656
import Data.Set (findMin) as Set
5757

5858
newtype ContractParams = ContractParams
@@ -63,6 +63,7 @@ newtype ContractParams = ContractParams
6363
type ContractResult =
6464
{ txHash :: TransactionHash
6565
, changeAddress :: Address
66+
, nonSpendableAddress :: Address
6667
, mintedToken :: CurrencySymbol /\ TokenName
6768
, nonSpendableOref :: TransactionInput
6869
}
@@ -100,8 +101,8 @@ assertSelectedUtxoIsNotSpent
100101
:: ContractCheck ContractResult
101102
assertSelectedUtxoIsNotSpent =
102103
assertionToCheck "Non-spendable UTxO hasn't been spent"
103-
\{ changeAddress, nonSpendableOref } -> do
104-
utxos <- lift $ utxosAt changeAddress
104+
\{ nonSpendableAddress, nonSpendableOref } -> do
105+
utxos <- lift $ utxosAt nonSpendableAddress
105106
let
106107
assertionFailure :: ContractAssertionFailure
107108
assertionFailure =
@@ -120,6 +121,11 @@ contract :: ContractParams -> Contract Unit
120121
contract (ContractParams p) = do
121122
logInfo' "Examples.BalanceTxConstraints"
122123

124+
aliceAddress <-
125+
liftedM "Failed to get Alice's address"
126+
$ head
127+
<$> (withKeyWallet p.aliceKeyWallet getWalletAddressesWithNetworkTag)
128+
123129
alicePubKeyHash <-
124130
liftedM "Failed to get own PKH" $ head <$> ownPaymentPubKeyHashes
125131

@@ -133,9 +139,16 @@ contract (ContractParams p) = do
133139
$ head
134140
<$> (withKeyWallet p.bobKeyWallet getWalletAddressesWithNetworkTag)
135141

142+
bobsCollateralArray <- withKeyWallet p.bobKeyWallet do
143+
fold <$> getWalletCollateral
144+
let
145+
bobsCollateral =
146+
Map.fromFoldable $ bobsCollateralArray <#> unwrap >>>
147+
\{ input, output } -> Tuple input output
148+
136149
nonSpendableOref <-
137-
liftedM "Failed to get utxos at Bob's address"
138-
(Set.findMin <<< Map.keys <$> utxosAt bobAddress)
150+
liftedM "Failed to get utxos at Alice's address"
151+
(Set.findMin <<< Map.keys <$> utxosAt aliceAddress)
139152

140153
mp /\ cs <- Helpers.mkCurrencySymbol alwaysMintsPolicy
141154
tn <- Helpers.mkTokenName "The Token"
@@ -154,6 +167,7 @@ contract (ContractParams p) = do
154167
<> BalanceTxConstraints.mustUseUtxosAtAddress bobAddress
155168
<> BalanceTxConstraints.mustSendChangeToAddress bobAddress
156169
<> BalanceTxConstraints.mustNotSpendUtxoWithOutRef nonSpendableOref
170+
<> BalanceTxConstraints.mustUseCollateralUtxos bobsCollateral
157171

158172
void $ runChecks checks $ lift do
159173
unbalancedTx <- mkUnbalancedTx lookups constraints
@@ -171,4 +185,10 @@ contract (ContractParams p) = do
171185
logInfo' "Tx submitted successfully!"
172186

173187
let changeAddress = (unwrap bobAddress).address
174-
pure { txHash, changeAddress, mintedToken: cs /\ tn, nonSpendableOref }
188+
pure
189+
{ txHash
190+
, changeAddress
191+
, nonSpendableAddress: (unwrap aliceAddress).address
192+
, mintedToken: cs /\ tn
193+
, nonSpendableOref
194+
}

spago.dhall

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ You can edit this file as you like.
99
, "aff-promise"
1010
, "aff-retry"
1111
, "affjax"
12+
, "ansi"
1213
, "argonaut"
1314
, "argonaut-codecs"
1415
, "arraybuffer-types"

src/Contract/BalanceTxConstraints.purs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Ctl.Internal.BalanceTx.Constraints
1111
, mustSendChangeWithDatum
1212
, mustUseAdditionalUtxos
1313
, mustUseCoinSelectionStrategy
14+
, mustUseCollateralUtxos
1415
, mustUseUtxosAtAddress
1516
, mustUseUtxosAtAddresses
1617
) as BalanceTxConstraints

src/Contract/Transaction.purs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ import Contract.UnbalancedTx (mkUnbalancedTx)
4949
import Control.Monad.Error.Class (catchError, liftEither, throwError)
5050
import Control.Monad.Reader (ReaderT, asks, runReaderT)
5151
import Control.Monad.Reader.Class (ask)
52-
import Ctl.Internal.BalanceTx (FinalizedTransaction)
53-
import Ctl.Internal.BalanceTx (FinalizedTransaction(FinalizedTransaction)) as FinalizedTransaction
5452
import Ctl.Internal.BalanceTx (balanceTxWithConstraints) as BalanceTx
5553
import Ctl.Internal.BalanceTx.Constraints (BalanceTxConstraintsBuilder)
5654
import Ctl.Internal.BalanceTx.Error
@@ -60,6 +58,7 @@ import Ctl.Internal.BalanceTx.Error
6058
, CouldNotConvertScriptOutputToTxInput
6159
, CouldNotGetChangeAddress
6260
, CouldNotGetCollateral
61+
, InsufficientCollateralUtxos
6362
, CouldNotGetUtxos
6463
, CollateralReturnError
6564
, CollateralReturnMinAdaValueCalcError
@@ -72,6 +71,8 @@ import Ctl.Internal.BalanceTx.Error
7271
, Expected(Expected)
7372
, explainBalanceTxError
7473
) as BalanceTxError
74+
import Ctl.Internal.BalanceTx.Types (FinalizedTransaction)
75+
import Ctl.Internal.BalanceTx.Types (FinalizedTransaction(FinalizedTransaction)) as FinalizedTransaction
7576
import Ctl.Internal.BalanceTx.UnattachedTx (UnindexedTx)
7677
import Ctl.Internal.Cardano.Types.NativeScript
7778
( NativeScript

src/Internal/BalanceTx/BalanceTx.purs

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
module Ctl.Internal.BalanceTx
2-
( module BalanceTxErrorExport
3-
, module FinalizedTransaction
4-
, balanceTxWithConstraints
2+
( balanceTxWithConstraints
53
) where
64

75
import Prelude
86

7+
import Contract.Log (logWarn')
98
import Control.Monad.Error.Class (liftMaybe)
109
import Control.Monad.Except.Trans (ExceptT(ExceptT), except, runExceptT)
1110
import Control.Monad.Logger.Class (info) as Logger
@@ -22,8 +21,10 @@ import Ctl.Internal.BalanceTx.Collateral
2221
( addTxCollateral
2322
, addTxCollateralReturn
2423
)
24+
import Ctl.Internal.BalanceTx.Collateral.Select (selectCollateral)
2525
import Ctl.Internal.BalanceTx.Constraints
2626
( BalanceTxConstraintsBuilder
27+
, _collateralUtxos
2728
, _nonSpendableInputs
2829
)
2930
import Ctl.Internal.BalanceTx.Constraints
@@ -34,23 +35,10 @@ import Ctl.Internal.BalanceTx.Constraints
3435
, _selectionStrategy
3536
, _srcAddresses
3637
) as Constraints
37-
import Ctl.Internal.BalanceTx.Error
38-
( Actual(Actual)
39-
, BalanceTxError
40-
( CouldNotGetChangeAddress
41-
, CouldNotGetCollateral
42-
, CouldNotGetUtxos
43-
, ExUnitsEvaluationFailed
44-
, ReindexRedeemersError
45-
, UtxoLookupFailedFor
46-
, UtxoMinAdaValueCalculationFailed
47-
)
48-
, Expected(Expected)
49-
, printTxEvaluationFailure
50-
) as BalanceTxErrorExport
5138
import Ctl.Internal.BalanceTx.Error
5239
( BalanceTxError
53-
( UtxoLookupFailedFor
40+
( InsufficientCollateralUtxos
41+
, UtxoLookupFailedFor
5442
, UtxoMinAdaValueCalculationFailed
5543
, ReindexRedeemersError
5644
, CouldNotGetUtxos
@@ -79,7 +67,6 @@ import Ctl.Internal.BalanceTx.Types
7967
, liftEitherContract
8068
, withBalanceTxConstraints
8169
)
82-
import Ctl.Internal.BalanceTx.Types (FinalizedTransaction(FinalizedTransaction)) as FinalizedTransaction
8370
import Ctl.Internal.BalanceTx.UnattachedTx
8471
( EvaluatedTx
8572
, UnindexedTx
@@ -106,6 +93,9 @@ import Ctl.Internal.Cardano.Types.Transaction
10693
, _witnessSet
10794
, pprintUtxoMap
10895
)
96+
import Ctl.Internal.Cardano.Types.TransactionUnspentOutput
97+
( transactionUnspentOutputsToUtxoMap
98+
)
10999
import Ctl.Internal.Cardano.Types.Value
110100
( AssetClass
111101
, Coin(Coin)
@@ -129,10 +119,14 @@ import Ctl.Internal.Contract.Wallet
129119
, getWalletCollateral
130120
, getWalletUtxos
131121
) as Wallet
132-
import Ctl.Internal.Helpers (liftEither, (??))
122+
import Ctl.Internal.Helpers (liftEither, pprintTagSet, (??))
133123
import Ctl.Internal.Partition (equipartition, partition)
124+
import Ctl.Internal.Plutus.Conversion (fromPlutusUtxoMap)
134125
import Ctl.Internal.Serialization.Address (Address)
135126
import Ctl.Internal.Types.OutputDatum (OutputDatum(NoOutputDatum, OutputDatum))
127+
import Ctl.Internal.Types.ProtocolParameters
128+
( ProtocolParameters(ProtocolParameters)
129+
)
136130
import Ctl.Internal.Types.Scripts
137131
( Language(PlutusV1)
138132
, PlutusScript(PlutusScript)
@@ -162,14 +156,15 @@ import Data.Lens.Setter ((%~), (.~), (?~))
162156
import Data.Log.Tag (TagSet, tag, tagSetTag)
163157
import Data.Log.Tag (fromArray) as TagSet
164158
import Data.Map (Map)
165-
import Data.Map (empty, insert, lookup, toUnfoldable, union) as Map
159+
import Data.Map (empty, filterKeys, insert, lookup, toUnfoldable, union) as Map
166160
import Data.Maybe (Maybe(Just, Nothing), fromMaybe, isJust, maybe)
167161
import Data.Newtype (class Newtype, unwrap, wrap)
168162
import Data.Set (Set)
169163
import Data.Set as Set
170164
import Data.Traversable (for, traverse)
171165
import Data.Tuple (fst)
172166
import Data.Tuple.Nested (type (/\), (/\))
167+
import Data.UInt (toInt) as UInt
173168
import Effect.Aff.Class (liftAff)
174169
import Effect.Class (liftEffect)
175170

@@ -275,14 +270,37 @@ balanceTxWithConstraints transaction extraUtxos constraintsBuilder = do
275270
setTransactionCollateral :: Address -> Transaction -> BalanceTxM Transaction
276271
setTransactionCollateral changeAddr transaction = do
277272
nonSpendableSet <- asksConstraints _nonSpendableInputs
278-
collateral <- do
279-
rawCollateral <- liftEitherContract $ note CouldNotGetCollateral <$>
280-
Wallet.getWalletCollateral
281-
-- filter out UTxOs that are set as non-spendable in the balancer constraints
282-
let
283-
isSpendable = not <<< flip Set.member nonSpendableSet <<< _.input <<<
284-
unwrap
285-
pure $ Array.filter isSpendable rawCollateral
273+
mbCollateralUtxos <- asksConstraints _collateralUtxos
274+
-- We must filter out UTxOs that are set as non-spendable in the balancer
275+
-- constraints
276+
let isSpendable = not <<< flip Set.member nonSpendableSet
277+
collateral <- case mbCollateralUtxos of
278+
-- if no collateral utxos are specified, use the wallet, but filter
279+
-- the unspendable ones
280+
Nothing -> do
281+
let isSpendableUtxo = isSpendable <<< _.input <<< unwrap
282+
{ yes: spendableUtxos, no: filteredUtxos } <-
283+
Array.partition isSpendableUtxo <$> do
284+
liftEitherContract $ note CouldNotGetCollateral <$>
285+
Wallet.getWalletCollateral
286+
when (not $ Array.null filteredUtxos) do
287+
logWarn' $ pprintTagSet
288+
"Some of the collateral UTxOs returned by the wallet were marked as non-spendable and ignored"
289+
(pprintUtxoMap (transactionUnspentOutputsToUtxoMap filteredUtxos))
290+
pure spendableUtxos
291+
-- otherwise, get all the utxos, filter out unspendable, and select
292+
-- collateral using internal algo, that is also used in KeyWallet
293+
Just utxoMap -> do
294+
ProtocolParameters params <- liftContract getProtocolParameters
295+
networkId <- askNetworkId
296+
let
297+
coinsPerUtxoUnit = params.coinsPerUtxoUnit
298+
maxCollateralInputs = UInt.toInt $ params.maxCollateralInputs
299+
utxoMap' = fromPlutusUtxoMap networkId $ Map.filterKeys isSpendable
300+
utxoMap
301+
mbCollateral <- liftEffect $ map Array.fromFoldable <$>
302+
selectCollateral coinsPerUtxoUnit maxCollateralInputs utxoMap'
303+
liftEither $ note (InsufficientCollateralUtxos utxoMap') mbCollateral
286304
addTxCollateralReturn collateral (addTxCollateral collateral transaction)
287305
changeAddr
288306

0 commit comments

Comments
 (0)