@@ -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
@@ -38,6 +40,7 @@ import Control.Monad.Except (MonadError, catchError)
38
40
import Test.QuickCheck (Arbitrary (.. ), oneof )
39
41
40
42
import Data.Acid (Query , Update , makeAcidic )
43
+ import qualified Data.Map.Merge.Strict as Map.Merge
41
44
import qualified Data.Map.Strict as Map
42
45
import Data.SafeCopy (base , deriveSafeCopy )
43
46
import Formatting (bprint , build , (%) )
@@ -47,8 +50,9 @@ import Pos.Core.Chrono (OldestFirst (..))
47
50
import qualified Pos.Core.Txp as Txp
48
51
import Pos.Txp (Utxo )
49
52
53
+ import qualified Cardano.Wallet.Kernel.DB.Util.IxSet as IxSet
50
54
import Cardano.Wallet.Kernel.PrefilterTx (AddrWithId ,
51
- PrefilteredBlock (.. ))
55
+ PrefilteredBlock (.. ), emptyPrefilteredBlock )
52
56
53
57
import Cardano.Wallet.Kernel.DB.HdWallet
54
58
import qualified Cardano.Wallet.Kernel.DB.HdWallet.Create as HD
@@ -160,21 +164,26 @@ cancelPending cancelled = void . runUpdate' . zoom dbHdWallets $
160
164
-- * For every address encountered in the block outputs, create an HdAddress if it
161
165
-- does not already exist.
162
166
applyBlock :: Map HdAccountId PrefilteredBlock -> Update DB ()
163
- applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $
167
+ applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $ do
168
+ blocksByAccount' <- fillInEmptyBlock blocksByAccount
164
169
createPrefiltered
165
170
initUtxoAndAddrs
166
171
(\ prefBlock -> zoom hdAccountCheckpoints $
167
172
modify $ Spec. applyBlock prefBlock)
168
- blocksByAccount
173
+ blocksByAccount'
169
174
where
170
- -- NOTE: When we create the new wallet, we look at the genesis UTxO and create
171
- -- an initial balance for all accounts that we recognize as ours. This means that
172
- -- when we later discover a new account that is also ours, it cannot appear
173
- -- in the genesis UTxO, because if it did, we would already have seen it
174
- -- (the genesis UTxO is static, after all). Hence we use empty initial utxo
175
- -- for accounts discovered during applyBlock.
176
- -- The Addrs need to be created during account initialisation and so we pass them here.
177
- initUtxoAndAddrs :: PrefilteredBlock -> (Utxo ,[AddrWithId ])
175
+ -- Initial UTxO and addresses for a new account
176
+ --
177
+ -- NOTE: When we initialize the kernel, we look at the genesis UTxO and create
178
+ -- an initial balance for all accounts that we recognize as ours. This means
179
+ -- that when we later discover a new account that is also ours, it cannot appear
180
+ -- in the genesis UTxO, because if it did, we would already have seen it (the
181
+ -- genesis UTxO is static, after all). Hence we use empty initial utxo for
182
+ -- accounts discovered during 'applyBlock' (and 'switchToFork')
183
+ --
184
+ -- The Addrs need to be created during account initialisation and so we pass
185
+ -- them here.
186
+ initUtxoAndAddrs :: PrefilteredBlock -> (Utxo , [AddrWithId ])
178
187
initUtxoAndAddrs pb = (Map. empty, pfbAddrs pb)
179
188
180
189
-- | Switch to a fork
@@ -184,11 +193,40 @@ applyBlock blocksByAccount = runUpdateNoErrors $ zoom dbHdWallets $
184
193
-- TODO: We use a plain list here rather than 'OldestFirst' since the latter
185
194
-- does not have a 'SafeCopy' instance.
186
195
switchToFork :: Int
187
- -> [PrefilteredBlock ]
196
+ -> [Map HdAccountId PrefilteredBlock ]
188
197
-> Update DB ()
189
- switchToFork n blocks = runUpdateNoErrors $
198
+ switchToFork n blocks = runUpdateNoErrors $ zoom dbHdWallets $ do
199
+ blocks' <- mapM fillInEmptyBlock blocks
200
+ createPrefiltered
201
+ initUtxoAndAddrs
202
+ (\ prefBlocks -> zoom hdAccountCheckpoints $
203
+ modify $ Spec. switchToFork n (OldestFirst prefBlocks))
204
+ (distribute blocks')
205
+ where
206
+ -- The natural result of prefiltering each block is a list of maps, but
207
+ -- in order to apply them to each account, we want a map of lists
208
+ --
209
+ -- NOTE: We have to be careful to /first/ use 'fillInEmptyBlock' to make
210
+ -- sure that if, say, the first and third slot both contain a block for
211
+ -- account A, but the second does not, we end up with an empty block
212
+ -- inserted for slot 2.
213
+ distribute :: [Map HdAccountId PrefilteredBlock ]
214
+ -> Map HdAccountId [PrefilteredBlock ]
215
+ distribute = Map. unionsWith (++) . map (Map. map (: [] ))
216
+
217
+ -- See comments in 'applyBlock'
218
+ initUtxoAndAddrs :: [PrefilteredBlock ] -> (Utxo , [AddrWithId ])
219
+ initUtxoAndAddrs pbs = (Map. empty, concatMap pfbAddrs pbs)
220
+
221
+ -- | Observable rollback, used for tests only
222
+ --
223
+ -- See 'switchToFork' for use in real code.
224
+ observableRollbackUseInTestsOnly :: Update DB ()
225
+ observableRollbackUseInTestsOnly = runUpdateNoErrors $
190
226
zoomAll (dbHdWallets . hdWalletsAccounts) $
191
- hdAccountCheckpoints %~ Spec. switchToFork n (OldestFirst blocks)
227
+ hdAccountCheckpoints %~ Spec. observableRollbackUseInTestsOnly
228
+
229
+
192
230
193
231
{- ------------------------------------------------------------------------------
194
232
Wallet creation
@@ -215,8 +253,37 @@ createHdWallet newRoot utxoByAccount = runUpdate' . zoom dbHdWallets $ do
215
253
Internal auxiliary: apply a function to a prefiltered block/utxo
216
254
-------------------------------------------------------------------------------}
217
255
256
+ -- | Given a map from account IDs, add default values for all accounts in
257
+ -- the wallet that aren't given a value in the map
258
+ fillInDefaults :: forall p e .
259
+ (HdAccount -> p ) -- ^ Default value
260
+ -> Map HdAccountId p -- ^ Map with values per account
261
+ -> Update' HdWallets e (Map HdAccountId p )
262
+ fillInDefaults def accs =
263
+ aux . IxSet. toMap <$> use hdWalletsAccounts
264
+ where
265
+ aux :: Map HdAccountId HdAccount -> Map HdAccountId p
266
+ aux = Map.Merge. merge newAccount needsDefault valueForExistingAcc accs
267
+
268
+ newAccount :: Map.Merge. SimpleWhenMissing HdAccountId p p
269
+ newAccount = Map.Merge. mapMaybeMissing $ \ _accId p -> Just p
270
+
271
+ needsDefault :: Map.Merge. SimpleWhenMissing HdAccountId HdAccount p
272
+ needsDefault = Map.Merge. mapMaybeMissing $ \ _accId acc -> Just (def acc)
273
+
274
+ valueForExistingAcc :: Map.Merge. SimpleWhenMatched HdAccountId p HdAccount p
275
+ valueForExistingAcc = Map.Merge. zipWithMatched $ \ _accId p _acc -> p
276
+
277
+ -- | Specialization of 'fillInDefaults' for prefiltered blocks
278
+ fillInEmptyBlock :: Map HdAccountId PrefilteredBlock
279
+ -> Update' HdWallets e (Map HdAccountId PrefilteredBlock )
280
+ fillInEmptyBlock = fillInDefaults (const emptyPrefilteredBlock)
281
+
218
282
-- | For each of the specified accounts, create them if they do not exist,
219
283
-- and apply the specified function.
284
+ --
285
+ -- NOTE: Any accounts that aren't in the map are simply skilled. See
286
+ -- 'fillInDefaults'.
220
287
createPrefiltered :: forall p e .
221
288
(p -> (Utxo ,[AddrWithId ]))
222
289
-- ^ Initial UTxO (when we are creating the account),
@@ -257,7 +324,6 @@ createPrefiltered initUtxoAndAddrs applyP accs = do
257
324
firstCheckpoint utxo' = Checkpoint {
258
325
_checkpointUtxo = InDb utxo'
259
326
, _checkpointUtxoBalance = InDb $ Spec. balance utxo'
260
- , _checkpointExpected = InDb Map. empty
261
327
, _checkpointPending = Pending . InDb $ Map. empty
262
328
-- Since this is the first checkpoint before we have applied
263
329
-- any blocks, the block metadata is empty
@@ -329,4 +395,6 @@ makeAcidic ''DB [
329
395
, 'updateHdAccountName
330
396
, 'deleteHdRoot
331
397
, 'deleteHdAccount
398
+ -- Testing
399
+ , 'observableRollbackUseInTestsOnly
332
400
]
0 commit comments