@@ -26,8 +26,10 @@ module Cardano.Wallet.Kernel.DB.AcidState (
26
26
-- *** DELETE
27
27
, DeleteHdRoot (.. )
28
28
, DeleteHdAccount (.. )
29
- -- * errors
29
+ -- * Errors
30
30
, NewPendingError
31
+ -- * Testing
32
+ , ObservableRollbackUseInTestsOnly (.. )
31
33
) where
32
34
33
35
import Universum
@@ -36,15 +38,17 @@ import Control.Lens.TH (makeLenses)
36
38
import Control.Monad.Except (MonadError , catchError )
37
39
38
40
import Data.Acid (Query , Update , makeAcidic )
41
+ import qualified Data.Map.Merge.Strict as Map.Merge
39
42
import qualified Data.Map.Strict as Map
40
43
import Data.SafeCopy (base , deriveSafeCopy )
41
44
42
45
import qualified Pos.Core as Core
43
46
import Pos.Core.Chrono (OldestFirst (.. ))
44
47
import Pos.Txp (Utxo )
45
48
49
+ import qualified Cardano.Wallet.Kernel.DB.Util.IxSet as IxSet
46
50
import Cardano.Wallet.Kernel.PrefilterTx (AddrWithId ,
47
- PrefilteredBlock (.. ))
51
+ PrefilteredBlock (.. ), emptyPrefilteredBlock )
48
52
49
53
import Cardano.Wallet.Kernel.DB.HdWallet
50
54
import qualified Cardano.Wallet.Kernel.DB.HdWallet.Create as HD
@@ -145,21 +149,26 @@ cancelPending cancelled = void . runUpdate' . zoom dbHdWallets $
145
149
-- * For every address encountered in the block outputs, create an HdAddress if it
146
150
-- does not already exist.
147
151
applyBlock :: Map HdAccountId PrefilteredBlock -> Update DB ()
148
- applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $
152
+ applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $ do
153
+ blocksByAccount' <- fillInEmptyBlock blocksByAccount
149
154
createPrefiltered
150
155
initUtxoAndAddrs
151
156
(\ prefBlock -> zoom hdAccountCheckpoints $
152
157
modify $ Spec. applyBlock prefBlock)
153
- blocksByAccount
158
+ blocksByAccount'
154
159
where
155
- -- NOTE: When we create the new wallet, we look at the genesis UTxO and create
156
- -- an initial balance for all accounts that we recognize as ours. This means that
157
- -- when we later discover a new account that is also ours, it cannot appear
158
- -- in the genesis UTxO, because if it did, we would already have seen it
159
- -- (the genesis UTxO is static, after all). Hence we use empty initial utxo
160
- -- for accounts discovered during applyBlock.
161
- -- The Addrs need to be created during account initialisation and so we pass them here.
162
- initUtxoAndAddrs :: PrefilteredBlock -> (Utxo ,[AddrWithId ])
160
+ -- Initial UTxO and addresses for a new account
161
+ --
162
+ -- NOTE: When we initialize the kernel, we look at the genesis UTxO and create
163
+ -- an initial balance for all accounts that we recognize as ours. This means
164
+ -- that when we later discover a new account that is also ours, it cannot appear
165
+ -- in the genesis UTxO, because if it did, we would already have seen it (the
166
+ -- genesis UTxO is static, after all). Hence we use empty initial utxo for
167
+ -- accounts discovered during 'applyBlock' (and 'switchToFork')
168
+ --
169
+ -- The Addrs need to be created during account initialisation and so we pass
170
+ -- them here.
171
+ initUtxoAndAddrs :: PrefilteredBlock -> (Utxo , [AddrWithId ])
163
172
initUtxoAndAddrs pb = (Map. empty, pfbAddrs pb)
164
173
165
174
-- | Switch to a fork
@@ -169,11 +178,40 @@ applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $
169
178
-- TODO: We use a plain list here rather than 'OldestFirst' since the latter
170
179
-- does not have a 'SafeCopy' instance.
171
180
switchToFork :: Int
172
- -> [PrefilteredBlock ]
181
+ -> [Map HdAccountId PrefilteredBlock ]
173
182
-> Update DB ()
174
- switchToFork n blocks = runUpdateNoErrors $
183
+ switchToFork n blocks = runUpdateNoErrors $ zoom dbHdWallets $ do
184
+ blocks' <- mapM fillInEmptyBlock blocks
185
+ createPrefiltered
186
+ initUtxoAndAddrs
187
+ (\ prefBlocks -> zoom hdAccountCheckpoints $
188
+ modify $ Spec. switchToFork n (OldestFirst prefBlocks))
189
+ (distribute blocks')
190
+ where
191
+ -- The natural result of prefiltering each block is a list of maps, but
192
+ -- in order to apply them to each account, we want a map of lists
193
+ --
194
+ -- NOTE: We have to be careful to /first/ use 'fillInEmptyBlock' to make
195
+ -- sure that if, say, the first and third slot both contain a block for
196
+ -- account A, but the second does not, we end up with an empty block
197
+ -- inserted for slot 2.
198
+ distribute :: [Map HdAccountId PrefilteredBlock ]
199
+ -> Map HdAccountId [PrefilteredBlock ]
200
+ distribute = Map. unionsWith (++) . map (Map. map (: [] ))
201
+
202
+ -- See comments in 'applyBlock'
203
+ initUtxoAndAddrs :: [PrefilteredBlock ] -> (Utxo , [AddrWithId ])
204
+ initUtxoAndAddrs pbs = (Map. empty, concatMap pfbAddrs pbs)
205
+
206
+ -- | Observable rollback, used for tests only
207
+ --
208
+ -- See 'switchToFork' for use in real code.
209
+ observableRollbackUseInTestsOnly :: Update DB ()
210
+ observableRollbackUseInTestsOnly = runUpdateNoErrors $
175
211
zoomAll (dbHdWallets . hdWalletsAccounts) $
176
- hdAccountCheckpoints %~ Spec. switchToFork n (OldestFirst blocks)
212
+ hdAccountCheckpoints %~ Spec. observableRollbackUseInTestsOnly
213
+
214
+
177
215
178
216
{- ------------------------------------------------------------------------------
179
217
Wallet creation
@@ -200,8 +238,37 @@ createHdWallet newRoot utxoByAccount = runUpdate' . zoom dbHdWallets $ do
200
238
Internal auxiliary: apply a function to a prefiltered block/utxo
201
239
-------------------------------------------------------------------------------}
202
240
241
+ -- | Given a map from account IDs, add default values for all accounts in
242
+ -- the wallet that aren't given a value in the map
243
+ fillInDefaults :: forall p e .
244
+ (HdAccount -> p ) -- ^ Default value
245
+ -> Map HdAccountId p -- ^ Map with values per account
246
+ -> Update' HdWallets e (Map HdAccountId p )
247
+ fillInDefaults def accs =
248
+ aux <$> IxSet. toMap <$> use hdWalletsAccounts
249
+ where
250
+ aux :: Map HdAccountId HdAccount -> Map HdAccountId p
251
+ aux = Map.Merge. merge newAccount needsDefault valueForExistingAcc accs
252
+
253
+ newAccount :: Map.Merge. SimpleWhenMissing HdAccountId p p
254
+ newAccount = Map.Merge. mapMaybeMissing $ \ _accId p -> Just p
255
+
256
+ needsDefault :: Map.Merge. SimpleWhenMissing HdAccountId HdAccount p
257
+ needsDefault = Map.Merge. mapMaybeMissing $ \ _accId acc -> Just (def acc)
258
+
259
+ valueForExistingAcc :: Map.Merge. SimpleWhenMatched HdAccountId p HdAccount p
260
+ valueForExistingAcc = Map.Merge. zipWithMatched $ \ _accId p _acc -> p
261
+
262
+ -- | Specialization of 'fillInDefaults' for prefiltered blocks
263
+ fillInEmptyBlock :: Map HdAccountId PrefilteredBlock
264
+ -> Update' HdWallets e (Map HdAccountId PrefilteredBlock )
265
+ fillInEmptyBlock = fillInDefaults (const emptyPrefilteredBlock)
266
+
203
267
-- | For each of the specified accounts, create them if they do not exist,
204
268
-- and apply the specified function.
269
+ --
270
+ -- NOTE: Any accounts that aren't in the map are simply skilled. See
271
+ -- 'fillInDefaults'.
205
272
createPrefiltered :: forall p e .
206
273
(p -> (Utxo ,[AddrWithId ]))
207
274
-- ^ Initial UTxO (when we are creating the account),
@@ -242,7 +309,6 @@ createPrefiltered initUtxoAndAddrs applyP accs = do
242
309
firstCheckpoint utxo' = Checkpoint {
243
310
_checkpointUtxo = InDb utxo'
244
311
, _checkpointUtxoBalance = InDb $ Spec. balance utxo'
245
- , _checkpointExpected = InDb Map. empty
246
312
, _checkpointPending = Pending . InDb $ Map. empty
247
313
-- Since this is the first checkpoint before we have applied
248
314
-- any blocks, the block metadata is empty
@@ -314,4 +380,6 @@ makeAcidic ''DB [
314
380
, 'updateHdAccountName
315
381
, 'deleteHdRoot
316
382
, 'deleteHdAccount
383
+ -- Testing
384
+ , 'observableRollbackUseInTestsOnly
317
385
]
0 commit comments