@@ -6,7 +6,6 @@ module Cardano.Wallet.Kernel.Transactions (
6
6
, NewTransactionError (.. )
7
7
, PaymentError (.. )
8
8
, EstimateFeesError (.. )
9
- , mkSigner
10
9
, cardanoFee
11
10
-- * Internal & testing use only
12
11
, newTransaction
@@ -19,16 +18,20 @@ import Control.Retry (RetryPolicyM, RetryStatus, applyPolicy,
19
18
fullJitterBackoff , limitRetries , retrying )
20
19
import Crypto.Random (MonadRandom (.. ))
21
20
import qualified Data.Set as Set
21
+ import qualified Data.Vector as V
22
+ import System.Random.MWC (GenIO , asGenIO , initialize , uniformVector )
22
23
import Test.QuickCheck (Arbitrary (.. ))
23
24
24
25
import Formatting (bprint , build , sformat , (%) )
25
26
import qualified Formatting.Buildable
26
27
27
28
import qualified Data.ByteArray as ByteArray
28
- import qualified Pos.Binary as Bi
29
+ import qualified Data.ByteString as B
29
30
31
+ import qualified Cardano.Wallet.Kernel.Addresses as Kernel
30
32
import Cardano.Wallet.Kernel.CoinSelection.FromGeneric (Cardano ,
31
- CoinSelectionOptions , estimateCardanoFee , mkStdTx )
33
+ CoinSelFinalResult (.. ), CoinSelectionOptions ,
34
+ estimateCardanoFee , mkStdTx )
32
35
import qualified Cardano.Wallet.Kernel.CoinSelection.FromGeneric as CoinSelection
33
36
import Cardano.Wallet.Kernel.CoinSelection.Generic
34
37
(CoinSelHardErr (.. ))
@@ -37,12 +40,15 @@ import Cardano.Wallet.Kernel.DB.HdWallet
37
40
import qualified Cardano.Wallet.Kernel.DB.HdWallet as HD
38
41
import Cardano.Wallet.Kernel.DB.HdWallet.Read
39
42
(readHdAddressByCardanoAddress )
43
+ import Cardano.Wallet.Kernel.Types (AccountId (.. ), WalletId (.. ))
40
44
import Cardano.Wallet.Kernel.Util (paymentAmount , utxoBalance ,
41
45
utxoRestrictToInputs )
42
46
43
47
import Cardano.Wallet.Kernel (getWalletSnapshot , newPending )
44
48
import Cardano.Wallet.Kernel.DB.Read as Getters
45
- import Cardano.Wallet.Kernel.Internal (ActiveWallet (.. ))
49
+ import Cardano.Wallet.Kernel.Internal (ActiveWallet (.. ),
50
+ walletKeystore )
51
+ import qualified Cardano.Wallet.Kernel.Keystore as Keystore
46
52
47
53
import Pos.Core (Address , Coin , Tx (.. ), TxAux (.. ), TxOut (.. ),
48
54
TxOutAux (.. ), unsafeSubCoin )
@@ -85,19 +91,16 @@ instance Buildable PaymentError where
85
91
-- seconds, as well as internally retrying up to 5 times to propagate the
86
92
-- transaction via 'newPending'.
87
93
pay :: ActiveWallet
88
- -> IO Address
89
- -- ^ A computation to generate change 'Address'es.
90
- -> (Address -> Either CoinSelHardErr SafeSigner )
94
+ -> PassPhrase
91
95
-> CoinSelectionOptions
92
96
-> HdAccountId
93
- -- ^ The source @(root, account) from where the payment was
94
- -- originated
97
+ -- ^ The source HD Account from where the payment was originated
95
98
-> NonEmpty (Address , Coin )
96
99
-- ^ The payees
97
100
-> IO (Either PaymentError Tx )
98
- pay activeWallet genChangeAddr signAddress opts accountId payees = do
101
+ pay activeWallet spendingPassword opts accountId payees = do
99
102
retrying retryPolicy shouldRetry $ \ rs -> do
100
- tx <- newTransaction activeWallet genChangeAddr signAddress opts accountId payees
103
+ (tx, _) <- newTransaction activeWallet spendingPassword opts accountId payees
101
104
case tx of
102
105
Left e -> return (Left $ PaymentNewTransactionError e)
103
106
Right txAux -> do
@@ -131,58 +134,98 @@ pay activeWallet genChangeAddr signAddress opts accountId payees = do
131
134
132
135
-- | Creates a new 'TxAux' without submitting it to the network.
133
136
newTransaction :: ActiveWallet
134
- -> IO Address
135
- -- ^ A computation to generate change 'Address'es.
136
- -> (Address -> Either CoinSelHardErr SafeSigner )
137
- -- ^ A function to sign each Address
137
+ -> PassPhrase
138
+ -- ^ The spending password.
138
139
-> CoinSelectionOptions
139
140
-- ^ The options describing how to tune the coin selection.
140
141
-> HdAccountId
141
- -- ^ The source @(root, account) from where the payment was
142
- -- originated
142
+ -- ^ The source HD account from where the payment should originate
143
143
-> NonEmpty (Address , Coin )
144
144
-- ^ The payees
145
- -> IO (Either NewTransactionError TxAux )
146
- newTransaction ActiveWallet {.. } genChangeAddr signAddress options accountId payees = do
147
- snapshot <- getWalletSnapshot walletPassive
148
- let toTxOuts = fmap (\ (a,c) -> TxOutAux (TxOut a c))
149
- let genChangeOuts css = forM css $ \ change -> do
150
- changeAddr <- liftIO genChangeAddr
151
- return TxOutAux {
152
- toaOut = TxOut {
153
- txOutAddress = changeAddr
154
- , txOutValue = change
155
- }
156
- }
157
- let mkTx = mkStdTx walletProtocolMagic genChangeOuts signAddress
145
+ -> IO (Either NewTransactionError TxAux , Utxo )
146
+ newTransaction ActiveWallet {.. } spendingPassword options accountId payees = do
147
+
158
148
-- | NOTE(adn) This number was computed out of the work Matt Noonan did
159
149
-- on the size estimation. See [CBR-318].
160
150
let maxInputs = 350
151
+
152
+ snapshot <- getWalletSnapshot walletPassive
161
153
let availableUtxo = accountAvailableUtxo snapshot accountId
162
- freshCounter <- newIORef 1
163
- let initialSeed = encodeUtf8 @ Text @ ByteString . sformat build $ hash payees
164
- res <- flip runReaderT (initialSeed, freshCounter) . buildPayment $
154
+
155
+ initialEnv <- newEnvironment
156
+
157
+ -- STEP 1: Run coin selection.
158
+ res <- flip runReaderT initialEnv . buildPayment $
165
159
CoinSelection. random options
166
- mkTx
167
160
maxInputs
168
- (toTxOuts payees)
161
+ (fmap toTxOut payees)
169
162
availableUtxo
170
163
case res of
171
- Left err -> return . Left . CoinSelectionFailed $ err
172
- Right t -> return . Right $ t
164
+ Left err -> return (Left . CoinSelectionFailed $ err, availableUtxo)
165
+ Right (CoinSelFinalResult inputs outputs coins) -> do
166
+ -- STEP 2: Generate the change addresses needed.
167
+ changeAddresses <- genChangeOuts coins
168
+
169
+ -- STEP 3: Perform the signing and forge the final TxAux.
170
+ let keystore = walletPassive ^. walletKeystore
171
+ mbEsk <- Keystore. lookup (WalletIdHdRnd $ accountId ^. hdAccountIdParent) keystore
172
+ let allWallets = hdWallets snapshot
173
+ signAddress = mkSigner spendingPassword mbEsk allWallets
174
+ mkTx = mkStdTx walletProtocolMagic signAddress
175
+
176
+ (, availableUtxo) . bimap CoinSelectionFailed identity
177
+ <$> mkTx inputs outputs changeAddresses
178
+
179
+ where
180
+ -- Generate an initial seed for the random generator using the hash of
181
+ -- the payees, which ensure that the coin selection (and the fee estimation)
182
+ -- is \"pseudo deterministic\" and replicable.
183
+ newEnvironment :: IO Env
184
+ newEnvironment =
185
+ let initialSeed = V. fromList . map fromIntegral
186
+ . B. unpack
187
+ . encodeUtf8 @ Text @ ByteString
188
+ . sformat build
189
+ $ hash payees
190
+ in Env <$> initialize initialSeed
191
+
192
+ toTxOut :: (Address , Coin ) -> TxOutAux
193
+ toTxOut (a, c) = TxOutAux (TxOut a c)
194
+
195
+ -- | Generates the list of change outputs from a list of change coins.
196
+ genChangeOuts :: MonadIO m => [Coin ] -> m [TxOutAux ]
197
+ genChangeOuts css = forM css $ \ change -> do
198
+ changeAddr <- liftIO genChangeAddr
199
+ return TxOutAux {
200
+ toaOut = TxOut {
201
+ txOutAddress = changeAddr
202
+ , txOutValue = change
203
+ }
204
+ }
205
+
206
+ -- | Monadic computation to generate a new change 'Address'. This will
207
+ -- run after coin selection, when we create the final transaction as
208
+ -- part of 'mkTx'.
209
+ genChangeAddr :: IO Address
210
+ genChangeAddr = do
211
+ res <- Kernel. createAddress spendingPassword
212
+ (AccountIdHdRnd accountId)
213
+ walletPassive
214
+ case res of
215
+ Right addr -> pure addr
216
+ Left err -> throwM err
173
217
174
218
-- | Special monad used to process the payments, which randomness is derived
175
219
-- from a fixed seed obtained from hashing the payees. This guarantees that
176
220
-- when we estimate the fees and later create a transaction, the coin selection
177
221
-- will always yield the same value, making the process externally-predicatable.
178
222
newtype PayMonad a = PayMonad {
179
- buildPayment :: ReaderT (ByteString , IORef Word16 ) IO a
180
- } deriving ( Functor
181
- , Applicative
182
- , Monad
183
- , MonadIO
184
- , MonadReader (ByteString , IORef Word16 )
185
- )
223
+ buildPayment :: ReaderT Env IO a
224
+ } deriving ( Functor , Applicative , Monad , MonadIO , MonadReader Env )
225
+
226
+ -- | This 'Env' datatype is necessary to convince GHC that indeed we have
227
+ -- a 'MonadReader' instance defined on 'GenIO' for the 'PayMonad'.
228
+ newtype Env = Env { getEnv :: GenIO }
186
229
187
230
-- | \"Invalid\" 'MonadRandom' instance for 'PayMonad' which generates
188
231
-- randomness using the hash of 'NonEmpty (Address, Coin)' as fixed seed,
@@ -191,13 +234,10 @@ newtype PayMonad a = PayMonad {
191
234
-- which is yet deterministically reproduceable by feeding the same set of
192
235
-- payees.
193
236
instance MonadRandom PayMonad where
194
- getRandomBytes _ = do
195
- (initialSeed, counterRef) <- ask
196
- counterValue <- readIORef counterRef
197
- let randVal = ByteArray. convert . mappend (Bi. serialize' counterValue)
198
- $ initialSeed
199
- modifyIORef' counterRef succ
200
- return randVal
237
+ getRandomBytes len = do
238
+ gen <- asks getEnv
239
+ randomBytes <- liftIO (asGenIO (flip uniformVector len) gen)
240
+ return $ ByteArray. convert (B. pack $ V. toList randomBytes)
201
241
202
242
{- ------------------------------------------------------------------------------
203
243
Estimating fees
@@ -213,22 +253,17 @@ instance Arbitrary EstimateFeesError where
213
253
arbitrary = EstFeesTxCreationFailed <$> arbitrary
214
254
215
255
estimateFees :: ActiveWallet
216
- -> IO Address
217
- -- ^ A computation to generate a new change 'Address.
218
- -> (Address -> Either CoinSelHardErr SafeSigner )
219
- -- ^ A function to sign each Address
256
+ -> PassPhrase
257
+ -- ^ The spending password.
220
258
-> CoinSelectionOptions
221
259
-- ^ The options describing how to tune the coin selection.
222
260
-> HdAccountId
223
- -- ^ The source @(root, account) from where the payment was
224
- -- originated
261
+ -- ^ The source HD Account from where the payment should originate
225
262
-> NonEmpty (Address , Coin )
226
263
-- ^ The payees
227
264
-> IO (Either EstimateFeesError Coin )
228
- estimateFees activeWallet@ ActiveWallet {.. } genChangeAddr signAddress options accountId payees = do
229
- snapshot <- getWalletSnapshot walletPassive
230
- let originalUtxo = accountAvailableUtxo snapshot accountId
231
- res <- newTransaction activeWallet genChangeAddr signAddress options accountId payees
265
+ estimateFees activeWallet@ ActiveWallet {.. } spendingPassword options accountId payees = do
266
+ (res, originalUtxo) <- newTransaction activeWallet spendingPassword options accountId payees
232
267
case res of
233
268
Left e -> return . Left . EstFeesTxCreationFailed $ e
234
269
Right tx -> -- calculate the fee as the difference between inputs and outputs.
0 commit comments