Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit 81f0ea0

Browse files
Merge #4058
4058: Support UtxoNotEnoughFragmented error r=KtorZ a=paweljakubas Add multioutput integration tests Fix Lens accesses and confusion between WalAddress vs WalletAddress vs V1 Address ## Description This is backport of cardano-foundation/cardano-wallet#190 ## Linked issue <!--- Put here the relevant issue from YouTrack --> Co-authored-by: Pawel Jakubas <[email protected]>
2 parents 184d52c + d58e4bc commit 81f0ea0

File tree

7 files changed

+249
-6
lines changed

7 files changed

+249
-6
lines changed

CHANGELOG.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# CHANGELOG
22

3-
## Cardano SL 3.0.0
3+
## Cardano SL 3.0.0
44

55
### Fixes
66

@@ -29,7 +29,7 @@
2929

3030
- Additional node settings exposed through the wallet backend API in `/api/v1/node-settings`. This is in order to align and be on-par with the new node monitoring API. [#4045](https://github.com/input-output-hk/cardano-sl/pull/4045)
3131
Added settings:
32-
- `slotId`: The current slot and epoch
32+
- `slotId`: The current slot and epoch
3333
- `slotCount`: The number of slots per epoch
3434
- `maxTxSize`: The largest allowed transaction size in bytes
3535
- `feePolicy`: The fee policy, in flat Lovelace and variable Lovelace/byte
@@ -269,6 +269,9 @@
269269
- Add a test which checks if the configuration can be correctly parsed
270270
- [CDEC-405](https://iohk.myjetbrains.com/youtrack/issue/CDEC-405) [#3175](https://github.com/input-output-hk/cardano-sl/pull/3175)
271271

272+
- Add Utxo not enough fragmentation error handling and multi-output transaction tests
273+
- [cardano-wallet#190](https://github.com/input-output-hk/cardano-wallet/issues/190) [#4058](https://github.com/input-output-hk/cardano-sl/pull/4058)
274+
272275
### Documentation
273276

274277
- Make an inventory of existing wallet errors and exceptions ([CBR-307](https://iohk.myjetbrains.com/youtrack/issue/CO-307))

wallet/src/Cardano/Wallet/API/V1/ReifyWalletError.hs

+3
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,9 @@ newTransactionError e = case e of
306306
Kernel.NewTransactionInvalidTxIn ->
307307
V1.SignedTxSubmitError "NewTransactionInvalidTxIn"
308308

309+
(Kernel.NewTransactionNotEnoughUtxoFragmentation (Kernel.NumberOfMissingUtxos missingUtxo)) ->
310+
V1.UtxoNotEnoughFragmented (V1.ErrUtxoNotEnoughFragmented missingUtxo V1.msgUtxoNotEnoughFragmented)
311+
309312
redeemAdaError :: RedeemAdaError -> V1.WalletError
310313
redeemAdaError e = case e of
311314
(RedeemAdaError e') -> case e' of

wallet/src/Cardano/Wallet/API/V1/Swagger.hs

+35-1
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ $errors
335335

336336
-- 'UnsupportedMimeTypeError'
337337
, mkRow fmtErr $ UnsupportedMimeTypePresent "Expected Content-Type's main MIME-type to be 'application/json'."
338-
338+
, mkRow fmtErr $ UtxoNotEnoughFragmented (ErrUtxoNotEnoughFragmented 1 msgUtxoNotEnoughFragmented)
339339
-- TODO 'MnemonicError' ?
340340
]
341341
mkRow fmt err = T.intercalate "|" (fmt err)
@@ -684,6 +684,40 @@ curl -X POST https://localhost:8090/api/v1/transactions \
684684
```
685685

686686

687+
About UTXO Fragmentation
688+
------------------------
689+
690+
As described in [Sending Money to Multiple Recipients](#section/Common-Use-Cases/Sending-Money-to-Multiple-Recipients), it is possible to send ada to more than one destination. Cardano only allows a given UTXO to cover at most one single transaction output. As a result,
691+
when the number of transaction outputs is greater than the number the API returns a `UtxoNotEnoughFragmented` error which
692+
looks like the following
693+
```
694+
{
695+
"status": "error",
696+
"diagnostic": {
697+
"details": {
698+
"help": "Utxo is not enough fragmented to handle the number of outputs of this transaction. Query /api/v1/wallets/{walletId}/statistics/utxos endpoint for more information",
699+
"missingUtxos": 1
700+
}
701+
},
702+
"message": "UtxoNotEnoughFragmented"
703+
}
704+
```
705+
706+
To make sure the source account has a sufficient level of UTXO fragmentation (i.e. number of UTXOs),
707+
please monitor the state of the UTXOs as described in [Getting UTXO Statistics](#section/Common-Use-Cases/Getting-Utxo-Statistics). The
708+
number of wallet UTXOs should be no less than the transaction outputs, and the sum of all UTXOs should be enough to cover the total
709+
transaction amount, including fees.
710+
711+
Contrary to a classic accounting model, there's no such thing as spending _part of a UTXO_, and one has to wait for a transaction to be included in a
712+
block before spending the remaining change. This is very similar to using bank notes: one can't spend a USD 20 bill at two different shops at the same time,
713+
even if it is enough to cover both purchases — one has to wait for change from the first transaction before making the second one.
714+
There's no "ideal" level of fragmentation; it depends on one's needs. However, the more UTXOs that are available, the higher the concurrency capacity
715+
of one's wallet, allowing multiple transactions to be made at the same time.
716+
717+
Similarly, there's no practical maximum number of UTXOs, but there is nevertheless a maximum transaction size. By having many small UTXOs,
718+
one is taking the risk of hitting that restriction, should too many inputs be selected to fill a transaction. The only way to
719+
work around this is to make multiple smaller transactions.
720+
687721
Estimating Transaction Fees
688722
---------------------------
689723

wallet/src/Cardano/Wallet/API/V1/Types.hs

+29-1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ module Cardano.Wallet.API.V1.Types (
106106
-- * Wallet Errors
107107
, WalletError(..)
108108
, ErrNotEnoughMoney(..)
109+
, ErrUtxoNotEnoughFragmented(..)
110+
, msgUtxoNotEnoughFragmented
109111
, toServantError
110112
, toHttpErrorStatus
111113
, module Cardano.Wallet.Types.UtxoStatistics
@@ -1883,6 +1885,21 @@ instance FromJSON ErrNotEnoughMoney where
18831885
when (msg /= sformat build (ErrAvailableBalanceIsInsufficient 0)) mempty
18841886
ErrAvailableBalanceIsInsufficient <$> (o .: "availableBalance")
18851887

1888+
data ErrUtxoNotEnoughFragmented = ErrUtxoNotEnoughFragmented {
1889+
theMissingUtxos :: !Int
1890+
, theHelp :: !Text
1891+
} deriving (Eq, Generic, Show)
1892+
1893+
1894+
msgUtxoNotEnoughFragmented :: Text
1895+
msgUtxoNotEnoughFragmented = "Utxo is not enough fragmented to handle the number of outputs of this transaction. Query /api/v1/wallets/{walletId}/statistics/utxos endpoint for more information"
1896+
1897+
deriveJSON Aeson.defaultOptions ''ErrUtxoNotEnoughFragmented
1898+
1899+
instance Buildable ErrUtxoNotEnoughFragmented where
1900+
build (ErrUtxoNotEnoughFragmented missingUtxos _ ) =
1901+
bprint ("Missing "%build%" utxo(s) to accommodate all outputs of the transaction") missingUtxos
1902+
18861903

18871904
-- | Type representing any error which might be thrown by wallet.
18881905
--
@@ -1948,6 +1965,9 @@ data WalletError =
19481965
| RequestThrottled !Word64
19491966
-- ^ The request has been throttled. The 'Word64' is a count of microseconds
19501967
-- until the user should retry.
1968+
| UtxoNotEnoughFragmented !ErrUtxoNotEnoughFragmented
1969+
-- ^ available Utxo is not enough fragmented, ie., there is more outputs of transaction than
1970+
-- utxos
19511971
deriving (Generic, Show, Eq)
19521972

19531973
deriveGeneric ''WalletError
@@ -1990,6 +2010,9 @@ instance Arbitrary WalletError where
19902010
, NodeIsStillSyncing <$> arbitrary
19912011
, CannotCreateAddress <$> arbitraryText
19922012
, RequestThrottled <$> arbitrary
2013+
, UtxoNotEnoughFragmented <$> Gen.oneof
2014+
[ ErrUtxoNotEnoughFragmented <$> Gen.choose (1, 10) <*> arbitrary
2015+
]
19932016
]
19942017
where
19952018
arbitraryText :: Gen Text
@@ -2046,7 +2069,8 @@ instance Buildable WalletError where
20462069
bprint "Cannot create derivation path for new address, for external wallet."
20472070
RequestThrottled _ ->
20482071
bprint "You've made too many requests too soon, and this one was throttled."
2049-
2072+
UtxoNotEnoughFragmented x ->
2073+
bprint build x
20502074

20512075
-- | Convert wallet errors to Servant errors
20522076
instance ToServantError WalletError where
@@ -2089,6 +2113,8 @@ instance ToServantError WalletError where
20892113
err500
20902114
RequestThrottled{} ->
20912115
err400 { errHTTPCode = 429 }
2116+
UtxoNotEnoughFragmented{} ->
2117+
err403
20922118

20932119
-- | Declare the key used to wrap the diagnostic payload, if any
20942120
instance HasDiagnostic WalletError where
@@ -2131,3 +2157,5 @@ instance HasDiagnostic WalletError where
21312157
"msg"
21322158
RequestThrottled{} ->
21332159
"microsecondsUntilRetry"
2160+
UtxoNotEnoughFragmented{} ->
2161+
"details"

wallet/src/Cardano/Wallet/Kernel/Transactions.hs

+32
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Cardano.Wallet.Kernel.Transactions (
1010
, PaymentError(..)
1111
, EstimateFeesError(..)
1212
, RedeemAdaError(..)
13+
, NumberOfMissingUtxos(..)
1314
, cardanoFee
1415
, mkStdTx
1516
, prepareUnsignedTxWithSources
@@ -85,13 +86,24 @@ import UTxO.Util (shuffleNE)
8586
Generating payments and estimating fees
8687
-------------------------------------------------------------------------------}
8788

89+
data NumberOfMissingUtxos = NumberOfMissingUtxos Int
90+
91+
instance Buildable NumberOfMissingUtxos where
92+
build (NumberOfMissingUtxos number) =
93+
bprint ("NumberOfMissingUtxos " % build) number
94+
95+
instance Arbitrary NumberOfMissingUtxos where
96+
arbitrary = oneof [ NumberOfMissingUtxos <$> arbitrary
97+
]
98+
8899
data NewTransactionError =
89100
NewTransactionUnknownAccount UnknownHdAccount
90101
| NewTransactionUnknownAddress UnknownHdAddress
91102
| NewTransactionErrorCoinSelectionFailed CoinSelHardErr
92103
| NewTransactionErrorCreateAddressFailed Kernel.CreateAddressError
93104
| NewTransactionErrorSignTxFailed SignTransactionError
94105
| NewTransactionInvalidTxIn
106+
| NewTransactionNotEnoughUtxoFragmentation NumberOfMissingUtxos
95107

96108
instance Buildable NewTransactionError where
97109
build (NewTransactionUnknownAccount err) =
@@ -106,6 +118,9 @@ instance Buildable NewTransactionError where
106118
bprint ("NewTransactionErrorSignTxFailed " % build) err
107119
build NewTransactionInvalidTxIn =
108120
bprint "NewTransactionInvalidTxIn"
121+
build (NewTransactionNotEnoughUtxoFragmentation err) =
122+
bprint ("NewTransactionNotEnoughUtxoFragmentation" % build) err
123+
109124

110125
instance Arbitrary NewTransactionError where
111126
arbitrary = oneof [
@@ -117,6 +132,7 @@ instance Arbitrary NewTransactionError where
117132
, NewTransactionErrorCreateAddressFailed <$> arbitrary
118133
, NewTransactionErrorSignTxFailed <$> arbitrary
119134
, pure NewTransactionInvalidTxIn
135+
, NewTransactionNotEnoughUtxoFragmentation <$> arbitrary
120136
]
121137

122138
data PaymentError = PaymentNewTransactionError NewTransactionError
@@ -214,6 +230,9 @@ newUnsignedTransaction ActiveWallet{..} options accountId payees = runExceptT $
214230
availableUtxo <- withExceptT NewTransactionUnknownAccount $ exceptT $
215231
currentAvailableUtxo snapshot accountId
216232

233+
withExceptT NewTransactionNotEnoughUtxoFragmentation $ exceptT $
234+
checkUtxoFragmentation payees availableUtxo
235+
217236
-- STEP 1: Run coin selection.
218237
CoinSelFinalResult inputs outputs coins <-
219238
withExceptT NewTransactionErrorCoinSelectionFailed $ ExceptT $
@@ -247,6 +266,19 @@ newUnsignedTransaction ActiveWallet{..} options accountId payees = runExceptT $
247266
toTxOut :: (Address, Coin) -> TxOutAux
248267
toTxOut (a, c) = TxOutAux (TxOut a c)
249268

269+
checkUtxoFragmentation
270+
:: NonEmpty (Address, Coin)
271+
-> Utxo
272+
-> Either NumberOfMissingUtxos ()
273+
checkUtxoFragmentation outputs inputs =
274+
let numberOfUtxo = Map.size inputs
275+
numberOfOutputs = NonEmpty.length outputs
276+
diff = numberOfOutputs - numberOfUtxo
277+
in if diff > 0 then
278+
Left $ NumberOfMissingUtxos diff
279+
else
280+
Right ()
281+
250282
-- | Creates a new unsigned transaction.
251283
--
252284
-- NOTE: this function does /not/ perform a payment, it just creates a new

wallet/test/integration/Test/Integration/Framework/DSL.hs

+24
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module Test.Integration.Framework.DSL
3737
, defaultAccountId
3838
, defaultAssuranceLevel
3939
, defaultDistribution
40+
, customDistribution
4041
, defaultGroupingPolicy
4142
, defaultPage
4243
, defaultPerPage
@@ -72,6 +73,8 @@ module Test.Integration.Framework.DSL
7273
, ($-)
7374
, (</>)
7475
, (!!)
76+
, addresses
77+
, walAddresses
7578
, amount
7679
, assuranceLevel
7780
, backupPhrase
@@ -106,6 +109,7 @@ import Data.Generics.Internal.VL.Lens (lens)
106109
import Data.Generics.Product.Fields (field)
107110
import Data.Generics.Product.Typed (HasType, typed)
108111
import Data.List ((!!))
112+
import qualified Data.List.NonEmpty as NonEmpty
109113
import qualified Data.Map.Strict as Map
110114
import qualified Data.Text as T
111115
import qualified Data.Text.Encoding as T
@@ -268,6 +272,21 @@ defaultDistribution
268272
defaultDistribution c s = pure $
269273
PaymentDistribution (V1 $ head $ s ^. typed) (V1 $ mkCoin c)
270274

275+
customDistribution
276+
:: NonEmpty (Account, Word64)
277+
-> NonEmpty PaymentDistribution
278+
customDistribution payees =
279+
let recepientWalAddresses =
280+
NonEmpty.fromList
281+
$ map (view walAddresses)
282+
$ concatMap (view addresses . fst)
283+
$ payees
284+
285+
in NonEmpty.zipWith
286+
PaymentDistribution
287+
recepientWalAddresses
288+
(map ((\coin -> V1 $ mkCoin coin) . snd) payees)
289+
271290
defaultGroupingPolicy :: Maybe (V1 InputSelectionPolicy)
272291
defaultGroupingPolicy = Nothing
273292

@@ -386,6 +405,11 @@ walletName = typed
386405
spendingPasswordLastUpdate :: Lens' Wallet (V1 Timestamp)
387406
spendingPasswordLastUpdate = field @"walSpendingPasswordLastUpdate"
388407

408+
addresses :: HasType [WalletAddress] s => Lens' s [WalletAddress]
409+
addresses = typed
410+
411+
walAddresses :: HasType (V1 Address) s => Lens' s (V1 Address)
412+
walAddresses = typed
389413

390414
--
391415
-- EXPECTATIONS

0 commit comments

Comments
 (0)