haskoin-wallet 0.4.2 → 0.9.4
raw patch · 49 files changed
+8179/−8052 lines, 49 filesdep +Decimaldep +ansi-terminaldep +base16-bytestringdep −daemonsdep −deepseqdep −exceptionsdep ~HUnitdep ~QuickCheckdep ~aesonnew-uploaderPVP ok
version bump matches the API change (PVP)
Dependencies added: Decimal, ansi-terminal, base16-bytestring, base64-bytestring, entropy, haskoin-store-data, hspec, http-types, lens, lens-aeson, optparse-applicative, pretty, random, raw-strings-qq, secp256k1-haskell, transformers, wreq
Dependencies removed: daemons, deepseq, exceptions, file-embed, filepath, haskoin-node, lifted-async, lifted-base, monad-control, persistent-template, resourcet, semigroups, stm, stm-chans, stm-conduit, test-framework, test-framework-hunit, test-framework-quickcheck2, transformers-base, unix, yaml, zeromq4-haskell
Dependency ranges changed: HUnit, QuickCheck, aeson, aeson-pretty, base, bytestring, cereal, conduit, containers, data-default, directory, esqueleto, haskeline, haskoin-core, monad-logger, mtl, persistent, persistent-sqlite, split, string-conversions, text, time, unordered-containers
API changes (from Hackage documentation)
Files
- Network/Haskoin/Wallet.hs +0/−108
- Network/Haskoin/Wallet/Accounts.hs +0/−709
- Network/Haskoin/Wallet/Block.hs +0/−59
- Network/Haskoin/Wallet/Client.hs +0/−252
- Network/Haskoin/Wallet/Client/Commands.hs +0/−970
- Network/Haskoin/Wallet/Client/PrettyJson.hs +0/−23
- Network/Haskoin/Wallet/Database.hs +0/−17
- Network/Haskoin/Wallet/Internals.hs +0/−31
- Network/Haskoin/Wallet/Model.hs +0/−134
- Network/Haskoin/Wallet/Server.hs +0/−510
- Network/Haskoin/Wallet/Server/Handler.hs +0/−604
- Network/Haskoin/Wallet/Settings.hs +0/−196
- Network/Haskoin/Wallet/Transaction.hs +0/−1358
- Network/Haskoin/Wallet/Types.hs +0/−790
- Network/Haskoin/Wallet/Types/BlockInfo.hs +0/−77
- app/Main.hs +1/−3
- config/config.yml +0/−118
- config/help +0/−52
- config/models +0/−84
- examples/embedded-inproc-wallet-server/Main.hs +0/−183
- haskoin-wallet.cabal +188/−173
- src/Haskoin/Wallet.hs +24/−0
- src/Haskoin/Wallet/Amounts.hs +88/−0
- src/Haskoin/Wallet/Backup.hs +358/−0
- src/Haskoin/Wallet/Commands.hs +833/−0
- src/Haskoin/Wallet/Config.hs +114/−0
- src/Haskoin/Wallet/Database.hs +1424/−0
- src/Haskoin/Wallet/Entropy.hs +221/−0
- src/Haskoin/Wallet/FileIO.hs +131/−0
- src/Haskoin/Wallet/Main.hs +25/−0
- src/Haskoin/Wallet/Migration.hs +67/−0
- src/Haskoin/Wallet/Migration/SemVersion.hs +64/−0
- src/Haskoin/Wallet/Migration/V0_9_0.hs +198/−0
- src/Haskoin/Wallet/Parser.hs +1083/−0
- src/Haskoin/Wallet/PrettyPrinter.hs +547/−0
- src/Haskoin/Wallet/Signing.hs +310/−0
- src/Haskoin/Wallet/TxInfo.hs +403/−0
- src/Haskoin/Wallet/Util.hs +117/−0
- test/Haskoin/Wallet/AmountsSpec.hs +68/−0
- test/Haskoin/Wallet/CommandsSpec.hs +811/−0
- test/Haskoin/Wallet/EntropySpec.hs +265/−0
- test/Haskoin/Wallet/SigningSpec.hs +471/−0
- test/Haskoin/Wallet/TestUtils.hs +315/−0
- test/Haskoin/Wallet/VersionSpec.hs +52/−0
- test/Spec.hs +1/−0
- tests/Main.hs +0/−16
- tests/Network/Haskoin/Wallet/Arbitrary.hs +0/−37
- tests/Network/Haskoin/Wallet/Tests.hs +0/−24
- tests/Network/Haskoin/Wallet/Units.hs +0/−1524
@@ -1,108 +0,0 @@-{-|- This package provides a command line application called /hw/ (haskoin- wallet). It is a lightweight bitcoin wallet featuring BIP32 key management,- deterministic signatures (RFC-6979) and first order support for- multisignature transactions. A library API for /hw/ is also exposed.--}-module Network.Haskoin.Wallet-(--- *Client- clientMain-, OutputFormat(..)-, Config(..)---- *Server-, runSPVServer-, stopSPVServer-, SPVMode(..)---- *API JSON Types-, JsonAccount(..)-, JsonAddr(..)-, JsonCoin(..)-, JsonTx(..)---- *API Request Types-, WalletRequest(..)-, ListRequest(..)-, NewAccount(..)-, SetAccountGap(..)-, OfflineTxData(..)-, CoinSignData(..)-, TxAction(..)-, AddressLabel(..)-, NodeAction(..)-, AccountType(..)-, AddressType(..)-, addrTypeIndex-, TxType(..)-, TxConfidence(..)-, AddressInfo(..)-, BalanceInfo(..)---- *API Response Types-, WalletResponse(..)-, TxCompleteRes(..)-, ListResult(..)-, RescanRes(..)---- *Database Accounts-, initWallet-, accounts-, newAccount-, addAccountKeys-, getAccount-, isMultisigAccount-, isReadAccount-, isCompleteAccount---- *Database Addresses-, getAddress-, addressesAll-, addresses-, addressList-, unusedAddresses-, addressCount-, setAddrLabel-, addressPrvKey-, useAddress-, setAccountGap-, firstAddrTime-, getPathRedeem-, getPathPubKey---- *Database Bloom Filter-, getBloomFilter---- *Database transactions-, txs-, addrTxs-, getTx-, getAccountTx-, importTx-, importNetTx-, signAccountTx-, createWalletTx-, signOfflineTx-, getOfflineTxData---- *Database blocks-, importMerkles-, walletBestBlock---- *Database coins and balances-, spendableCoins-, accountBalance-, addressBalances---- *Rescan-, resetRescan-) where--import Network.Haskoin.Wallet.Client-import Network.Haskoin.Wallet.Server-import Network.Haskoin.Wallet.Settings-import Network.Haskoin.Wallet.Types-import Network.Haskoin.Wallet.Accounts-import Network.Haskoin.Wallet.Transaction-
@@ -1,709 +0,0 @@-module Network.Haskoin.Wallet.Accounts-(--- *Database Wallet- initWallet---- *Database Accounts-, accounts-, newAccount-, renameAccount-, addAccountKeys-, getAccount-, isMultisigAccount-, isReadAccount-, isCompleteAccount---- *Database Addresses-, getAddress-, addressesAll-, addresses-, addressList-, unusedAddresses-, lookupByPubKey-, addressCount-, setAddrLabel-, addressPrvKey-, useAddress-, generateAddrs-, setAccountGap-, firstAddrTime-, getPathRedeem-, getPathPubKey---- *Database Bloom Filter-, getBloomFilter---- * Helpers-, subSelectAddrCount-) where--import Control.Applicative ((<|>))-import Control.Monad (unless, void, when)-import Control.Monad.Trans (MonadIO, liftIO)-import Control.Monad.Base (MonadBase)-import Control.Monad.Catch (MonadThrow, throwM)-import Control.Monad.Trans.Resource (MonadResource)-import Control.Exception (throw)--import Data.Text (Text, unpack)-import Data.Maybe (mapMaybe, listToMaybe, isJust, isNothing)-import Data.Time.Clock (getCurrentTime)-import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)-import Data.List (nub)-import Data.Word (Word32)-import Data.String.Conversions (cs)-import Data.Serialize (encode)--import qualified Database.Persist as P (updateWhere, update , (=.))-import Database.Esqueleto- ( Value(..), SqlExpr- , select, from, where_, val, sub_select, countRows, count, unValue- , orderBy, limit, asc, desc, offset, get, countDistinct- , max_, case_, when_, then_, else_- , (^.), (==.), (&&.), (>.), (-.), (<.)- -- Reexports from Database.Persist- , SqlPersistT, Entity(..)- , insertUnique, insert_- )--import Network.Haskoin.Crypto-import Network.Haskoin.Block-import Network.Haskoin.Script-import Network.Haskoin.Node-import Network.Haskoin.Util-import Network.Haskoin.Constants-import Network.Haskoin.Node.HeaderTree--import Network.Haskoin.Wallet.Types-import Network.Haskoin.Wallet.Model--{- Initialization -}--initWallet :: MonadIO m => Double -> SqlPersistT m ()-initWallet fpRate = do- prevConfigRes <- select $ from $ \c -> return $ count $ c ^. WalletStateId- let cnt = maybe 0 unValue $ listToMaybe prevConfigRes- when (cnt == (0 :: Int)) $ do- time <- liftIO getCurrentTime- -- Create an initial bloom filter- -- TODO: Compute a random nonce- let bloom = bloomCreate (filterLen 0) fpRate 0 BloomUpdateNone- insert_ WalletState- { walletStateHeight = 0- , walletStateBlock = headerHash genesisHeader- , walletStateBloomFilter = bloom- , walletStateBloomElems = 0- , walletStateBloomFp = fpRate- , walletStateVersion = 1- , walletStateCreated = time- }--{- Account -}---- | Fetch all accounts-accounts :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => ListRequest -> SqlPersistT m ([Account], Word32)-accounts ListRequest{..} = do- cntRes <- select $ from $ \acc ->- return $ countDistinct $ acc ^. AccountId-- let cnt = maybe 0 unValue $ listToMaybe cntRes-- when (listOffset > 0 && listOffset >= cnt) $ throw $ WalletException- "Offset beyond end of data set"-- res <- fmap (map entityVal) $ select $ from $ \acc -> do- limitOffset listLimit listOffset- return acc-- return (res, cnt)--initGap :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account -> SqlPersistT m ()-initGap accE = do- void $ createAddrs accE AddressExternal 20- void $ createAddrs accE AddressInternal 20---- | Create a new account-newAccount :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => NewAccount- -> SqlPersistT m (Entity Account, Maybe Mnemonic)-newAccount NewAccount{..} = do- unless (validAccountType newAccountType) $- throwM $ WalletException "Invalid account type"- let gen = isNothing newAccountMnemonic &&- isNothing newAccountMaster &&- null newAccountKeys- (mnemonicM, masterM, keys) <- if gen- then do- when (isJust newAccountMaster || isJust newAccountMnemonic) $- throwM $ WalletException- "Master key or mnemonic not allowed for generate"- ent <- liftIO $ getEntropy 16- let ms = fromRight $ toMnemonic ent- root = makeXPrvKey $ fromRight $ mnemonicToSeed "" ms- master = case newAccountDeriv of- Nothing -> root- Just d -> derivePath d root- keys = deriveXPubKey master : newAccountKeys- return (Just ms, Just master, keys)- else case newAccountMnemonic of- Just ms -> do- when (isJust newAccountMaster) $ throwM $ WalletException- "Cannot provide both master key and mnemonic"- root <- case mnemonicToSeed "" (cs ms) of- Right s -> return $ makeXPrvKey s- Left _ -> throwM $ WalletException- "Mnemonic sentence invalid"- let master = case newAccountDeriv of- Nothing -> root- Just d -> derivePath d root- keys = deriveXPubKey master : newAccountKeys- return (Nothing, Just master, keys)- Nothing -> case newAccountMaster of- Just master -> do- let keys = deriveXPubKey master : newAccountKeys- return (Nothing, newAccountMaster, keys)- Nothing -> return (Nothing, newAccountMaster, newAccountKeys)-- -- Build the account- now <- liftIO getCurrentTime- let acc = Account- { accountName = newAccountName- , accountType = newAccountType- , accountMaster = if newAccountReadOnly- then Nothing- else masterM- , accountDerivation = newAccountDeriv- , accountKeys = nub keys- , accountGap = 0- , accountCreated = now- }-- -- Check if all the keys are valid- unless (isValidAccKeys acc) $- throwM $ WalletException "Invalid account keys"-- -- Insert our account in the database- let canSetGap = isCompleteAccount acc- newAcc = acc{ accountGap = if canSetGap then 10 else 0 }-- insertUnique newAcc >>= \resM -> case resM of- -- The account got created.- Just ai -> do- let accE = Entity ai newAcc- -- If we can set the gap, create the gap addresses- when canSetGap $ initGap accE- return (accE, mnemonicM)- -- The account already exists- Nothing -> throwM $ WalletException "Account already exists"--renameAccount :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account- -> AccountName- -> SqlPersistT m Account-renameAccount (Entity ai acc) name = do- P.update ai [ AccountName P.=. name ]- return $ acc{ accountName = name }---- | Add new thirdparty keys to a multisignature account. This function can--- fail if the multisignature account already has all required keys.-addAccountKeys :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account -- ^ Account Entity- -> [XPubKey] -- ^ Thirdparty public keys to add- -> SqlPersistT m Account -- ^ Account information-addAccountKeys (Entity ai acc) keys- -- We can only add keys on incomplete accounts- | isCompleteAccount acc = throwM $- WalletException "The account is already complete"- | null keys || not (isValidAccKeys accKeys) = throwM $- WalletException "Invalid account keys"- | otherwise = do- let canSetGap = isCompleteAccount accKeys- updGap = [ AccountGap P.=. 10 | canSetGap]- newAcc = accKeys{ accountGap = if canSetGap then 10 else 0 }- -- Update the account with the keys and the new gap if it is complete- P.update ai $ (AccountKeys P.=. newKeys) : updGap- -- If we can set the gap, create the gap addresses- when canSetGap $ initGap $ Entity ai newAcc- return newAcc- where- newKeys = accountKeys acc ++ keys- accKeys = acc{ accountKeys = newKeys }--isValidAccKeys :: Account -> Bool-isValidAccKeys Account{..} = testMaster && case accountType of- AccountRegular -> length accountKeys == 1- AccountMultisig _ n -> goMultisig n- where- goMultisig n =- length accountKeys == length (nub accountKeys) &&- length accountKeys <= n && not (null accountKeys)- testMaster = case accountMaster of- Just m -> deriveXPubKey m `elem` accountKeys- Nothing -> True---- Helper functions to get an Account if it exists, or throw an exception--- otherwise.-getAccount :: (MonadIO m, MonadThrow m) => AccountName- -> SqlPersistT m (Entity Account)-getAccount accountName = do- as <- select $ from $ \a -> do- where_ $ a ^. AccountName ==. val accountName- return a- case as of- (accEnt:_) -> return accEnt- _ -> throwM $ WalletException $ unwords- [ "Account", unpack accountName, "does not exist" ]--{- Addresses -}---- | Get an address if it exists, or throw an exception otherwise. Fetching--- addresses in the hidden gap will also throw an exception.-getAddress :: (MonadIO m, MonadThrow m)- => Entity Account -- ^ Account Entity- -> AddressType -- ^ Address type- -> KeyIndex -- ^ Derivation index (key)- -> SqlPersistT m (Entity WalletAddr) -- ^ Address-getAddress accE@(Entity ai _) addrType index = do- res <- select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- &&. x ^. WalletAddrIndex ==. val index- &&. x ^. WalletAddrIndex <. subSelectAddrCount accE addrType- )- limit 1- return x- case res of- (addrE:_) -> return addrE- _ -> throwM $ WalletException $ unwords- [ "Invalid address index", show index ]---- | All addresses in the wallet, including hidden gap addresses. This is useful--- for building a bloom filter.-addressesAll :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => SqlPersistT m [WalletAddr]-addressesAll = fmap (map entityVal) $ select $ from return---- | All addresses in one account excluding hidden gap.-addresses :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account -- ^ Account Entity- -> AddressType -- ^ Address Type- -> SqlPersistT m [WalletAddr] -- ^ Addresses-addresses accE@(Entity ai _) addrType = fmap (map entityVal) $- select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- &&. x ^. WalletAddrIndex <. subSelectAddrCount accE addrType- )- return x---- | Get address list.-addressList :: MonadIO m- => Entity Account -- ^ Account Entity- -> AddressType -- ^ Address type- -> ListRequest -- ^ List request- -> SqlPersistT m ([WalletAddr], Word32)- -- ^ List result-addressList accE@(Entity ai _) addrType ListRequest{..} = do- cnt <- addressCount accE addrType-- when (listOffset > 0 && listOffset >= cnt) $ throw $ WalletException- "Offset beyond end of data set"-- res <- fmap (map entityVal) $ select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- &&. x ^. WalletAddrIndex <. val cnt- )- let order = if listReverse then asc else desc- orderBy [ order (x ^. WalletAddrIndex) ]- when (listLimit > 0) $ limit $ fromIntegral listLimit- when (listOffset > 0) $ offset $ fromIntegral listOffset- return x-- return (res, cnt)---- | Get a count of all the addresses in an account-addressCount :: MonadIO m- => Entity Account -- ^ Account Entity- -> AddressType -- ^ Address type- -> SqlPersistT m Word32 -- ^ Address Count-addressCount (Entity ai acc) addrType = do- res <- select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- )- return countRows- let cnt = maybe 0 unValue $ listToMaybe res- return $ if cnt > accountGap acc then cnt - accountGap acc else 0---- | Get a list of all unused addresses.-unusedAddresses :: MonadIO m- => Entity Account -- ^ Account ID- -> AddressType -- ^ Address type- -> ListRequest- -> SqlPersistT m ([WalletAddr], Word32) -- ^ Unused addresses-unusedAddresses (Entity ai acc) addrType ListRequest{..} = do- cntRes <- select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- )- return countRows-- let cnt = maybe 0 unValue $ listToMaybe cntRes-- when (listOffset > 0 && listOffset >= gap) $ throw $ WalletException- "Offset beyond end of data set"-- res <- fmap (map entityVal) $ select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- )- orderBy [ order $ x ^. WalletAddrIndex ]- limit $ fromIntegral $ lim cnt- offset $ fromIntegral $ off cnt- return x- return (res, gap)- where- gap = accountGap acc- lim' = if listLimit > 0 then listLimit else gap- off cnt | listReverse = listOffset + gap- | otherwise = cnt - 2 * gap + listOffset- lim cnt | listReverse = min lim' (gap - listOffset)- | otherwise = min lim' (cnt - off cnt - gap)- order = if listReverse then desc else asc--lookupByPubKey :: (MonadIO m, MonadThrow m)- => Entity Account -- ^ Account Entity- -> PubKeyC -- ^ Pubkey of interest- -> AddressType -- ^ Address type- -> SqlPersistT m [WalletAddr]-lookupByPubKey (Entity ai _) key addrType =- fmap (map entityVal) $ select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- &&. x ^. WalletAddrKey ==. val (Just key)- )- return x---- | Add a label to an address.-setAddrLabel :: (MonadIO m, MonadThrow m)- => Entity Account -- ^ Account ID- -> KeyIndex -- ^ Derivation index- -> AddressType -- ^ Address type- -> Text -- ^ New label- -> SqlPersistT m WalletAddr-setAddrLabel accE i addrType label = do- Entity addrI addr <- getAddress accE addrType i- P.update addrI [ WalletAddrLabel P.=. label ]- return $ addr{ walletAddrLabel = label }---- | Returns the private key of an address.-addressPrvKey :: (MonadIO m, MonadThrow m)- => Entity Account -- ^ Account Entity- -> Maybe XPrvKey -- ^ If not in account- -> KeyIndex -- ^ Derivation index of the address- -> AddressType -- ^ Address type- -> SqlPersistT m PrvKeyC -- ^ Private key-addressPrvKey accE@(Entity ai acc) masterM index addrType = do- ret <- select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- &&. x ^. WalletAddrIndex ==. val index- &&. x ^. WalletAddrIndex <. subSelectAddrCount accE addrType- )- return $ x ^. WalletAddrIndex- case ret of- (Value idx:_) -> do- accKey <- case accountMaster acc <|> masterM of- Just key -> return key- Nothing -> throwM $ WalletException "Could not get private key"- let addrKey =- prvSubKey (prvSubKey accKey (addrTypeIndex addrType)) idx- return $ xPrvKey addrKey- _ -> throwM $ WalletException "Invalid address"---- | Create new addresses in an account and increment the internal bloom filter.--- This is a low-level function that simply creates the desired amount of new--- addresses in an account, disregarding visible and hidden address gaps. You--- should use the function `setAccountGap` if you want to control the gap of an--- account instead.-createAddrs :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account- -> AddressType- -> Word32- -> SqlPersistT m [WalletAddr]-createAddrs (Entity ai acc) addrType n- | n == 0 = throwM $ WalletException $- unwords [ "Invalid value", show n ]- | not (isCompleteAccount acc) =- throwM $ WalletException $ unwords- [ "Keys are still missing from the incomplete account"- , unpack $ accountName acc- ]- | otherwise = do- now <- liftIO getCurrentTime- -- Find the next derivation index from the last address- lastRes <- select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- )- return $ max_ (x ^. WalletAddrIndex)- let nextI = case lastRes of- (Value (Just lastI):_) -> lastI + 1- _ -> 0- build (addr, keyM, rdmM, i) = WalletAddr- { walletAddrAccount = ai- , walletAddrAddress = addr- , walletAddrIndex = i- , walletAddrType = addrType- , walletAddrLabel = ""- , walletAddrRedeem = rdmM- , walletAddrKey = keyM- , walletAddrCreated = now- }- res = map build $ take (fromIntegral n) $ deriveFrom nextI-- -- Save the addresses and increment the bloom filter- splitInsertMany_ res- incrementFilter res- return res- where- -- Branch type (external = 0, internal = 1)- branchType = addrTypeIndex addrType- deriveFrom = case accountType acc of- AccountMultisig m _ ->- let f (a, r, i) = (a, Nothing, Just r, i)- deriv = Deriv :/ branchType- in map f . derivePathMSAddrs (accountKeys acc) deriv m- AccountRegular -> case accountKeys acc of- (key:_) -> let f (a, k, i) = (a, Just k, Nothing, i)- in map f . derivePathAddrs key (Deriv :/ branchType)- [] -> throw $ WalletException $ unwords- [ "createAddrs: No key in regular account (corrupt database)"- , unpack $ accountName acc- ]---- | Generate all the addresses up to certain index.-generateAddrs :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account- -> AddressType- -> KeyIndex- -> SqlPersistT m Int-generateAddrs accE addrType genIndex = do- cnt <- addressCount accE addrType- let toGen = fromIntegral genIndex - fromIntegral cnt + 1- if toGen > 0- then do- void $ createAddrs accE addrType $ fromIntegral toGen- return toGen- else return 0---- | Use an address and make sure we have enough gap addresses after it.--- Returns the new addresses that have been created.-useAddress :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => WalletAddr -> SqlPersistT m [WalletAddr]-useAddress WalletAddr{..} = do- res <- select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val walletAddrAccount- &&. x ^. WalletAddrType ==. val walletAddrType- &&. x ^. WalletAddrIndex >. val walletAddrIndex- )- return countRows- case res of- (Value cnt:_) -> get walletAddrAccount >>= \accM -> case accM of- Just acc -> do- let accE = Entity walletAddrAccount acc- gap = fromIntegral (accountGap acc) :: Int- missing = 2*gap - cnt- if missing > 0- then createAddrs accE walletAddrType $ fromIntegral missing- else return []- _ -> return [] -- Should not happen- _ -> return [] -- Should not happen---- | Set the address gap of an account to a new value. This will create new--- internal and external addresses as required. The gap can only be increased,--- not decreased in size.-setAccountGap :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account -- ^ Account Entity- -> Word32 -- ^ New gap value- -> SqlPersistT m (Entity Account)-setAccountGap accE@(Entity ai acc) gap- | not (isCompleteAccount acc) =- throwM $ WalletException $ unwords- [ "Keys are still missing from the incomplete account"- , unpack $ accountName acc- ]- | missing <= 0 = throwM $ WalletException- "The gap of an account can only be increased"- | otherwise = do- _ <- createAddrs accE AddressExternal $ fromInteger $ missing*2- _ <- createAddrs accE AddressInternal $ fromInteger $ missing*2- P.update ai [ AccountGap P.=. gap ]- return $ Entity ai acc{ accountGap = gap }- where- missing = toInteger gap - toInteger (accountGap acc)---- Return the creation time of the first address in the wallet.-firstAddrTime :: MonadIO m => SqlPersistT m (Maybe Timestamp)-firstAddrTime = do- res <- select $ from $ \x -> do- orderBy [ asc (x ^. WalletAddrId) ]- limit 1- return $ x ^. WalletAddrCreated- return $ case res of- (Value d:_) -> Just $ toPOSIX d- _ -> Nothing- where- toPOSIX = fromInteger . round . utcTimeToPOSIXSeconds--{- Bloom filters -}---- | Add the given addresses to the bloom filter. If the number of elements--- becomes too large, a new bloom filter is computed from scratch.-incrementFilter :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => [WalletAddr]- -> SqlPersistT m ()-incrementFilter addrs = do- (bloom, elems, _) <- getBloomFilter- let newElems = elems + (length addrs * 2)- if filterLen newElems > filterLen elems- then computeNewFilter- else setBloomFilter (addToFilter bloom addrs) newElems---- | Generate a new bloom filter from the data in the database-computeNewFilter :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => SqlPersistT m ()-computeNewFilter = do- (_, _, fpRate) <- getBloomFilter- -- Create a new empty bloom filter- -- TODO: Choose a random nonce for the bloom filter- -- TODO: Check global bloom filter length limits- cntRes <- select $ from $ \x -> return $ count $ x ^. WalletAddrId- let elems = maybe 0 unValue $ listToMaybe cntRes- newBloom = bloomCreate (filterLen elems) fpRate 0 BloomUpdateNone- addrs <- addressesAll- let bloom = addToFilter newBloom addrs- setBloomFilter bloom elems---- Compute the size of a filter given a number of elements. Scale--- the filter length by powers of 2.-filterLen :: Int -> Int-filterLen = round . pow2 . ceiling . log2- where- pow2 x = (2 :: Double) ** fromInteger x- log2 x = logBase (2 :: Double) (fromIntegral x)---- | Add elements to a bloom filter-addToFilter :: BloomFilter -> [WalletAddr] -> BloomFilter-addToFilter bloom addrs =- bloom3- where- pks = mapMaybe walletAddrKey addrs- rdms = mapMaybe walletAddrRedeem addrs- -- Add the Hash160 of the addresses- f1 b a = bloomInsert b $ encode $ getAddrHash a- bloom1 = foldl f1 bloom $ map walletAddrAddress addrs- -- Add the redeem scripts- f2 b r = bloomInsert b $ encodeOutputBS r- bloom2 = foldl f2 bloom1 rdms- -- Add the public keys- f3 b p = bloomInsert b $ encode p- bloom3 = foldl f3 bloom2 pks---- | Returns a bloom filter containing all the addresses in this wallet. This--- includes internal and external addresses. The bloom filter can be set on a--- peer connection to filter the transactions received by that peer.-getBloomFilter :: (MonadIO m, MonadThrow m)- => SqlPersistT m (BloomFilter, Int, Double)-getBloomFilter = do- res <- select $ from $ \c -> do- limit 1- return ( c ^. WalletStateBloomFilter- , c ^. WalletStateBloomElems- , c ^. WalletStateBloomFp- )- case res of- ((Value b, Value n, Value fp):_) -> return (b, n, fp)- _ -> throwM $- WalletException "getBloomFilter: Database not initialized"---- | Save a bloom filter and the number of elements it contains-setBloomFilter :: MonadIO m => BloomFilter -> Int -> SqlPersistT m ()-setBloomFilter bloom elems =- P.updateWhere [] [ WalletStateBloomFilter P.=. bloom- , WalletStateBloomElems P.=. elems- ]---- Helper function to compute the redeem script of a given derivation path--- for a given multisig account.-getPathRedeem :: Account -> SoftPath -> RedeemScript-getPathRedeem acc@Account{..} deriv = case accountType of- AccountMultisig m _ -> if isCompleteAccount acc- then sortMulSig $ PayMulSig pubKeys m- else throw $ WalletException $ unwords- [ "getPathRedeem: Incomplete multisig account"- , unpack accountName- ]- _ -> throw $ WalletException $ unwords- [ "getPathRedeem: Account", unpack accountName- , "is not a multisig account"- ]- where- f = toPubKeyG . xPubKey . derivePubPath deriv- pubKeys = map f accountKeys---- Helper function to compute the public key of a given derivation path for--- a given non-multisig account.-getPathPubKey :: Account -> SoftPath -> PubKeyC-getPathPubKey acc@Account{..} deriv- | isMultisigAccount acc = throw $ WalletException $- unwords [ "getPathPubKey: Account", unpack accountName- , "is not a regular non-multisig account"- ]- | otherwise = case accountKeys of- (key:_) -> xPubKey $ derivePubPath deriv key- _ -> throw $ WalletException $ unwords- [ "getPathPubKey: No keys are available in account"- , unpack accountName- ]--{- Helpers -}--subSelectAddrCount :: Entity Account- -> AddressType- -> SqlExpr (Value KeyIndex)-subSelectAddrCount (Entity ai acc) addrType =- sub_select $ from $ \x -> do- where_ ( x ^. WalletAddrAccount ==. val ai- &&. x ^. WalletAddrType ==. val addrType- )- let gap = val $ accountGap acc- return $ case_- [ when_ (countRows >. gap)- then_ (countRows -. gap)- ] (else_ $ val 0)--validMultisigParams :: Int -> Int -> Bool-validMultisigParams m n = n >= 1 && n <= 15 && m >= 1 && m <= n--validAccountType :: AccountType -> Bool-validAccountType t = case t of- AccountRegular -> True- AccountMultisig m n -> validMultisigParams m n--isMultisigAccount :: Account -> Bool-isMultisigAccount acc = case accountType acc of- AccountRegular -> False- AccountMultisig{} -> True--isReadAccount :: Account -> Bool-isReadAccount = isNothing . accountMaster--isCompleteAccount :: Account -> Bool-isCompleteAccount acc = case accountType acc of- AccountRegular -> length (accountKeys acc) == 1- AccountMultisig _ n -> length (accountKeys acc) == n-
@@ -1,59 +0,0 @@-module Network.Haskoin.Wallet.Block where--import Control.Exception (throw)-import Control.Monad.Catch (MonadThrow, throwM)-import Control.Monad.Trans (MonadIO)-import Data.Maybe (fromMaybe)-import Database.Persist.Sql (SqlPersistT)-import Network.Haskoin.Block-import Network.Haskoin.Node.HeaderTree-import Network.Haskoin.Wallet.Model-import Network.Haskoin.Wallet.Transaction-import Network.Haskoin.Wallet.Types--mainChain :: (MonadIO m, MonadThrow m)- => Either BlockHeight BlockHash- -> ListRequest- -> SqlPersistT m (ListResult NodeBlock)-mainChain blockE ListRequest{..} = do- bestHash <- fst <$> walletBestBlock- bestM <- getBlockByHash bestHash- best <- maybe (throwM $ WalletException "Could not find wallet best block")- return bestM- remoteNode <- case blockE of- Right h -> do- remoteNodeM <- getBlockByHash h- maybe (throwM $ WalletException "Colud not get remote node")- return remoteNodeM- Left h -> do- heightNodeM <- getBlockByHeight best h- maybe (throwM $ WalletException "Could not find bock height")- return heightNodeM- frst <- (+1) . nodeBlockHeight <$> splitBlock best remoteNode- if nodeBlockHeight best < frst- then return $ ListResult [] 0- else do- let cnt = nodeBlockHeight best - frst- limit = min listLimit (cnt - listOffset)- offset =- if listReverse- then cnt - listOffset - limit- else listOffset- nodes <- getBlocksFromHeight best limit (frst + offset)- return $ ListResult nodes cnt--blockTxs :: [NodeBlock] -> [WalletTx] -> [(NodeBlock, [WalletTx])]-blockTxs blocks transactions = reverse $ go [] blocks transactions- where- go bs [] _ = bs- go bs (n:ns) [] = go ((n,[]):bs) ns []- go [] (n:ns) xs = go [(n,[])] ns xs- go (b:bs) (n:ns) (x:xs)- | nodeHash (fst b) == blockHashOf x =- go ((fst b, x : snd b) : bs) (n:ns) xs- | nodeHash n == blockHashOf x =- go ((n, [x]) : b : bs) ns xs- | otherwise = go ((n, []) : b : bs) ns (x:xs)- blockHashOf t = fromMaybe- (throw $ WalletException "Unexpected unconfirmed transaction")- (walletTxConfirmedBy t)
@@ -1,252 +0,0 @@-module Network.Haskoin.Wallet.Client (clientMain) where--import System.FilePath ((</>))-import System.Directory (createDirectoryIfMissing)-import System.Posix.Directory (changeWorkingDirectory)-import System.Posix.Files- ( setFileMode- , setFileCreationMask- , unionFileModes- , ownerModes- , groupModes- , otherModes- , fileExist- )-import System.Environment (getArgs, lookupEnv)-import System.Info (os)-import System.Console.GetOpt- ( getOpt- , usageInfo- , OptDescr (Option)- , ArgDescr (NoArg, ReqArg)- , ArgOrder (Permute)- )--import Control.Monad (when, forM_)-import Control.Monad.Trans (liftIO)-import qualified Control.Monad.Reader as R (runReaderT)--import Data.Default (def)-import Data.FileEmbed (embedFile)-import Data.Yaml (decodeFileEither)-import Data.String.Conversions (cs)--import Network.Haskoin.Constants-import Network.Haskoin.Crypto-import Network.Haskoin.Wallet.Settings-import Network.Haskoin.Wallet.Client.Commands-import Network.Haskoin.Wallet.Types--import System.Exit (exitFailure)-import System.IO (hPutStrLn, stderr)-import System.FilePath.Posix (isAbsolute)--usageHeader :: String-usageHeader = "Usage: hw [<options>] <command> [<args>]"--cmdHelp :: [String]-cmdHelp = lines $ cs $ $(embedFile "config/help")--warningMsg :: String-warningMsg = unwords- [ "!!!", "This software is experimental."- , "Use only small amounts of Bitcoins.", "!!!"- ]--usage :: [String]-usage = warningMsg : usageInfo usageHeader options : cmdHelp--read' :: Read x => String -> String -> x-read' e s = case reads s of- [(x, "")] -> x- _ -> error e--options :: [OptDescr (Config -> Config)]-options =- [ Option "c" ["count"]- ( ReqArg- (\s cfg -> cfg { configCount = read' "Could not parse count" s })- "INT"- ) $ "Items per page. Default: " ++ show (configCount def)- , Option "m" ["minconf"]- ( ReqArg- (\s cfg ->- cfg { configMinConf = read' "Colud not parse minconf" s }- ) "INT"- ) $ "Minimum confirmations. Default: "- ++ show (configMinConf def)- , Option "f" ["fee"]- ( ReqArg- (\s cfg -> cfg { configFee = read' "Could not parse fee" s })- "INT"- ) $ "Fee per kilobyte. Default: " ++ show (configFee def)- , Option "R" ["rcptfee"]- (NoArg $ \cfg -> cfg { configRcptFee = True }) $- "Recipient pays fee. Default: " ++ show (configRcptFee def)- , Option "S" ["nosig"]- (NoArg $ \cfg -> cfg { configSignTx = False }) $- "Do not sign. Default: " ++ show (not $ configSignTx def)- , Option "i" ["internal"]- (NoArg $ \cfg -> cfg { configAddrType = AddressInternal }) $- "Internal addresses. Default: "- ++ show (configAddrType def == AddressInternal)- , Option "o" ["offline"]- (NoArg $ \cfg -> cfg { configOffline = True }) $- "Offline balance. Default: " ++ show (configOffline def)- , Option "r" ["revpage"]- (NoArg $ \cfg -> cfg { configReversePaging = True }) $- "Reverse paging. Default: "- ++ show (configReversePaging def)- , Option "p" ["path"]- ( ReqArg ( \s cfg -> case parseHard s of- Just p -> cfg { configPath = Just p }- Nothing -> error "Could not parse derivation path"- ) "PATH"- )- "Derivation path (e.g. m/44'/3')"- , Option "j" ["json"]- (NoArg $ \cfg -> cfg { configFormat = OutputJSON })- "Output JSON"- , Option "y" ["yaml"]- (NoArg $ \cfg -> cfg { configFormat = OutputYAML })- "Output YAML"- , Option "s" ["socket"]- (ReqArg (\s cfg -> cfg { configConnect = s }) "URI") $- "Server socket. Default: " ++ configConnect def- , Option "d" ["detach"]- (NoArg $ \cfg -> cfg { configDetach = True }) $- "Detach server. Default: " ++ show (configDetach def)- , Option "t" ["testnet"]- (NoArg $ \cfg -> cfg { configTestnet = True }) "Testnet3 network"- , Option "g" ["config"]- (ReqArg (\s cfg -> cfg { configFile = s }) "FILE") $- "Config file. Default: " ++ configFile def- , Option "w" ["workdir"]- (ReqArg (\s cfg -> cfg { configDir = s }) "DIR")- "Working directory. OS-dependent default"- , Option "v" ["verbose"]- (NoArg $ \cfg -> cfg { configVerbose = True }) "Verbose output"- ]---- Create and change current working directory-setWorkDir :: Config -> IO ()-setWorkDir cfg = do- let workDir = configDir cfg </> networkName- _ <- setFileCreationMask $ otherModes `unionFileModes` groupModes- createDirectoryIfMissing True workDir- setFileMode workDir ownerModes- changeWorkingDirectory workDir---- Build application configuration-getConfig :: [Config -> Config] -> IO Config-getConfig fs = do- -- Create initial configuration from defaults and command-line arguments- let initCfg = foldr ($) def fs-- -- If working directory set in initial configuration, use it- dir <- case configDir initCfg of "" -> appDir- d -> return d-- -- Make configuration file relative to working directory- let cfgFile = if isAbsolute (configFile initCfg)- then configFile initCfg- else dir </> configFile initCfg-- -- Get configuration from file, if it exists- e <- fileExist cfgFile- if e then do- cfgE <- decodeFileEither cfgFile- case cfgE of- Left x -> error $ show x- -- Override settings from file using command-line- Right cfg -> return $ fixConfigDir (foldr ($) cfg fs) dir- else return $ fixConfigDir initCfg dir- where- -- If working directory not set, use default- fixConfigDir cfg dir = case configDir cfg of "" -> cfg{ configDir = dir }- _ -> cfg---clientMain :: IO ()-clientMain = getArgs >>= \args -> case getOpt Permute options args of- (fs, commands, []) -> do- cfg <- getConfig fs- when (configTestnet cfg) switchToTestnet3- setWorkDir cfg- dispatchCommand cfg commands- (_, _, msgs) -> forM_ (msgs ++ usage) putStrLn--dispatchCommand :: Config -> [String] -> IO ()-dispatchCommand cfg args = flip R.runReaderT cfg $ case args of- "start" : [] -> cmdStart- "stop" : [] -> cmdStop- "newacc" : name : [] -> cmdNewAcc False name []- "newread" : name : [] -> cmdNewAcc True name []- "newms" : name : m : n : [] -> cmdNewAcc False name [m, n]- "newreadms" : name : m : n : [] -> cmdNewAcc True name [m, n]- "addkey" : name : [] -> cmdAddKey name- "setgap" : name : gap : [] -> cmdSetGap name gap- "account" : name : [] -> cmdAccount name- "accounts" : page -> cmdAccounts page- "rename" : name : new : [] -> cmdRenameAcc name new- "list" : name : page -> cmdList name page- "pubkeys" : name : page -> cmdPubKeys name page- "unused" : name : page -> cmdUnused name page- "label" : name : index : label : [] -> cmdLabel name index label- "txs" : name : page -> cmdTxs name page- "addrtxs" : name : index : page -> cmdAddrTxs name index page- "getindex" : name : key : [] -> cmdKeyIndex name key- "genaddrs" : name : i : [] -> cmdGenAddrs name i- "send" : name : add : amnt : [] -> cmdSend name add amnt- "sendmany" : name : xs -> cmdSendMany name xs- "import" : name : [] -> cmdImport name- "sign" : name : txid : [] -> cmdSign name txid- "gettx" : name : txid : [] -> cmdGetTx name txid- "balance" : name : [] -> cmdBalance name- "getoffline" : name : txid : [] -> cmdGetOffline name txid- "signoffline" : name : tx : dat : [] -> cmdSignOffline name tx dat- "rescan" : rescantime -> cmdRescan rescantime- "deletetx" : txid : [] -> cmdDeleteTx txid- "sync" : name : block : page -> cmdSync name block page- "pending" : name : page -> cmdPending name page- "dead" : name : page -> cmdDead name page- "monitor" : name -> cmdMonitor name- "decodetx" : [] -> cmdDecodeTx- "status" : [] -> cmdStatus- "keypair" : [] -> cmdKeyPair- "blockinfo" : hashes -> cmdBlockInfo hashes- "version" : [] -> cmdVersion- "help" : [] -> liftIO $ forM_ usage (hPutStrLn stderr)- [] -> liftIO $ forM_ usage (hPutStrLn stderr)- _ -> liftIO $- forM_ ("Invalid command" : usage) (hPutStrLn stderr) >> exitFailure--appDir :: IO FilePath-appDir = case os of "mingw" -> windows- "mingw32" -> windows- "mingw64" -> windows- "darwin" -> osx- "linux" -> unix- _ -> unix- where- windows = do- localAppData <- lookupEnv "LOCALAPPDATA"- dirM <- case localAppData of- Nothing -> lookupEnv "APPDATA"- Just l -> return $ Just l- case dirM of- Just d -> return $ d </> "Haskoin Wallet"- Nothing -> return "."- osx = do- homeM <- lookupEnv "HOME"- case homeM of- Just home -> return $ home </> "Library"- </> "Application Support"- </> "Haskoin Wallet"- Nothing -> return "."- unix = do- homeM <- lookupEnv "HOME"- case homeM of- Just home -> return $ home </> ".hw"- Nothing -> return "."-
@@ -1,970 +0,0 @@-module Network.Haskoin.Wallet.Client.Commands-( cmdStart-, cmdStop-, cmdNewAcc-, cmdAddKey-, cmdSetGap-, cmdAccount-, cmdRenameAcc-, cmdAccounts-, cmdList-, cmdPubKeys-, cmdUnused-, cmdLabel-, cmdTxs-, cmdAddrTxs-, cmdKeyIndex-, cmdGenAddrs-, cmdSend-, cmdSendMany-, cmdImport-, cmdSign-, cmdBalance-, cmdGetTx-, cmdGetOffline-, cmdSignOffline-, cmdRescan-, cmdDecodeTx-, cmdVersion-, cmdStatus-, cmdBlockInfo-, cmdMonitor-, cmdSync-, cmdKeyPair-, cmdDeleteTx-, cmdPending-, cmdDead-)-where--import Control.Applicative ((<|>))-import Control.Concurrent.Async.Lifted (async, wait)-import Control.Monad (forM_, forever, liftM2,- unless, when)-import qualified Control.Monad.Reader as R (ReaderT, ask, asks)-import Control.Monad.Trans (liftIO)-import Data.Aeson (FromJSON, ToJSON, Value (..),- decode, eitherDecode, object,- toJSON, (.=))-import qualified Data.Aeson as Aeson (encode)-import qualified Data.ByteString.Char8 as B8 (hPutStrLn, putStrLn,- unwords)-import Data.String (fromString)-import Data.List (intercalate, intersperse)-import Data.Maybe (fromMaybe, isJust, isNothing,- listToMaybe, maybeToList)-import Data.Monoid ((<>))-import Data.Restricted (rvalue)-import Data.Serialize (encode)-import Data.String.Conversions (cs)-import Data.Text (Text, pack, splitOn, unpack)-import Data.Word (Word32, Word64)-import qualified Data.Yaml as YAML (encode)-import qualified Data.Time.Format as Time-import Network.Haskoin.Block-import Network.Haskoin.Constants-import Network.Haskoin.Crypto-import Network.Haskoin.Node.STM-import Network.Haskoin.Script-import Network.Haskoin.Transaction-import Network.Haskoin.Util-import Network.Haskoin.Wallet.Server-import Network.Haskoin.Wallet.Settings-import Network.Haskoin.Wallet.Types-import qualified Network.Haskoin.Wallet.Client.PrettyJson as JSON-import qualified System.Console.Haskeline as Haskeline-import System.IO (stderr)-import System.ZMQ4 (KeyFormat (..), Req (..),- Socket, SocketType, Sub (..),- connect, curveKeyPair,- receive, receiveMulti,- restrict, send,- setCurvePublicKey,- setCurveSecretKey,- setCurveServerKey, setLinger,- subscribe, withContext,- withSocket)-import Text.Read (readMaybe)--type Handler = R.ReaderT Config IO--defaultDeriv :: HardPath-defaultDeriv = Deriv :| 0---- hw start [config] [--detach]-cmdStart :: Handler ()-cmdStart = do- cfg <- R.ask- liftIO $ runSPVServer cfg---- hw stop [config]-cmdStop :: Handler ()-cmdStop = do- resE <- sendZmq StopServerR- handleResponse (resE :: Either String (WalletResponse (Maybe ()))) (const $ return ())- liftIO $ putStrLn "Process stopped"--getSigningKeys :: String- -> Handler (Maybe XPrvKey)-getSigningKeys name = do- derivM <- R.asks configPath- kM <- masterKey- case kM of- Just _ -> return Nothing- Nothing -> do- keyOrMnemonic <-- liftIO . Haskeline.runInputT- Haskeline.defaultSettings $- Haskeline.getPassword (Just '*')- "Mnemonic or private extended key: "- case keyOrMnemonic of- Just ms -> return $ go (cs ms) derivM- Nothing -> error "No action due to EOF"- where- masterKey = do- resE <- sendZmq $ GetAccountR $ pack name- case resE of- Right (ResponseValid (Just acc)) ->- return $ jsonAccountMaster acc- Right (ResponseError e) -> error $ cs e- Left e -> error e- _ -> error "You find yourself in a strange place"- go "" _ = error "Need key to sign"- go str derivM = case xPrvImport str of- Just k -> case derivM of- Just d -> Just $ derivePath d k- Nothing -> Just k- Nothing -> case mnemonicToSeed "" str of- Right s -> Just (makeXPrvKey s)- Left _ -> error "Could not parse key"--checkExists :: String -> Handler Bool-checkExists name = do- resE <- sendZmq $ GetAccountR $ pack name- case (resE :: Either String (WalletResponse JsonAccount)) of- Right (ResponseValid _) -> return True- Right (ResponseError _) -> return False- Left e -> error e--getKey :: Handler (Maybe Mnemonic, Maybe XPrvKey, Maybe HardPath, Maybe XPubKey)-getKey = do- derivM <- R.asks configPath- i <- liftIO . Haskeline.runInputT Haskeline.defaultSettings $- Haskeline.getPassword (Just '*')- "Type mnemonic, extended key or leave empty to generate: "- case i of- Just s -> go (cs s) derivM- Nothing -> error "No action due to EOF"- where- go "" derivM = return- ( Nothing- , Nothing- , derivM <|> Just defaultDeriv- , Nothing- )- go str' derivM = case xPrvImport str' of- Just k -> return- ( Nothing- , Just $ maybe k (`derivePath` k) derivM- , derivM- , Nothing- )- Nothing -> case xPubImport str' of- Just p -> return- ( Nothing- , Nothing- , derivM- , Just p- )- Nothing -> return- ( Just $ cs str'- , Nothing- , derivM <|> Just defaultDeriv- , Nothing- )---- First argument: is account read-only?-cmdNewAcc :: Bool -> String -> [String] -> Handler ()-cmdNewAcc r name ls = do- _ <- return $! typ- e <- checkExists name- when e $ error "Account exists"- (mnemonicM, masterM, derivM, keyM) <- getKey- let newAcc = NewAccount- { newAccountName = pack name- , newAccountType = typ- , newAccountMnemonic = cs <$> mnemonicM- , newAccountMaster = masterM- , newAccountDeriv = derivM- , newAccountKeys = maybeToList keyM- , newAccountReadOnly = r- }- resE <- sendZmq $ PostAccountsR newAcc- handleResponse resE $ liftIO . putStr . printAccount- where- typ = case ls of- [] -> AccountRegular- [mS, nS] -> fromMaybe (error "Account information incorrect") $ do- m <- readMaybe mS- n <- readMaybe nS- return $ AccountMultisig m n- _ -> error "Number of parametres incorrect"--cmdAddKey :: String -> Handler ()-cmdAddKey name = do- e <- checkExists name- unless e $ error "Account does not exist"- (mnemonicM, masterM, derivM, pubM) <- getKey- let key = case mnemonicM of- Just ms -> case mnemonicToSeed "" (cs ms) of- Right s -> deriveXPubKey $- derivePath (fromMaybe defaultDeriv derivM) $- makeXPrvKey s- Left _ -> error "Could not decode mnemonic sentence"- Nothing -> case masterM of- Just m -> deriveXPubKey $ maybe m (`derivePath` m) derivM- Nothing -> fromMaybe (error "No keys provided") pubM- resE <- sendZmq (PostAccountKeysR (pack name) [key])- handleResponse resE $ liftIO . putStr . printAccount--cmdSetGap :: String -> String -> Handler ()-cmdSetGap name gap = do- resE <- sendZmq (PostAccountGapR (pack name) setGap)- handleResponse resE $ liftIO . putStr . printAccount- where- setGap = SetAccountGap $ read gap--cmdAccount :: String -> Handler ()-cmdAccount name = do- resE <- sendZmq (GetAccountR $ pack name)- handleResponse resE $ liftIO . putStr . printAccount--cmdAccounts :: [String] -> Handler ()-cmdAccounts ls = do- let page = fromMaybe 1 $ listToMaybe ls >>= readMaybe- listAction page GetAccountsR $ \ts -> do- let xs = map (liftIO . putStr . printAccount) ts- sequence_ $ intersperse (liftIO $ putStrLn "-") xs--cmdRenameAcc :: String -> String -> Handler ()-cmdRenameAcc oldName newName = do- resE <- sendZmq $ PostAccountRenameR (pack oldName) (pack newName)- handleResponse resE $ liftIO . putStr . printAccount--listAction :: (FromJSON a, ToJSON a)- => Word32- -> (ListRequest -> WalletRequest)- -> ([a] -> Handler ())- -> Handler ()-listAction page requestBuilder action = do- c <- R.asks configCount- r <- R.asks configReversePaging- case c of- 0 -> do- let listReq = ListRequest 0 0 r- resE <- sendZmq (requestBuilder listReq)- handleResponse resE $ \(ListResult a _) -> action a- _ -> do- when (page < 1) $ error "Page cannot be less than 1"- let listReq = ListRequest ((page - 1) * c) c r- resE <- sendZmq (requestBuilder listReq)- handleResponse resE $ \(ListResult a m) -> case m of- 0 -> liftIO . putStrLn $ "No elements"- _ -> do- liftIO . putStrLn $- "Page " ++ show page ++ " of " ++ show (pages m c) ++- " (" ++ show m ++ " elements)"- action a- where- pages m c | m `mod` c == 0 = m `div` c- | otherwise = m `div` c + 1--listJsonAddrs :: (JsonAddr -> String)- -> String- -> [String]- -> Handler ()-listJsonAddrs showFunc name ls = do- t <- R.asks configAddrType- m <- R.asks configMinConf- o <- R.asks configOffline- let page = fromMaybe 1 $ listToMaybe ls >>= readMaybe- f = GetAddressesR (pack name) t m o- listAction page f $ \as -> forM_ as (liftIO . putStrLn . showFunc)--cmdList :: String -> [String] -> Handler ()-cmdList = listJsonAddrs printAddress--cmdPubKeys :: String -> [String] -> Handler ()-cmdPubKeys = listJsonAddrs printPubKey--cmdUnused :: String -> [String] -> Handler ()-cmdUnused name ls = do- t <- R.asks configAddrType- let page = fromMaybe 1 $ listToMaybe ls >>= readMaybe- f = GetAddressesUnusedR (pack name) t- listAction page f $ \as -> forM_ (as :: [JsonAddr]) $- liftIO . putStrLn . printAddress--cmdLabel :: String -> String -> String -> Handler ()-cmdLabel name iStr label = do- t <- R.asks configAddrType- resE <- sendZmq (PutAddressR (pack name) i t addrLabel)- handleResponse resE $ liftIO . putStrLn . printAddress- where- i = read iStr- addrLabel = AddressLabel $ pack label--cmdTxs :: String -> [String] -> Handler ()-cmdTxs name ls = do- let page = fromMaybe 1 $ listToMaybe ls >>= readMaybe- r <- R.asks configReversePaging- listAction page (GetTxsR (pack name)) $ \ts -> do- let xs = map (liftIO . putStr . printTx Nothing) ts- xs' = if r then xs else reverse xs- sequence_ $ intersperse (liftIO $ putStrLn "-") xs'--cmdPending :: String -> [String] -> Handler ()-cmdPending name ls = do- let page = fromMaybe 1 $ listToMaybe ls >>= readMaybe- r <- R.asks configReversePaging- listAction page (GetPendingR (pack name)) $ \ts -> do- let xs = map (liftIO . putStr . printTx Nothing) ts- xs' = if r then xs else reverse xs- sequence_ $ intersperse (liftIO $ putStrLn "-") xs'--cmdDead :: String -> [String] -> Handler ()-cmdDead name ls = do- let page = fromMaybe 1 $ listToMaybe ls >>= readMaybe- r <- R.asks configReversePaging- listAction page (GetDeadR (pack name)) $ \ts -> do- let xs = map (liftIO . putStr . printTx Nothing) ts- xs' = if r then xs else reverse xs- sequence_ $ intersperse (liftIO $ putStrLn "-") xs'--cmdAddrTxs :: String -> String -> [String] -> Handler ()-cmdAddrTxs name i ls = do- t <- R.asks configAddrType- m <- R.asks configMinConf- o <- R.asks configOffline- r <- R.asks configReversePaging- let page = fromMaybe 1 $ listToMaybe ls >>= readMaybe- f = GetAddrTxsR (pack name) index t- resE <- sendZmq (GetAddressR (pack name) index t m o)- handleResponse resE $ \JsonAddr{..} -> listAction page f $ \ts -> do- let xs = map (liftIO . putStr . printTx (Just jsonAddrAddress)) ts- xs' = if r then xs else reverse xs- sequence_ $ intersperse (liftIO $ putStrLn "-") xs'- where- index = fromMaybe (error "Could not read index") $ readMaybe i--cmdKeyIndex :: String -> String -> Handler ()-cmdKeyIndex name k = do- t <- R.asks configAddrType- resE <- sendZmq $ GetIndexR (pack name) (fromString k) t- handleResponse resE $ \res -> case res of- [] -> liftIO $ putStrLn "No matching pubkeys found"- lst -> liftIO $ putStrLn $ unlines $ -- Two or more pubkeys with the same index is extremely improbable. But let's print it out if it happens.- map (\adr -> showLine (jsonAddrIndex adr, jsonAddrKey adr)) lst- where- showLine :: (KeyIndex, Maybe PubKeyC) -> String- showLine (idx,k') = unwords [ show (idx :: KeyIndex), ":", showPubKey k' ]- showPubKey = maybe "<no pubkey available>" (jsonStr2Str . toJSON)- jsonStr2Str (String t) = cs t- jsonStr2Str _ = ""--cmdGenAddrs :: String -> String -> Handler ()-cmdGenAddrs name i = do- t <- R.asks configAddrType- let req = PostAddressesR (pack name) index t- resE <- sendZmq req- handleResponse resE $ \cnt -> liftIO . putStrLn $- unwords [ "Generated", show (cnt :: Int), "addresses" ]- where- index = read i--cmdSend :: String -> String -> String -> Handler ()-cmdSend name addrStr amntStr = cmdSendMany name [addrStr ++ ":" ++ amntStr]--cmdSendMany :: String -> [String] -> Handler ()-cmdSendMany name xs = case rcpsM of- Just rcps -> do- fee <- R.asks configFee- rcptFee <- R.asks configRcptFee- minconf <- R.asks configMinConf- sign <- R.asks configSignTx- masterM <- if sign then getSigningKeys name else return Nothing- let action = CreateTx rcps fee minconf rcptFee sign- resE <- sendZmq (PostTxsR (pack name) masterM action)- handleResponse resE $ liftIO . putStr . printTx Nothing- _ -> error "Could not parse recipient information"- where- g str = map cs $ splitOn ":" (pack str)- f [a,v] = liftM2 (,) (base58ToAddr a) (readMaybe $ cs v)- f _ = Nothing- rcpsM = mapM (f . g) xs--getHexTx :: Handler Tx-getHexTx = do- hexM <- Haskeline.runInputT Haskeline.defaultSettings $- Haskeline.getInputLine ""- let txM = case hexM of- Nothing -> error "No action due to EOF"- Just hex -> decodeToMaybe =<< decodeHex (cs hex)- case txM of- Just tx -> return tx- Nothing -> error "Could not parse transaction"--cmdImport :: String -> Handler ()-cmdImport name = do- tx <- getHexTx- let action = ImportTx tx- resE <- sendZmq (PostTxsR (pack name) Nothing action)- handleResponse resE $ liftIO . putStr . printTx Nothing--cmdSign :: String -> String -> Handler ()-cmdSign name txidStr = case txidM of- Just txid -> do- masterM <- getSigningKeys name- let action = SignTx txid- resE <- sendZmq (PostTxsR (pack name) masterM action)- handleResponse resE $ liftIO . putStr . printTx Nothing- _ -> error "Could not parse txid"- where- txidM = hexToTxHash $ cs txidStr--cmdGetOffline :: String -> String -> Handler ()-cmdGetOffline name tidStr = case tidM of- Just tid -> do- resE <- sendZmq (GetOfflineTxR (pack name) tid)- handleResponse resE $ \(OfflineTxData tx dat) -> do- liftIO $ putStrLn $ unwords- [ "Tx :", cs $ encodeHex $ encode tx ]- liftIO $ putStrLn $ unwords- [ "CoinData:", cs $ encodeHex $ cs $ Aeson.encode dat ]- _ -> error "Could not parse txid"- where- tidM = hexToTxHash $ cs tidStr--cmdSignOffline :: String -> String -> String -> Handler ()-cmdSignOffline name txStr datStr = case (txM, datM) of- (Just tx, Just dat) -> do- masterM <- getSigningKeys name- resE <- sendZmq (PostOfflineTxR (pack name) masterM tx dat)- handleResponse resE $ \(TxCompleteRes tx' c) -> do- liftIO $ putStrLn $ unwords- [ "Tx :", cs $ encodeHex $ encode tx' ]- liftIO $ putStrLn $ unwords- [ "Complete:", if c then "Yes" else "No" ]- _ -> error "Could not decode input data"- where- datM = decode . cs =<< decodeHex (cs datStr)- txM = decodeToMaybe =<< decodeHex (cs txStr)--cmdBalance :: String -> Handler ()-cmdBalance name = do- m <- R.asks configMinConf- o <- R.asks configOffline- resE <- sendZmq (GetBalanceR (pack name) m o)- handleResponse resE $ \bal ->- liftIO $ putStrLn $ unwords [ "Balance:", show (bal :: Word64) ]--cmdGetTx :: String -> String -> Handler ()-cmdGetTx name tidStr = case tidM of- Just tid -> do- resE <- sendZmq (GetTxR (pack name) tid)- handleResponse resE $ liftIO . putStr . printTx Nothing- _ -> error "Could not parse txid"- where- tidM = hexToTxHash $ cs tidStr--cmdRescan :: [String] -> Handler ()-cmdRescan timeLs = do- let timeM = case timeLs of- [] -> Nothing- str:_ -> case readMaybe str of- Nothing -> error "Could not decode time"- Just t -> Just t- resE <- sendZmq (PostNodeR $ NodeActionRescan timeM)- handleResponse resE $ \(RescanRes ts) ->- liftIO $ putStrLn $ unwords [ "Timestamp:", show ts]--cmdDeleteTx :: String -> Handler ()-cmdDeleteTx tidStr = case tidM of- Just tid -> do- resE <- sendZmq (DeleteTxIdR tid)- handleResponse resE $ \() -> return ()- Nothing -> error "Could not parse txid"- where- tidM = hexToTxHash $ cs tidStr--cmdMonitor :: [String] -> Handler ()-cmdMonitor ls = do- cfg@Config{..} <- R.ask- -- TODO: I can do this in the same thread without ^C twice (see sendZmq)- liftIO $ withContext $ \ctx -> withSocket ctx Sub $ \sock -> do- setLinger (restrict (0 :: Int)) sock- setupAuth cfg sock- connect sock configConnectNotif- subscribe sock "[block]"- forM_ ls $ \name -> subscribe sock $ "{" <> cs name <> "}"- forever $ do- [_,m] <- receiveMulti sock- handleNotif configFormat $ eitherDecode $ cs m--cmdSync :: String -> String -> [String] -> Handler ()-cmdSync acc block ls = do- let page = fromMaybe 1 $ listToMaybe ls >>= readMaybe- f = case length block of- 64 -> GetSyncR (cs acc) $- fromMaybe (error "Could not decode block id") $- hexToBlockHash $ cs block- _ -> GetSyncHeightR (cs acc) $- fromMaybe (error "Could not decode block height") $- readMaybe block- r <- R.asks configReversePaging- listAction page f $ \blocks -> do- let blocks' = if r then reverse blocks else blocks- forM_ (blocks' :: [JsonSyncBlock]) $ liftIO . putStrLn . printSyncBlock--cmdDecodeTx :: Handler ()-cmdDecodeTx = do- tx <- getHexTx- format <- R.asks configFormat- liftIO $ formatStr $ cs $ case format of- OutputJSON -> cs $ jsn tx- _ -> YAML.encode $ val tx- where- val = encodeTxJSON- jsn = JSON.encodePretty . val--cmdVersion :: Handler ()-cmdVersion = liftIO $ do- putStrLn $ unwords [ "network :", cs networkName ]- putStrLn $ unwords [ "user-agent:", cs haskoinUserAgent ]--cmdStatus :: Handler ()-cmdStatus = do- v <- R.asks configVerbose- resE <- sendZmq (PostNodeR NodeActionStatus)- handleResponse resE $ mapM_ (liftIO . putStrLn) . printNodeStatus v--cmdKeyPair :: Handler ()-cmdKeyPair = do- (pub, sec) <- curveKeyPair- liftIO $ do- B8.putStrLn $ B8.unwords [ "public :", rvalue pub ]- B8.putStrLn $ B8.unwords [ "private:", rvalue sec ]--cmdBlockInfo :: [String] -> Handler ()-cmdBlockInfo headers = do- -- Show best block if no arguments are provided- hashL <- if null headers then- -- Fetch best block hash from status msg, and return as list- (: []) . parseRes <$> sendZmq (PostNodeR NodeActionStatus)- else- return (map fromString headers)- sendZmq (GetBlockInfoR hashL) >>=- \resE -> handleResponse resE (liftIO . printResults)- where- printResults :: [BlockInfo] -> IO ()- printResults = mapM_ $ putStrLn . unlines . printBlockInfo- parseRes :: Either String (WalletResponse NodeStatus) -> BlockHash- parseRes = nodeStatusBestHeader . fromMaybe- (error "No response to NodeActionStatus msg") . parseResponse---{- Helpers -}--handleNotif :: OutputFormat -> Either String Notif -> IO ()-handleNotif _ (Left e) = error e-handleNotif fmt (Right notif) = case fmt of- OutputJSON -> formatStr $ cs $- JSON.encodePretty notif- OutputYAML -> do- putStrLn "---"- formatStr $ cs $ YAML.encode notif- putStrLn "..."- OutputNormal ->- putStrLn $ printNotif notif--parseResponse- :: Either String (WalletResponse a)- -> Maybe a-parseResponse resE = case resE of- Right (ResponseValid resM) -> resM- Right (ResponseError err) -> error $ unpack err- Left err -> error err--handleResponse- :: (FromJSON a, ToJSON a)- => Either String (WalletResponse a)- -> (a -> Handler ())- -> Handler ()-handleResponse resE handle = case parseResponse resE of- Just a -> formatOutput a =<< R.asks configFormat- Nothing -> return ()- where- formatOutput a format = case format of- OutputJSON -> liftIO . formatStr $ cs $- JSON.encodePretty a- OutputYAML -> liftIO . formatStr $ cs $ YAML.encode a- OutputNormal -> handle a--sendZmq :: (FromJSON a, ToJSON a)- => WalletRequest- -> Handler (Either String (WalletResponse a))-sendZmq req = do- cfg <- R.ask- let msg = cs $ Aeson.encode req- when (configVerbose cfg) $ liftIO $- B8.hPutStrLn stderr $ "Outgoing JSON: " `mappend` msg- -- TODO: If I do this in the same thread I have to ^C twice to exit- a <- async $ liftIO $ withContext $ \ctx ->- withSocket ctx Req $ \sock -> do- setLinger (restrict (0 :: Int)) sock- setupAuth cfg sock- connect sock (configConnect cfg)- send sock [] (cs $ Aeson.encode req)- eitherDecode . cs <$> receive sock- wait a--setupAuth :: (SocketType t)- => Config- -> Socket t- -> IO ()-setupAuth cfg sock = do- let clientKeyM = configClientKey cfg- clientKeyPubM = configClientKeyPub cfg- serverKeyPubM = configServerKeyPub cfg- forM_ clientKeyM $ \clientKey -> do- let serverKeyPub = fromMaybe- (error "Server public key not provided")- serverKeyPubM- clientKeyPub = fromMaybe- (error "Client public key not provided")- clientKeyPubM- setCurveServerKey TextFormat serverKeyPub sock- setCurvePublicKey TextFormat clientKeyPub sock- setCurveSecretKey TextFormat clientKey sock--formatStr :: String -> IO ()-formatStr str = forM_ (lines str) putStrLn--encodeTxJSON :: Tx -> Value-encodeTxJSON tx = object- [ "txid" .= (cs $ txHashToHex (txHash tx) :: Text)- , "version" .= txVersion tx- , "inputs" .= map encodeTxInJSON (txIn tx)- , "outputs" .= map encodeTxOutJSON (txOut tx)- , "locktime" .= txLockTime tx- ]--encodeTxInJSON :: TxIn -> Value-encodeTxInJSON (TxIn o s i) = object $- [ "outpoint" .= encodeOutPointJSON o- , "sequence" .= i- , "raw-script" .= (cs $ encodeHex s :: Text)- , "script" .= encodeScriptJSON sp- ] ++ decoded- where- sp = fromMaybe (Script []) $ decodeToMaybe s- decoded = either (const []) f $ decodeInputBS s- f inp = ["decoded-script" .= encodeScriptInputJSON inp]--encodeTxOutJSON :: TxOut -> Value-encodeTxOutJSON (TxOut v s) = object $- [ "value" .= v- , "raw-script" .= (cs $ encodeHex s :: Text)- , "script" .= encodeScriptJSON sp- ] ++ decoded- where- sp = fromMaybe (Script []) $ decodeToMaybe s- decoded = either (const [])- (\out -> ["decoded-script" .= encodeScriptOutputJSON out])- (decodeOutputBS s)--encodeOutPointJSON :: OutPoint -> Value-encodeOutPointJSON (OutPoint h i) = object- [ "txid" .= (cs $ txHashToHex h :: Text)- , "pos" .= i- ]--encodeScriptJSON :: Script -> Value-encodeScriptJSON (Script ops) =- toJSON $ map f ops- where- f (OP_PUSHDATA bs _) = String $ pack $ unwords- ["OP_PUSHDATA", cs $ encodeHex bs]- f x = String $ pack $ show x--encodeScriptInputJSON :: ScriptInput -> Value-encodeScriptInputJSON si = case si of- RegularInput (SpendPK s) -> object- [ "spendpubkey" .= object [ "sig" .= encodeSigJSON s ] ]- RegularInput (SpendPKHash s p) -> object- [ "spendpubkeyhash" .= object- [ "sig" .= encodeSigJSON s- , "pubkey" .= (cs $ encodeHex (encode p) :: Text)- , "sender-address" .= (cs $ addrToBase58 (pubKeyAddr p) :: Text)- ]- ]- RegularInput (SpendMulSig sigs) -> object- [ "spendmulsig" .= object [ "sigs" .= map encodeSigJSON sigs ] ]- ScriptHashInput s r -> object- [ "spendscripthash" .= object- [ "scriptinput" .= encodeScriptInputJSON (RegularInput s)- , "redeem" .= encodeScriptOutputJSON r- , "raw-redeem" .= (cs $ encodeHex (encodeOutputBS r) :: Text)- , "sender-address" .= (cs $ addrToBase58 (scriptAddr r) :: Text)- ]- ]--encodeScriptOutputJSON :: ScriptOutput -> Value-encodeScriptOutputJSON so = case so of- PayPK p -> object- [ "pay2pubkey" .= object- [ "pubkey" .= (cs $ encodeHex (encode p) :: Text) ]- ]- PayPKHash a -> object- [ "pay2pubkeyhash" .= object- [ "address-base64" .=- (cs $ encodeHex (encode $ getAddrHash a) :: Text)- , "address-base58" .= (cs $ addrToBase58 a :: Text)- ]- ]- PayMulSig ks r -> object- [ "pay2mulsig" .= object- [ "required-keys" .= r- , "pubkeys" .= (map (cs . encodeHex . encode) ks :: [Text])- ]- ]- PayScriptHash a -> object- [ "pay2scripthash" .= object- [ "address-base64" .= (cs $ encodeHex $ encode $ getAddrHash a :: Text)- , "address-base58" .= (cs (addrToBase58 a) :: Text)- ]- ]- DataCarrier bs -> object- [ "op_return" .= object- [ "data" .= (cs $ encodeHex bs :: Text)- ]- ]--encodeSigJSON :: TxSignature -> Value-encodeSigJSON ts@(TxSignature _ sh) = object- [ "raw-sig" .= (cs $ encodeHex (encodeSig ts) :: Text)- , "sighash" .= encodeSigHashJSON sh- ]--encodeSigHashJSON :: SigHash -> Value-encodeSigHashJSON sh = case sh of- SigAll acp -> object- [ "type" .= String "SigAll"- , "acp" .= acp- ]- SigNone acp -> object- [ "type" .= String "SigNone"- , "acp" .= acp- ]- SigSingle acp -> object- [ "type" .= String "SigSingle"- , "acp" .= acp- ]- SigUnknown acp v -> object- [ "type" .= String "SigUnknown"- , "acp" .= acp- , "value" .= v- ]--{- Print utilities -}--printAccount :: JsonAccount -> String-printAccount JsonAccount{..} = unlines $- [ "Account : " ++ unpack jsonAccountName- , "Type : " ++ showType- , "Gap : " ++ show jsonAccountGap- ]- ++- [ "Deriv : " ++ pathToStr d- | d <- maybeToList jsonAccountDerivation- ]- ++- [ "Mnemonic: " ++ cs ms- | ms <- maybeToList jsonAccountMnemonic- ]- ++- concat [ printKeys | not (null jsonAccountKeys) ]- where- printKeys =- ("Keys : " ++ cs (xPubExport (head jsonAccountKeys))) :- map ((" " ++) . cs . xPubExport) (tail jsonAccountKeys)- showType = case jsonAccountType of- AccountRegular -> if isNothing jsonAccountMaster- then "Read-Only" else "Regular"- AccountMultisig m n -> unwords- [ if isNothing jsonAccountMaster- then "Read-Only Multisig" else "Multisig"- , show m, "of", show n- ]--printAddress :: JsonAddr -> String-printAddress JsonAddr{..} = unwords $- [ show jsonAddrIndex, ":", cs (addrToBase58 jsonAddrAddress) ]- ++- [ "(" ++ unpack jsonAddrLabel ++ ")" | not (null $ unpack jsonAddrLabel) ]- ++ concat- [ [ "[Received: " ++ show (balanceInfoInBalance bal) ++ "]"- , "[Coins: " ++ show (balanceInfoCoins bal) ++ "]"- , "[Spent Coins: " ++ show (balanceInfoSpentCoins bal) ++ "]"- ]- | isJust jsonAddrBalance && balanceInfoCoins bal > 0- ]- where- bal = fromMaybe (error "Could not get address balance") jsonAddrBalance--printPubKey :: JsonAddr -> String-printPubKey JsonAddr{..} = unwords $- [ show jsonAddrIndex, ":", showPubKey jsonAddrKey ]- ++- [ "(" ++ unpack jsonAddrLabel ++ ")" | not (null $ unpack jsonAddrLabel) ]- where- showPubKey = maybe "<no pubkey available>" (jsonStr2Str . toJSON)- jsonStr2Str (String t) = cs t- jsonStr2Str _ = "" -- It totally is a String though--printNotif :: Notif -> String-printNotif (NotifTx tx) = printTx Nothing tx-printNotif (NotifBlock b) = printBlock b--printTx :: Maybe Address -> JsonTx -> String-printTx aM tx@JsonTx{..} = unlines $- [ "Id : " ++ cs (txHashToHex jsonTxHash) ]- ++- [ "Value : " ++ printTxType jsonTxType ++ " " ++ show jsonTxValue ]- ++- [ "Confidence : " ++ printTxConfidence tx ]- ++ concat- [ printAddrInfos "Inputs : " jsonTxInputs- | not (null jsonTxInputs)- ]- ++ concat- [ printAddrInfos "Outputs : " jsonTxOutputs- | not (null jsonTxOutputs)- ]- ++ concat- [ printAddrInfos "Change : " jsonTxChange- | not (null jsonTxChange)- ]- where- printAddrInfos header xs =- (header ++ f (head xs)) :- map ((" " ++) . f) (tail xs)- f (AddressInfo addr valM local) = unwords $- cs (addrToBase58 addr) :- [ show v | v <- maybeToList valM ]- ++- [ "<-" | maybe local (== addr) aM ]--printTxConfidence :: JsonTx -> String-printTxConfidence JsonTx{..} = case jsonTxConfidence of- TxBuilding -> "Building" ++ confirmations- TxPending -> "Pending" ++ confirmations- TxDead -> "Dead" ++ confirmations- TxOffline -> "Offline"- where- confirmations = case jsonTxConfirmations of- Just conf -> " (Confirmations: " ++ show conf ++ ")"- _ -> ""--printTxType :: TxType -> String-printTxType t = case t of- TxIncoming -> "Incoming"- TxOutgoing -> "Outgoing"- TxSelf -> "Self"--printBlock :: JsonBlock -> String-printBlock JsonBlock{..} = unlines- [ "Block Hash : " ++ cs (blockHashToHex jsonBlockHash)- , "Block Height : " ++ show jsonBlockHeight- , "Previous block : " ++ cs (blockHashToHex jsonBlockPrev)- ]--printSyncBlock :: JsonSyncBlock -> String-printSyncBlock JsonSyncBlock{..} = unlines- [ "Block Hash : " ++ cs (blockHashToHex jsonSyncBlockHash)- , "Block Height : " ++ show jsonSyncBlockHeight- , "Previous block : " ++ cs (blockHashToHex jsonSyncBlockPrev)- , "Transactions : " ++ show (length jsonSyncBlockTxs)- ]--printNodeStatus :: Bool -> NodeStatus -> [String]-printNodeStatus verbose NodeStatus{..} =- [ "Network Height : " ++ show nodeStatusNetworkHeight- , "Best Header : " ++ cs (blockHashToHex nodeStatusBestHeader)- , "Best Header Height: " ++ show nodeStatusBestHeaderHeight- , "Best Block : " ++ cs (blockHashToHex nodeStatusBestBlock)- , "Best Block Height : " ++ show nodeStatusBestBlockHeight- , "Bloom Filter Size : " ++ show nodeStatusBloomSize- ] ++- [ "Header Peer : " ++ show h- | h <- maybeToList nodeStatusHeaderPeer, verbose- ] ++- [ "Merkle Peer : " ++ show m- | m <- maybeToList nodeStatusMerklePeer, verbose- ] ++- [ "Pending Headers : " ++ show nodeStatusHaveHeaders | verbose ] ++- [ "Pending Tickles : " ++ show nodeStatusHaveTickles | verbose ] ++- [ "Pending Txs : " ++ show nodeStatusHaveTxs | verbose ] ++- [ "Pending GetData : " ++ show (map txHashToHex nodeStatusGetData)- | verbose- ] ++- [ "Pending Rescan : " ++ show r- | r <- maybeToList nodeStatusRescan, verbose- ] ++- [ "Synced Mempool : " ++ show nodeStatusMempool | verbose ] ++- [ "HeaderSync Lock : " ++ show nodeStatusSyncLock | verbose ] ++- [ "Peers: " ] ++- intercalate ["-"] (map (printPeerStatus verbose) nodeStatusPeers)--printPeerStatus :: Bool -> PeerStatus -> [String]-printPeerStatus verbose PeerStatus{..} =- [ " Peer Id : " ++ show peerStatusPeerId- , " Peer Host: " ++ peerHostString peerStatusHost- , " Connected: " ++ if peerStatusConnected then "yes" else "no"- , " Height : " ++ show peerStatusHeight- ] ++- [ " Protocol : " ++ show p | p <- maybeToList peerStatusProtocol- ] ++- [ " UserAgent: " ++ ua | ua <- maybeToList peerStatusUserAgent- ] ++- [ " Avg Ping : " ++ p | p <- maybeToList peerStatusPing- ] ++- [ " DoS Score: " ++ show d | d <- maybeToList peerStatusDoSScore- ] ++- [ " Merkles : " ++ show peerStatusHaveMerkles | verbose ] ++- [ " Messages : " ++ show peerStatusHaveMessage | verbose ] ++- [ " Nonces : " ++ show peerStatusPingNonces | verbose ] ++- [ " Reconnect: " ++ show t- | t <- maybeToList peerStatusReconnectTimer, verbose- ] ++- [ " Logs : " | verbose ] ++- [ " - " ++ msg | msg <- fromMaybe [] peerStatusLog, verbose]--printBlockInfo :: BlockInfo -> [String]-printBlockInfo BlockInfo{..} =- [ "Block Height : " ++ show blockInfoHeight- , "Block Hash : " ++ cs (blockHashToHex blockInfoHash)- , "Block Timestamp : " ++ formatUTCTime blockInfoTimestamp- , "Previous Block : " ++ cs (blockHashToHex blockInfoPrevBlock)- , "Merkle Root : " ++ cs blockInfoMerkleRoot- , "Block Version : " ++ "0x" ++ cs (encodeHex versionData)- , "Block Difficulty : " ++ show (blockDiff blockInfoBits)- , "Chain Work : " ++ show blockInfoChainWork- ]- where- blockDiff :: Word32 -> Double- blockDiff target = getTarget (blockBits genesisHeader) / getTarget target- getTarget = fromIntegral . decodeCompact- versionData = integerToBS (fromIntegral blockInfoVersion)- formatUTCTime = Time.formatTime Time.defaultTimeLocale- "%Y-%m-%d %H:%M:%S (UTC)"
@@ -1,23 +0,0 @@-{-# LANGUAGE CPP #-}-module Network.Haskoin.Wallet.Client.PrettyJson-( encodePretty )-where--import qualified Data.ByteString.Lazy as BL-import Data.Aeson (ToJSON)-import Data.Aeson.Encode.Pretty as Export (Config (..),- defConfig,- encodePretty')---- aeson-pretty 0.8.0 introduces a new way to specify indentation-#if MIN_VERSION_aeson_pretty(0,8,0)-import Data.Aeson.Encode.Pretty as Export (Indent(..))-jsonIndent :: Indent-jsonIndent = Spaces 2-#else-jsonIndent :: Int-jsonIndent = 2-#endif--encodePretty :: ToJSON a => a -> BL.ByteString-encodePretty = encodePretty' defConfig{ confIndent = jsonIndent }
@@ -1,17 +0,0 @@-module Network.Haskoin.Wallet.Database where--import Control.Monad.Logger (MonadLoggerIO)-import Control.Monad.Trans.Control (MonadBaseControl)--import Database.Persist.Sql (ConnectionPool)-import Database.Persist.Sqlite (SqliteConf(..), createSqlitePool)--type DatabaseConfType = SqliteConf--getDatabasePool :: (MonadLoggerIO m, MonadBaseControl IO m)- => DatabaseConfType -> m ConnectionPool-getDatabasePool conf = createSqlitePool (sqlDatabase conf) (sqlPoolSize conf)--paramLimit :: Int-paramLimit = 20-
@@ -1,31 +0,0 @@-{-|-This module expose haskoin-wallet internals. No guarantee is made on the-stability of the interface of these internal modules.--}--module Network.Haskoin.Wallet.Internals-( module Network.Haskoin.Wallet-, module Network.Haskoin.Wallet.Accounts-, module Network.Haskoin.Wallet.Transaction-, module Network.Haskoin.Wallet.Client-, module Network.Haskoin.Wallet.Client.Commands-, module Network.Haskoin.Wallet.Server-, module Network.Haskoin.Wallet.Server.Handler-, module Network.Haskoin.Wallet.Settings-, module Network.Haskoin.Wallet.Database-, module Network.Haskoin.Wallet.Types-, module Network.Haskoin.Wallet.Model-) where--import Network.Haskoin.Wallet-import Network.Haskoin.Wallet.Accounts-import Network.Haskoin.Wallet.Transaction-import Network.Haskoin.Wallet.Client-import Network.Haskoin.Wallet.Client.Commands-import Network.Haskoin.Wallet.Server-import Network.Haskoin.Wallet.Server.Handler-import Network.Haskoin.Wallet.Settings-import Network.Haskoin.Wallet.Database-import Network.Haskoin.Wallet.Types-import Network.Haskoin.Wallet.Model-
@@ -1,134 +0,0 @@-module Network.Haskoin.Wallet.Model-( -- Database types- Account(..)-, AccountId-, WalletAddr(..)-, WalletAddrId-, WalletState(..)-, WalletStateId-, WalletCoin(..)-, WalletCoinId-, SpentCoin(..)-, SpentCoinId-, WalletTx(..)-, WalletTxId-, EntityField(..)-, Unique(..)-, migrateWallet---- JSON conversion-, toJsonAccount-, toJsonAddr-, toJsonCoin-, toJsonTx--) where--import Data.Word (Word32, Word64)-import Data.Time (UTCTime)-import Data.Text (Text)-import Data.String.Conversions (cs)--import Database.Persist (EntityField, Unique)-import Database.Persist.Quasi (lowerCaseSettings)-import Database.Persist.TH- ( share- , mkPersist- , sqlSettings- , mkMigrate- , persistFileWith- )--import Network.Haskoin.Wallet.Types-import Network.Haskoin.Block-import Network.Haskoin.Transaction-import Network.Haskoin.Script-import Network.Haskoin.Crypto-import Network.Haskoin.Node-import Network.Haskoin.Node.HeaderTree--share [ mkPersist sqlSettings- , mkMigrate "migrateWallet"- ]- $(persistFileWith lowerCaseSettings "config/models")--{- JSON Types -}--toJsonAccount :: Maybe Mnemonic -> Account -> JsonAccount-toJsonAccount msM acc = JsonAccount- { jsonAccountName = accountName acc- , jsonAccountType = accountType acc- , jsonAccountMnemonic = fmap cs msM- , jsonAccountMaster = accountMaster acc- , jsonAccountDerivation = accountDerivation acc- , jsonAccountKeys = accountKeys acc- , jsonAccountGap = accountGap acc- , jsonAccountCreated = accountCreated acc- }--toJsonAddr :: WalletAddr- -> Maybe BalanceInfo- -> JsonAddr-toJsonAddr addr balM = JsonAddr- { jsonAddrAddress = walletAddrAddress addr- , jsonAddrIndex = walletAddrIndex addr- , jsonAddrType = walletAddrType addr- , jsonAddrLabel = walletAddrLabel addr- , jsonAddrRedeem = walletAddrRedeem addr- , jsonAddrKey = walletAddrKey addr- , jsonAddrCreated = walletAddrCreated addr- , jsonAddrBalance = balM- }--toJsonTx :: AccountName- -> Maybe (BlockHash, BlockHeight) -- ^ Current best block- -> WalletTx- -> JsonTx-toJsonTx acc bbM tx = JsonTx- { jsonTxHash = walletTxHash tx- , jsonTxNosigHash = walletTxNosigHash tx- , jsonTxType = walletTxType tx- , jsonTxInValue = walletTxInValue tx- , jsonTxOutValue = walletTxOutValue tx- , jsonTxValue = fromIntegral (walletTxInValue tx) -- fromIntegral (walletTxOutValue tx)- , jsonTxInputs = walletTxInputs tx- , jsonTxOutputs = walletTxOutputs tx- , jsonTxChange = walletTxChange tx- , jsonTxTx = walletTxTx tx- , jsonTxIsCoinbase = walletTxIsCoinbase tx- , jsonTxConfidence = walletTxConfidence tx- , jsonTxConfirmedBy = walletTxConfirmedBy tx- , jsonTxConfirmedHeight = walletTxConfirmedHeight tx- , jsonTxConfirmedDate = walletTxConfirmedDate tx- , jsonTxCreated = walletTxCreated tx- , jsonTxAccount = acc- , jsonTxConfirmations = f =<< walletTxConfirmedHeight tx- , jsonTxBestBlock = fst <$> bbM- , jsonTxBestBlockHeight = snd <$> bbM- }- where- f confirmedHeight = case bbM of- Just (_, h) -> return $ fromInteger $- max 0 $ toInteger h - toInteger confirmedHeight + 1- _ -> Nothing--toJsonCoin :: WalletCoin- -> Maybe JsonTx -- ^ Coin’s transaction- -> Maybe JsonAddr -- ^ Coin’s address- -> Maybe JsonTx -- ^ Coin’s spending transaction- -> JsonCoin-toJsonCoin coin txM addrM spendM = JsonCoin- { jsonCoinHash = walletCoinHash coin- , jsonCoinPos = walletCoinPos coin- , jsonCoinValue = walletCoinValue coin- , jsonCoinScript = walletCoinScript coin- , jsonCoinCreated = walletCoinCreated coin- -- Optional tx- , jsonCoinTx = txM- -- Optional address- , jsonCoinAddress = addrM- -- Optional spending tx- , jsonCoinSpendingTx = spendM- }-
@@ -1,510 +0,0 @@-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE TemplateHaskell #-}-module Network.Haskoin.Wallet.Server-( runSPVServer-, stopSPVServer-, runSPVServerWithContext-) where--import Control.Concurrent.Async.Lifted (async, link,- waitAnyCancel)-import Control.Concurrent.STM (atomically, retry)-import Control.Concurrent.STM.TBMChan (TBMChan, newTBMChan,- readTBMChan)-import Control.DeepSeq (NFData (..))-import Control.Exception.Lifted (ErrorCall (..),- SomeException (..),- catches)-import qualified Control.Exception.Lifted as E (Handler (..))-import Control.Monad (forM_, forever, unless,- void, when)-import Control.Monad.Base (MonadBase)-import Control.Monad.Catch (MonadThrow)-import Control.Monad.Fix (fix)-import Control.Monad.Logger (MonadLoggerIO,- filterLogger, logDebug,- logError, logInfo,- logWarn,- runStdoutLoggingT)-import Control.Monad.Trans (lift, liftIO)-import Control.Monad.Trans.Control (MonadBaseControl,- liftBaseOpDiscard)-import Control.Monad.Trans.Resource (MonadResource,- runResourceT)-import Data.Aeson (Value, decode, encode)-import Data.ByteString (ByteString)-import qualified Data.ByteString.Lazy as BL (fromStrict,- toStrict)-import Data.Conduit (await, awaitForever,- ($$))-import qualified Data.HashMap.Strict as H (lookup)-import Data.List.NonEmpty (NonEmpty ((:|)))-import qualified Data.Map.Strict as M (Map, assocs, elems,- empty,- fromListWith, null,- unionWith)-import Data.Maybe (fromJust, fromMaybe,- isJust)-import Data.Monoid ((<>))-import Data.String.Conversions (cs)-import Data.Text (pack)-import Data.Word (Word32)-import Database.Esqueleto (from, val, where_,- (&&.), (<=.), (==.),- (^.))-import Database.Persist.Sql (ConnectionPool,- runMigration)-import Network.Haskoin.Block-import Network.Haskoin.Constants-import Network.Haskoin.Node.BlockChain-import Network.Haskoin.Node.HeaderTree-import Network.Haskoin.Node.Peer-import Network.Haskoin.Node.STM-import Network.Haskoin.Transaction-import Network.Haskoin.Wallet.Accounts-import Network.Haskoin.Wallet.Database-import Network.Haskoin.Wallet.Model-import Network.Haskoin.Wallet.Server.Handler-import Network.Haskoin.Wallet.Settings-import Network.Haskoin.Wallet.Transaction-import Network.Haskoin.Wallet.Types-import System.Posix.Daemon (Redirection (ToFile),- killAndWait,- runDetached)-import System.ZMQ4 (Context, KeyFormat (..),- Pub (..), Rep (..),- Socket, bind, receive,- receiveMulti, restrict,- send, sendMulti,- setCurveSecretKey,- setCurveServer,- setLinger, withContext,- withSocket, z85Decode)--data EventSession = EventSession- { eventBatchSize :: !Int- , eventNewAddrs :: !(M.Map AccountId Word32)- }- deriving (Eq, Show, Read)--instance NFData EventSession where- rnf EventSession{..} =- rnf eventBatchSize `seq`- rnf (M.elems eventNewAddrs)--runSPVServer :: Config -> IO ()-runSPVServer cfg = maybeDetach cfg $ -- start the server process- withContext (run . runSPVServerWithContext cfg)- where- -- Setup logging monads- run = runResourceT . runLogging- runLogging = runStdoutLoggingT . filterLogger logFilter- logFilter _ level = level >= configLogLevel cfg---- |Run the server, and use the specifed ZeroMQ context.--- Useful if you want to communicate with the server using--- the "inproc" ZeroMQ transport, where a shared context is--- required.-runSPVServerWithContext :: ( MonadLoggerIO m- , MonadBaseControl IO m- , MonadBase IO m- , MonadThrow m- , MonadResource m- )- => Config -> Context -> m ()-runSPVServerWithContext cfg ctx = do- -- Initialize the database- -- Check the operation mode of the server.- pool <- initDatabase cfg- -- Notification channel- notif <- liftIO $ atomically $ newTBMChan 1000- case configMode cfg of- -- In this mode, we do not launch an SPV node. We only accept- -- client requests through the ZMQ API.- SPVOffline -> do- let session = HandlerSession cfg pool Nothing notif- as <- mapM async- -- Run the ZMQ API-command server- [ runWalletCmd ctx session- -- Run the ZMQ notification thread- , runWalletNotif ctx session- ]- mapM_ link as- (_,r) <- waitAnyCancel as- return r- -- In this mode, we launch the client ZMQ API and we sync the- -- wallet database with an SPV node.- SPVOnline -> do- -- Initialize the node state- node <- getNodeState (Right pool)- -- Spin up the node threads- let session = HandlerSession cfg pool (Just node) notif- as <- mapM async- -- Start the SPV node- [ runNodeT (spv pool) node- -- Merkle block synchronization- , runNodeT (runMerkleSync pool notif) node- -- Import solo transactions as they arrive from peers- , runNodeT (txSource $$ processTx pool notif) node- -- Respond to transaction GetData requests- , runNodeT (handleGetData $ (`runDBPool` pool) . getTx) node- -- Re-broadcast pending transactions- , runNodeT (broadcastPendingTxs pool) node- -- Run the ZMQ API-command server- , runWalletCmd ctx session- -- Run the ZMQ notification thread- , runWalletNotif ctx session- ]- mapM_ link as- (_,r) <- waitAnyCancel as- $(logDebug) "Exiting main thread"- return r- where- spv pool = do- -- Get our bloom filter- (bloom, elems, _) <- runDBPool getBloomFilter pool- startSPVNode hosts bloom elems- -- Bitcoin nodes to connect to- nodes = fromMaybe- (error $ "BTC nodes for " ++ networkName ++ " not found")- (pack networkName `H.lookup` configBTCNodes cfg)- hosts = map (\x -> PeerHost (btcNodeHost x) (btcNodePort x)) nodes- -- Run the merkle syncing thread- runMerkleSync pool notif = do- $(logDebug) "Waiting for a valid bloom filter for merkle downloads..."-- -- Only download merkles if we have a valid bloom filter- _ <- atomicallyNodeT waitBloomFilter-- -- Provide a fast catchup time if we are at height 0- fcM <- fmap (fmap adjustFCTime) $ (`runDBPool` pool) $ do- (_, h) <- walletBestBlock- if h == 0 then firstAddrTime else return Nothing- maybe (return ()) (atomicallyNodeT . rescanTs) fcM-- -- Start the merkle sync- merkleSync pool 500 notif- $(logDebug) "Exiting Merkle-sync thread"- -- Run a thread that will re-broadcast pending transactions- broadcastPendingTxs pool = forever $ do- (hash, _) <- runSqlNodeT $ walletBestBlock- -- Wait until we are synced- atomicallyNodeT $ do- synced <- areBlocksSynced hash- unless synced $ lift retry- -- Send an INV for those transactions to all peers- broadcastTxs =<< runDBPool (getPendingTxs 0) pool- -- Wait until we are not synced- atomicallyNodeT $ do- synced <- areBlocksSynced hash- when synced $ lift retry- $(logDebug) "Exiting tx-broadcast thread"- processTx pool notif = do- awaitForever $ \tx -> lift $ do- (_, newAddrs) <- runDBPool (importNetTx tx (Just notif)) pool- unless (null newAddrs) $ do- $(logInfo) $ pack $ unwords- [ "Generated", show $ length newAddrs- , "new addresses while importing the tx."- , "Updating the bloom filter"- ]- (bloom, elems, _) <- runDBPool getBloomFilter pool- atomicallyNodeT $ sendBloomFilter bloom elems- $(logDebug) "Exiting tx-import thread"--initDatabase :: (MonadBaseControl IO m, MonadLoggerIO m)- => Config -> m ConnectionPool-initDatabase cfg = do- -- Create a database pool- let dbCfg = fromMaybe- (error $ "DB config settings for " ++ networkName ++ " not found")- (pack networkName `H.lookup` configDatabase cfg)- pool <- getDatabasePool dbCfg- -- Initialize wallet database- flip runDBPool pool $ do- _ <- runMigration migrateWallet- _ <- runMigration migrateHeaderTree- initWallet $ configBloomFP cfg- -- Return the semaphrone and the connection pool- return pool--merkleSync- :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m, MonadResource m)- => ConnectionPool- -> Word32- -> TBMChan Notif- -> NodeT m ()-merkleSync pool bSize notif = do- -- Get our best block- (hash, _) <- runDBPool walletBestBlock pool- $(logDebug) "Starting merkle batch download"- -- Wait for a new block or a rescan- (action, source) <- merkleDownload hash bSize- $(logDebug) "Received a merkle action and source. Processing the source..."-- -- Read and process the data from the source- (lastMerkleM, mTxsAcc, aMap) <- source $$ go Nothing [] M.empty- $(logDebug) "Merkle source processed and closed"-- -- Send a new bloom filter to our peers if new addresses were generated- unless (M.null aMap) $ do- $(logInfo) $ pack $ unwords- [ "Generated", show $ sum $ M.elems aMap- , "new addresses while importing the merkle block."- , "Sending our bloom filter."- ]- (bloom, elems, _) <- runDBPool getBloomFilter pool- atomicallyNodeT $ sendBloomFilter bloom elems-- -- Check if we should rescan the current merkle batch- $(logDebug) "Checking if we need to rescan the current batch..."- rescan <- shouldRescan aMap- when rescan $ $(logDebug) "We need to rescan the current batch"- -- Compute the new batch size- let newBSize | rescan = max 1 $ bSize `div` 2- | otherwise = min 500 $ bSize + max 1 (bSize `div` 20)-- when (newBSize /= bSize) $ $(logDebug) $ pack $ unwords- [ "Changing block batch size from", show bSize, "to", show newBSize ]-- -- Did we receive all the merkles that we asked for ?- let missing = (headerHash <$> lastMerkleM) /=- Just (nodeHash $ last $ actionNodes action)-- when missing $ $(logWarn) $ pack $ unwords- [ "Merkle block stream closed prematurely"- , show lastMerkleM- ]-- -- TODO: We could still salvage a partially received batch- unless (rescan || missing) $ do- $(logDebug) "Importing merkles into the wallet..."- -- Confirm the transactions- runDBPool (importMerkles action mTxsAcc (Just notif)) pool- $(logDebug) "Done importing merkles into the wallet"- logBlockChainAction action-- merkleSync pool newBSize notif- where- go lastMerkleM mTxsAcc aMap = await >>= \resM -> case resM of- Just (Right tx) -> do- $(logDebug) $ pack $ unwords- [ "Importing merkle tx", cs $ txHashToHex $ txHash tx ]- (_, newAddrs) <- lift $ runDBPool (importNetTx tx Nothing) pool- $(logDebug) $ pack $ unwords- [ "Generated", show $ length newAddrs- , "new addresses while importing tx"- , cs $ txHashToHex $ txHash tx- ]- let newMap = M.unionWith (+) aMap $ groupByAcc newAddrs- go lastMerkleM mTxsAcc newMap- Just (Left (MerkleBlock mHead _ _ _, mTxs)) -> do- $(logDebug) $ pack $ unwords- [ "Buffering merkle block"- , cs $ blockHashToHex $ headerHash mHead- ]- go (Just mHead) (mTxs:mTxsAcc) aMap- -- Done processing this batch. Reverse mTxsAcc as we have been- -- prepending new values to it.- _ -> return (lastMerkleM, reverse mTxsAcc, aMap)- groupByAcc addrs =- let xs = map (\a -> (walletAddrAccount a, 1)) addrs- in M.fromListWith (+) xs- shouldRescan aMap = do- -- Try to find an account whos gap is smaller than the number of new- -- addresses generated in that account.- res <- (`runDBPool` pool) $ splitSelect (M.assocs aMap) $ \ks ->- from $ \a -> do- let andCond (ai, cnt) =- a ^. AccountId ==. val ai &&.- a ^. AccountGap <=. val cnt- where_ $ join2 $ map andCond ks- return $ a ^. AccountId- return $ not $ null res- -- Some logging of the blocks- logBlockChainAction action = case action of- BestChain nodes -> $(logInfo) $ pack $ unwords- [ "Best chain height"- , show $ nodeBlockHeight $ last nodes- , "(", cs $ blockHashToHex $ nodeHash $ last nodes- , ")"- ]- ChainReorg _ o n -> $(logInfo) $ pack $ unlines $- [ "Chain reorg."- , "Orphaned blocks:"- ]- ++ map ((" " ++) . cs . blockHashToHex . nodeHash) o- ++ [ "New blocks:" ]- ++ map ((" " ++) . cs . blockHashToHex . nodeHash) n- ++ [ unwords [ "Best merkle chain height"- , show $ nodeBlockHeight $ last n- ]- ]- SideChain n -> $(logWarn) $ pack $ unlines $- "Side chain:" :- map ((" " ++) . cs . blockHashToHex . nodeHash) n- KnownChain n -> $(logWarn) $ pack $ unlines $- "Known chain:" :- map ((" " ++) . cs . blockHashToHex . nodeHash) n--maybeDetach :: Config -> IO () -> IO ()-maybeDetach cfg action =- if configDetach cfg then runDetached pidFile logFile action >> logStarted else action- where- logStarted = putStrLn "Process started"- pidFile = Just $ configPidFile cfg- logFile = ToFile $ configLogFile cfg--stopSPVServer :: Config -> IO ()-stopSPVServer cfg =- -- TODO: Should we send a message instead of killing the process ?- killAndWait $ configPidFile cfg---- Run the main ZeroMQ loop--- TODO: Support concurrent requests using DEALER socket when we can do--- concurrent MySQL requests.-runWalletNotif :: ( MonadLoggerIO m- , MonadBaseControl IO m- , MonadBase IO m- , MonadThrow m- , MonadResource m- )- => Context -> HandlerSession -> m ()-runWalletNotif ctx session =- liftBaseOpDiscard (withSocket ctx Pub) $ \sock -> do- liftIO $ setLinger (restrict (0 :: Int)) sock- setupCrypto ctx sock session- liftIO $ bind sock $ configBindNotif $ handlerConfig session- forever $ do- xM <- liftIO $ atomically $ readTBMChan $ handlerNotifChan session- forM_ xM $ \x ->- let (typ, pay) = case x of- NotifBlock _ ->- ("[block]", cs $ encode x)- NotifTx JsonTx{..} ->- ("{" <> cs jsonTxAccount <> "}", cs $ encode x)- in liftIO $ sendMulti sock $ typ :| [pay]--runWalletCmd :: ( MonadLoggerIO m- , MonadBaseControl IO m- , MonadBase IO m- , MonadThrow m- , MonadResource m- )- => Context -> HandlerSession -> m ()-runWalletCmd ctx session = do- liftBaseOpDiscard (withSocket ctx Rep) $ \sock -> do- liftIO $ setLinger (restrict (0 :: Int)) sock- setupCrypto ctx sock session- liftIO $ bind sock $ configBind $ handlerConfig session- fix $ \loop -> do- bs <- liftIO $ receive sock- let msg = decode $ BL.fromStrict bs- res <- case msg of- Just StopServerR -> do- $(logInfo) "Received StopServer request"- return (ResponseValid Nothing)- Just r -> catchErrors $- runHandler (dispatchRequest r) session- Nothing -> return $ ResponseError "Could not decode request"- liftIO $ send sock [] $ BL.toStrict $ encode res- unless (msg == Just StopServerR) loop- $(logInfo) "Exiting ZMQ command thread..."- where- catchErrors m = catches m- [ E.Handler $ \(WalletException err) -> do- $(logError) $ pack err- return $ ResponseError $ pack err- , E.Handler $ \(ErrorCall err) -> do- $(logError) $ pack err- return $ ResponseError $ pack err- , E.Handler $ \(SomeException exc) -> do- $(logError) $ pack $ show exc- return $ ResponseError $ pack $ show exc- ]--setupCrypto :: (MonadLoggerIO m, MonadBaseControl IO m)- => Context -> Socket a -> HandlerSession -> m ()-setupCrypto ctx' sock session = do- when (isJust serverKeyM) $ liftIO $ do- let k = fromJust $ configServerKey $ handlerConfig session- setCurveServer True sock- setCurveSecretKey TextFormat k sock- when (isJust clientKeyPubM) $ do- k <- z85Decode (fromJust clientKeyPubM)- void $ async $ runZapAuth ctx' k- where- cfg = handlerConfig session- serverKeyM = configServerKey cfg- clientKeyPubM = configClientKeyPub cfg--runZapAuth :: ( MonadLoggerIO m- , MonadBaseControl IO m- , MonadBase IO m- )- => Context -> ByteString -> m ()-runZapAuth ctx k = do- $(logDebug) $ "Starting ØMQ authentication thread"- liftBaseOpDiscard (withSocket ctx Rep) $ \zap -> do- liftIO $ setLinger (restrict (0 :: Int)) zap- liftIO $ bind zap "inproc://zeromq.zap.01"- forever $ do- buffer <- liftIO $ receiveMulti zap- let actionE =- case buffer of- v:q:_:_:_:m:p:_ -> do- when (v /= "1.0") $- Left (q, "500", "Version number not valid")- when (m /= "CURVE") $- Left (q, "400", "Mechanism not supported")- when (p /= k) $- Left (q, "400", "Invalid client public key")- return q- _ -> Left ("", "500", "Malformed request")- case actionE of- Right q -> do- $(logInfo) "Authenticated client successfully"- liftIO $ sendMulti zap $- "1.0" :| [q, "200", "OK", "client", ""]- Left (q, c, m) -> do- $(logError) $ pack $ unwords- [ "Failed to authenticate client:" , cs c, cs m ]- liftIO $ sendMulti zap $- "1.0" :| [q, c, m, "", ""]---dispatchRequest :: ( MonadLoggerIO m- , MonadBaseControl IO m- , MonadBase IO m- , MonadThrow m- , MonadResource m- )- => WalletRequest -> Handler m (WalletResponse Value)-dispatchRequest req = fmap ResponseValid $ case req of- GetAccountsR p -> getAccountsR p- PostAccountsR na -> postAccountsR na- PostAccountRenameR n n' -> postAccountRenameR n n'- GetAccountR n -> getAccountR n- PostAccountKeysR n ks -> postAccountKeysR n ks- PostAccountGapR n g -> postAccountGapR n g- GetAddressesR n t m o p -> getAddressesR n t m o p- GetAddressesUnusedR n t p -> getAddressesUnusedR n t p- GetAddressR n i t m o -> getAddressR n i t m o- GetIndexR n k t -> getIndexR n k t- PutAddressR n i t l -> putAddressR n i t l- PostAddressesR n i t -> postAddressesR n i t- GetTxsR n p -> getTxsR n p- GetAddrTxsR n i t p -> getAddrTxsR n i t p- PostTxsR n k a -> postTxsR n k a- GetTxR n h -> getTxR n h- GetOfflineTxR n h -> getOfflineTxR n h- PostOfflineTxR n k t c -> postOfflineTxR n k t c- GetBalanceR n mc o -> getBalanceR n mc o- PostNodeR na -> postNodeR na- DeleteTxIdR t -> deleteTxIdR t- GetSyncR a n b -> getSyncR a (Right n) b- GetSyncHeightR a n b -> getSyncR a (Left n) b- GetPendingR a p -> getPendingR a p- GetDeadR a p -> getDeadR a p- GetBlockInfoR l -> getBlockInfoR l- StopServerR -> error "Should be handled elsewhere"
@@ -1,604 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TemplateHaskell #-}-module Network.Haskoin.Wallet.Server.Handler where--import Control.Arrow (first)-import Control.Concurrent.STM.TBMChan (TBMChan)-import Control.Exception (SomeException (..),- tryJust)-import Control.Monad (liftM, forM, unless, when)-import Control.Monad.Base (MonadBase)-import Control.Monad.Catch (MonadThrow, throwM)-import Control.Monad.Logger (MonadLoggerIO, logError,- logInfo)-import Control.Monad.Reader (ReaderT, asks, runReaderT)-import Control.Monad.Trans (MonadIO, lift, liftIO)-import Control.Monad.Trans.Control (MonadBaseControl)-import Control.Monad.Trans.Resource (MonadResource)-import Data.Aeson (Value (..), toJSON)-import qualified Data.Map.Strict as M (elems, fromList,- intersectionWith)-import Data.String.Conversions (cs)-import Data.Text (Text, pack, unpack)-import Data.Word (Word32)-import Data.Maybe (catMaybes)-import Database.Esqueleto (Entity (..), SqlPersistT)-import Database.Persist.Sql (ConnectionPool,- SqlPersistM,- runSqlPersistMPool,- runSqlPool)-import Network.Haskoin.Block-import Network.Haskoin.Crypto-import Network.Haskoin.Node.BlockChain-import Network.Haskoin.Node.HeaderTree-import Network.Haskoin.Node.Peer-import Network.Haskoin.Node.STM-import Network.Haskoin.Transaction-import Network.Haskoin.Wallet.Accounts-import Network.Haskoin.Wallet.Block-import Network.Haskoin.Wallet.Model-import Network.Haskoin.Wallet.Settings-import Network.Haskoin.Wallet.Transaction-import Network.Haskoin.Wallet.Types-import Network.Haskoin.Wallet.Types.BlockInfo (fromNodeBlock)--type Handler m = ReaderT HandlerSession m--data HandlerSession = HandlerSession- { handlerConfig :: !Config- , handlerPool :: !ConnectionPool- , handlerNodeState :: !(Maybe SharedNodeState)- , handlerNotifChan :: !(TBMChan Notif)- }--runHandler :: Monad m => Handler m a -> HandlerSession -> m a-runHandler = runReaderT--runDB :: MonadBaseControl IO m => SqlPersistT m a -> Handler m a-runDB action = asks handlerPool >>= lift . runDBPool action--runDBPool :: MonadBaseControl IO m => SqlPersistT m a -> ConnectionPool -> m a-runDBPool = runSqlPool--tryDBPool :: MonadLoggerIO m => ConnectionPool -> SqlPersistM a -> m (Maybe a)-tryDBPool pool action = do- resE <- liftIO $ tryJust f $ runSqlPersistMPool action pool- case resE of- Right res -> return $ Just res- Left err -> do- $(logError) $ pack $ unwords [ "A database error occured:", err]- return Nothing- where- f (SomeException e) = Just $ show e--runNode :: MonadIO m => NodeT m a -> Handler m a-runNode action = do- nodeStateM <- asks handlerNodeState- case nodeStateM of- Just nodeState -> lift $ runNodeT action nodeState- _ -> error "runNode: No node state available"--{- Server Handlers -}--getAccountsR :: ( MonadLoggerIO m- , MonadBaseControl IO m- , MonadBase IO m- , MonadThrow m- , MonadResource m- )- => ListRequest- -> Handler m (Maybe Value)-getAccountsR lq@ListRequest{..} = do- $(logInfo) $ format $ unlines- [ "GetAccountsR"- , " Offset : " ++ show listOffset- , " Limit : " ++ show listLimit- , " Reversed : " ++ show listReverse- ]- (accs, cnt) <- runDB $ accounts lq- return $ Just $ toJSON $ ListResult (map (toJsonAccount Nothing) accs) cnt--postAccountsR- :: (MonadResource m, MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)- => NewAccount -> Handler m (Maybe Value)-postAccountsR newAcc@NewAccount{..} = do- $(logInfo) $ format $ unlines- [ "PostAccountsR"- , " Account name: " ++ unpack newAccountName- , " Account type: " ++ show newAccountType- ]- (Entity _ newAcc', mnemonicM) <- runDB $ newAccount newAcc- -- Update the bloom filter if the account is complete- whenOnline $ when (isCompleteAccount newAcc') updateNodeFilter- return $ Just $ toJSON $ toJsonAccount mnemonicM newAcc'--postAccountRenameR- :: (MonadResource m, MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)- => AccountName -> AccountName -> Handler m (Maybe Value)-postAccountRenameR oldName newName = do- $(logInfo) $ format $ unlines- [ "PostAccountRenameR"- , " Account name: " ++ unpack oldName- , " New name : " ++ unpack newName- ]- newAcc <- runDB $ do- accE <- getAccount oldName- renameAccount accE newName- return $ Just $ toJSON $ toJsonAccount Nothing newAcc--getAccountR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> Handler m (Maybe Value)-getAccountR name = do- $(logInfo) $ format $ unlines- [ "GetAccountR"- , " Account name: " ++ unpack name- ]- Entity _ acc <- runDB $ getAccount name- return $ Just $ toJSON $ toJsonAccount Nothing acc--postAccountKeysR- :: (MonadResource m, MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)- => AccountName -> [XPubKey] -> Handler m (Maybe Value)-postAccountKeysR name keys = do- $(logInfo) $ format $ unlines- [ "PostAccountKeysR"- , " Account name: " ++ unpack name- , " Key count : " ++ show (length keys)- ]- newAcc <- runDB $ do- accE <- getAccount name- addAccountKeys accE keys- -- Update the bloom filter if the account is complete- whenOnline $ when (isCompleteAccount newAcc) updateNodeFilter- return $ Just $ toJSON $ toJsonAccount Nothing newAcc--postAccountGapR :: ( MonadLoggerIO m- , MonadBaseControl IO m- , MonadBase IO m- , MonadThrow m- , MonadResource m- )- => AccountName -> SetAccountGap- -> Handler m (Maybe Value)-postAccountGapR name (SetAccountGap gap) = do- $(logInfo) $ format $ unlines- [ "PostAccountGapR"- , " Account name: " ++ unpack name- , " New gap size: " ++ show gap- ]- -- Update the gap- Entity _ newAcc <- runDB $ do- accE <- getAccount name- setAccountGap accE gap- -- Update the bloom filter- whenOnline updateNodeFilter- return $ Just $ toJSON $ toJsonAccount Nothing newAcc--getAddressesR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName- -> AddressType- -> Word32- -> Bool- -> ListRequest- -> Handler m (Maybe Value)-getAddressesR name addrType minConf offline listReq = do- $(logInfo) $ format $ unlines- [ "GetAddressesR"- , " Account name: " ++ unpack name- , " Address type: " ++ show addrType- , " Start index : " ++ show (listOffset listReq)- , " Reversed : " ++ show (listReverse listReq)- , " MinConf : " ++ show minConf- , " Offline : " ++ show offline- ]-- (res, bals, cnt) <- runDB $ do- accE <- getAccount name- (res, cnt) <- addressList accE addrType listReq- case res of- [] -> return (res, [], cnt)- _ -> do- let is = map walletAddrIndex res- (iMin, iMax) = (minimum is, maximum is)- bals <- addressBalances accE iMin iMax addrType minConf offline- return (res, bals, cnt)-- -- Join addresses and balances together- let g (addr, bal) = toJsonAddr addr (Just bal)- addrBals = map g $ M.elems $ joinAddrs res bals- return $ Just $ toJSON $ ListResult addrBals cnt- where- joinAddrs addrs bals =- let f addr = (walletAddrIndex addr, addr)- in M.intersectionWith (,) (M.fromList $ map f addrs) (M.fromList bals)--getAddressesUnusedR- :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> AddressType -> ListRequest -> Handler m (Maybe Value)-getAddressesUnusedR name addrType lq@ListRequest{..} = do- $(logInfo) $ format $ unlines- [ "GetAddressesUnusedR"- , " Account name: " ++ unpack name- , " Address type: " ++ show addrType- , " Offset : " ++ show listOffset- , " Limit : " ++ show listLimit- , " Reversed : " ++ show listReverse- ]-- (addrs, cnt) <- runDB $ do- accE <- getAccount name- unusedAddresses accE addrType lq-- return $ Just $ toJSON $ ListResult (map (`toJsonAddr` Nothing) addrs) cnt--getAddressR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> KeyIndex -> AddressType- -> Word32 -> Bool- -> Handler m (Maybe Value)-getAddressR name i addrType minConf offline = do- $(logInfo) $ format $ unlines- [ "GetAddressR"- , " Account name: " ++ unpack name- , " Index : " ++ show i- , " Address type: " ++ show addrType- ]-- (addr, balM) <- runDB $ do- accE <- getAccount name- addrE <- getAddress accE addrType i- bals <- addressBalances accE i i addrType minConf offline- return $ case bals of- ((_,bal):_) -> (entityVal addrE, Just bal)- _ -> (entityVal addrE, Nothing)- return $ Just $ toJSON $ toJsonAddr addr balM--getIndexR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName- -> PubKeyC- -> AddressType- -> Handler m (Maybe Value)-getIndexR name key addrType = do- $(logInfo) $ format $ unlines- [ "getIndexR"- , " Account name: " ++ unpack name- , " Key : " ++ show key- , " Address type: " ++ show addrType- ]- addrLst <- runDB $ do- accE <- getAccount name- lookupByPubKey accE key addrType- let hello = map (`toJsonAddr` Nothing) addrLst- liftIO $ putStrLn $ show hello- liftIO $ print $ toJSON $ hello- return $ Just $ toJSON $ hello--putAddressR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName- -> KeyIndex- -> AddressType- -> AddressLabel- -> Handler m (Maybe Value)-putAddressR name i addrType (AddressLabel label) = do- $(logInfo) $ format $ unlines- [ "PutAddressR"- , " Account name: " ++ unpack name- , " Index : " ++ show i- , " Label : " ++ unpack label- ]-- newAddr <- runDB $ do- accE <- getAccount name- setAddrLabel accE i addrType label-- return $ Just $ toJSON $ toJsonAddr newAddr Nothing--postAddressesR :: ( MonadLoggerIO m- , MonadBaseControl IO m- , MonadThrow m- , MonadBase IO m- , MonadResource m- )- => AccountName- -> KeyIndex- -> AddressType- -> Handler m (Maybe Value)-postAddressesR name i addrType = do- $(logInfo) $ format $ unlines- [ "PostAddressesR"- , " Account name: " ++ unpack name- , " Index : " ++ show i- ]-- cnt <- runDB $ do- accE <- getAccount name- generateAddrs accE addrType i-- -- Update the bloom filter- whenOnline updateNodeFilter-- return $ Just $ toJSON cnt--getTxs :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName- -> ListRequest- -> String- -> (AccountId -> ListRequest -> SqlPersistT m ([WalletTx], Word32))- -> Handler m (Maybe Value)-getTxs name lq@ListRequest{..} cmd f = do- $(logInfo) $ format $ unlines- [ cmd- , " Account name: " ++ unpack name- , " Offset : " ++ show listOffset- , " Limit : " ++ show listLimit- , " Reversed : " ++ show listReverse- ]-- (res, cnt, bb) <- runDB $ do- Entity ai _ <- getAccount name- bb <- walletBestBlock- (res, cnt) <- f ai lq- return (res, cnt, bb)-- return $ Just $ toJSON $ ListResult (map (g bb) res) cnt- where- g bb = toJsonTx name (Just bb)--getTxsR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> ListRequest -> Handler m (Maybe Value)-getTxsR name lq = getTxs name lq "GetTxsR" (txs Nothing)--getPendingR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> ListRequest -> Handler m (Maybe Value)-getPendingR name lq = getTxs name lq "GetPendingR" (txs (Just TxPending))--getDeadR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> ListRequest -> Handler m (Maybe Value)-getDeadR name lq = getTxs name lq "GetDeadR" (txs (Just TxDead))--getAddrTxsR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> KeyIndex -> AddressType -> ListRequest- -> Handler m (Maybe Value)-getAddrTxsR name index addrType lq@ListRequest{..} = do- $(logInfo) $ format $ unlines- [ "GetAddrTxsR"- , " Account name : " ++ unpack name- , " Address index: " ++ show index- , " Address type : " ++ show addrType- , " Offset : " ++ show listOffset- , " Limit : " ++ show listLimit- , " Reversed : " ++ show listReverse- ]-- (res, cnt, bb) <- runDB $ do- accE <- getAccount name- addrE <- getAddress accE addrType index- bb <- walletBestBlock- (res, cnt) <- addrTxs accE addrE lq- return (res, cnt, bb)-- return $ Just $ toJSON $ ListResult (map (f bb) res) cnt- where- f bb = toJsonTx name (Just bb)--postTxsR :: ( MonadLoggerIO m, MonadBaseControl IO m, MonadBase IO m- , MonadThrow m, MonadResource m- )- => AccountName -> Maybe XPrvKey -> TxAction -> Handler m (Maybe Value)-postTxsR name masterM action = do- (accE@(Entity ai _), bb) <- runDB $ do- accE <- getAccount name- bb <- walletBestBlock- return (accE, bb)-- notif <- asks handlerNotifChan- (txRes, newAddrs) <- case action of- CreateTx rs fee minconf rcptFee sign -> do- $(logInfo) $ format $ unlines- [ "PostTxsR CreateTx"- , " Account name: " ++ unpack name- , " Recipients : " ++ show (map (first addrToBase58) rs)- , " Fee : " ++ show fee- , " Minconf : " ++ show minconf- , " Rcpt. Fee : " ++ show rcptFee- , " Sign : " ++ show sign- ]- runDB $ createWalletTx- accE (Just notif) masterM rs fee minconf rcptFee sign- ImportTx tx -> do- $(logInfo) $ format $ unlines- [ "PostTxsR ImportTx"- , " Account name: " ++ unpack name- , " TxId : " ++ cs (txHashToHex (txHash tx))- ]- runDB $ do- (res, newAddrs) <- importTx tx (Just notif) ai- case filter ((== ai) . walletTxAccount) res of- (txRes:_) -> return (txRes, newAddrs)- _ -> throwM $ WalletException- "Could not import the transaction"- SignTx txid -> do- $(logInfo) $ format $ unlines- [ "PostTxsR SignTx"- , " Account name: " ++ unpack name- , " TxId : " ++ cs (txHashToHex txid)- ]- runDB $ do- (res, newAddrs) <- signAccountTx accE (Just notif) masterM txid- case filter ((== ai) . walletTxAccount) res of- (txRes:_) -> return (txRes, newAddrs)- _ -> throwM $ WalletException- "Could not import the transaction"- whenOnline $ do- -- Update the bloom filter- unless (null newAddrs) updateNodeFilter- -- If the transaction is pending, broadcast it to the network- when (walletTxConfidence txRes == TxPending) $- runNode $ broadcastTxs [walletTxHash txRes]- return $ Just $ toJSON $ toJsonTx name (Just bb) txRes--getTxR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> TxHash -> Handler m (Maybe Value)-getTxR name txid = do- $(logInfo) $ format $ unlines- [ "GetTxR"- , " Account name: " ++ unpack name- , " TxId : " ++ cs (txHashToHex txid)- ]- (res, bb) <- runDB $ do- Entity ai _ <- getAccount name- bb <- walletBestBlock- res <- getAccountTx ai txid- return (res, bb)- return $ Just $ toJSON $ toJsonTx name (Just bb) res--deleteTxIdR :: (MonadLoggerIO m, MonadThrow m, MonadBaseControl IO m)- => TxHash -> Handler m (Maybe Value)-deleteTxIdR txid = do- $(logInfo) $ format $ unlines- [ "DeleteTxR"- , " TxId: " ++ cs (txHashToHex txid)- ]- runDB $ deleteTx txid- return Nothing--getBalanceR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => AccountName -> Word32 -> Bool- -> Handler m (Maybe Value)-getBalanceR name minconf offline = do- $(logInfo) $ format $ unlines- [ "GetBalanceR"- , " Account name: " ++ unpack name- , " Minconf : " ++ show minconf- , " Offline : " ++ show offline- ]- bal <- runDB $ do- Entity ai _ <- getAccount name- accountBalance ai minconf offline- return $ Just $ toJSON bal--getOfflineTxR :: ( MonadLoggerIO m, MonadBaseControl IO m- , MonadBase IO m, MonadThrow m, MonadResource m- )- => AccountName -> TxHash -> Handler m (Maybe Value)-getOfflineTxR accountName txid = do- $(logInfo) $ format $ unlines- [ "GetOfflineTxR"- , " Account name: " ++ unpack accountName- , " TxId : " ++ cs (txHashToHex txid)- ]- (dat, _) <- runDB $ do- Entity ai _ <- getAccount accountName- getOfflineTxData ai txid- return $ Just $ toJSON dat--postOfflineTxR :: ( MonadLoggerIO m, MonadBaseControl IO m- , MonadBase IO m, MonadThrow m, MonadResource m- )- => AccountName- -> Maybe XPrvKey- -> Tx- -> [CoinSignData]- -> Handler m (Maybe Value)-postOfflineTxR accountName masterM tx signData = do- $(logInfo) $ format $ unlines- [ "PostTxsR SignOfflineTx"- , " Account name: " ++ unpack accountName- , " TxId : " ++ cs (txHashToHex (txHash tx))- ]- Entity _ acc <- runDB $ getAccount accountName- let signedTx = signOfflineTx acc masterM tx signData- complete = verifyStdTx signedTx $ map toDat signData- toDat CoinSignData{..} = (coinSignScriptOutput, coinSignOutPoint)- return $ Just $ toJSON $ TxCompleteRes signedTx complete--postNodeR :: (MonadLoggerIO m, MonadBaseControl IO m, MonadThrow m)- => NodeAction -> Handler m (Maybe Value)-postNodeR action = case action of- NodeActionRescan tM -> do- t <- case tM of- Just t -> return $ adjustFCTime t- Nothing -> do- timeM <- runDB firstAddrTime- maybe err (return . adjustFCTime) timeM- $(logInfo) $ format $ unlines- [ "NodeR Rescan"- , " Timestamp: " ++ show t- ]- whenOnline $ do- runDB resetRescan- runNode $ atomicallyNodeT $ rescanTs t- return $ Just $ toJSON $ RescanRes t- NodeActionStatus -> do- status <- runNode $ atomicallyNodeT nodeStatus- return $ Just $ toJSON status- where- err = throwM $ WalletException- "No keys have been generated in the wallet"--getSyncR :: (MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)- => AccountName- -> Either BlockHeight BlockHash- -> ListRequest- -> Handler m (Maybe Value)-getSyncR acc blockE lq@ListRequest{..} = runDB $ do- $(logInfo) $ format $ unlines- [ "GetSyncR"- , " Account name: " ++ cs acc- , " Block : " ++ showBlock- , " Offset : " ++ show listOffset- , " Limit : " ++ show listLimit- , " Reversed : " ++ show listReverse- ]- ListResult nodes cnt <- mainChain blockE lq- case nodes of- [] -> return $ Just $ toJSON $ ListResult ([] :: [()]) cnt- b:_ -> do- Entity ai _ <- getAccount acc- ts <- accTxsFromBlock ai (nodeBlockHeight b)- (fromIntegral $ length nodes)- let bts = blockTxs nodes ts- return $ Just $ toJSON $ ListResult (map f bts) cnt- where- f (block, txs') = JsonSyncBlock- { jsonSyncBlockHash = nodeHash block- , jsonSyncBlockHeight = nodeBlockHeight block- , jsonSyncBlockPrev = nodePrev block- , jsonSyncBlockTxs = map (toJsonTx acc Nothing) txs'- }- showBlock = case blockE of- Left e -> show e- Right b -> cs $ blockHashToHex b--getBlockInfoR :: (MonadThrow m, MonadLoggerIO m, MonadBaseControl IO m)- => [BlockHash]- -> Handler m (Maybe Value)-getBlockInfoR headerLst = do- lstMaybeBlk <- forM headerLst (runNode . runSqlNodeT . getBlockByHash)- return $ toJSON <$> Just (handleRes lstMaybeBlk)- where- handleRes :: [Maybe NodeBlock] -> [BlockInfo]- handleRes = map fromNodeBlock . catMaybes--{- Helpers -}--whenOnline :: Monad m => Handler m () -> Handler m ()-whenOnline handler = do- mode <- configMode `liftM` asks handlerConfig- when (mode == SPVOnline) handler--updateNodeFilter- :: (MonadBaseControl IO m, MonadLoggerIO m, MonadThrow m)- => Handler m ()-updateNodeFilter = do- $(logInfo) $ format "Sending a new bloom filter"- (bloom, elems, _) <- runDB getBloomFilter- runNode $ atomicallyNodeT $ sendBloomFilter bloom elems--adjustFCTime :: Timestamp -> Timestamp-adjustFCTime ts = fromInteger $ max 0 $ toInteger ts - 86400 * 7--format :: String -> Text-format str = pack $ "[ZeroMQ] " ++ str-
@@ -1,196 +0,0 @@-module Network.Haskoin.Wallet.Settings-( SPVMode(..)-, OutputFormat(..)-, Config(..)-) where--import Control.Monad (mzero)-import Control.Exception (throw)-import Control.Monad.Logger (LogLevel(..))--import Data.Default (Default, def)-import Data.FileEmbed (embedFile)-import Data.Yaml (decodeEither')-import Data.Word (Word32, Word64)-import Data.HashMap.Strict (HashMap, unionWith)-import Data.String.Conversions (cs)-import Data.ByteString (ByteString)-import Data.Text (Text)-import Data.Text.Encoding (encodeUtf8)-import Data.Aeson- ( Value(..), FromJSON, ToJSON- , parseJSON, toJSON, withObject- , (.:)- )--import Network.Haskoin.Crypto-import Network.Haskoin.Wallet.Database-import Network.Haskoin.Wallet.Types--import Data.Restricted (Restricted, Div5)-import System.ZMQ4 (toRestricted)--data SPVMode = SPVOnline | SPVOffline- deriving (Eq, Show, Read)--newtype LogLevelJSON = LogLevelJSON LogLevel- deriving (Eq, Show, Read)--data OutputFormat- = OutputNormal- | OutputJSON- | OutputYAML--data Config = Config- { configCount :: !Word32- -- ^ Output size of commands- , configMinConf :: !Word32- -- ^ Minimum number of confirmations- , configSignTx :: !Bool- -- ^ Sign transactions- , configFee :: !Word64- -- ^ Fee to pay per 1000 bytes when creating new transactions- , configRcptFee :: !Bool- -- ^ Recipient pays fee (dangerous, no config file setting)- , configAddrType :: !AddressType- -- ^ Return internal instead of external addresses- , configOffline :: !Bool- -- ^ Display the balance including offline transactions- , configReversePaging :: !Bool- -- ^ Use reverse paging for displaying addresses and transactions- , configPath :: !(Maybe HardPath)- -- ^ Derivation path when creating account- , configFormat :: !OutputFormat- -- ^ How to format the command-line results- , configConnect :: !String- -- ^ ZeroMQ socket to connect to (location of the server)- , configConnectNotif :: !String- -- ^ ZeroMQ socket to connect for notifications- , configDetach :: !Bool- -- ^ Detach server when launched from command-line- , configFile :: !FilePath- -- ^ Configuration file- , configTestnet :: !Bool- -- ^ Use Testnet3 network- , configDir :: !FilePath- -- ^ Working directory- , configBind :: !String- -- ^ Bind address for the ZeroMQ socket- , configBindNotif :: !String- -- ^ Bind address for ZeroMQ notifications- , configBTCNodes :: !(HashMap Text [BTCNode])- -- ^ Trusted Bitcoin full nodes to connect to- , configMode :: !SPVMode- -- ^ Operation mode of the SPV node.- , configBloomFP :: !Double- -- ^ False positive rate for the bloom filter.- , configDatabase :: !(HashMap Text DatabaseConfType)- -- ^ Database configuration- , configLogFile :: !FilePath- -- ^ Log file- , configPidFile :: !FilePath- -- ^ PID File- , configLogLevel :: !LogLevel- -- ^ Log level- , configVerbose :: !Bool- -- ^ Verbose- , configServerKey :: !(Maybe (Restricted Div5 ByteString))- -- ^ Server key for authentication and encryption (server config)- , configServerKeyPub :: !(Maybe (Restricted Div5 ByteString))- -- ^ Server public key for authentication and encryption (client config)- , configClientKey :: !(Maybe (Restricted Div5 ByteString))- -- ^ Client key for authentication and encryption (client config)- , configClientKeyPub :: !(Maybe (Restricted Div5 ByteString))- -- ^ Client public key for authentication and encryption- -- (client + server config)- }--configBS :: ByteString-configBS = $(embedFile "config/config.yml")--instance ToJSON OutputFormat where- toJSON OutputNormal = String "normal"- toJSON OutputJSON = String "json"- toJSON OutputYAML = String "yaml"--instance FromJSON OutputFormat where- parseJSON (String "normal") = return OutputNormal- parseJSON (String "json") = return OutputJSON- parseJSON (String "yaml") = return OutputYAML- parseJSON _ = mzero--instance ToJSON SPVMode where- toJSON SPVOnline = String "online"- toJSON SPVOffline = String "offline"--instance FromJSON SPVMode where- parseJSON (String "online") = return SPVOnline- parseJSON (String "offline") = return SPVOffline- parseJSON _ = mzero--instance ToJSON LogLevelJSON where- toJSON (LogLevelJSON LevelDebug) = String "debug"- toJSON (LogLevelJSON LevelInfo) = String "info"- toJSON (LogLevelJSON LevelWarn) = String "warn"- toJSON (LogLevelJSON LevelError) = String "error"- toJSON (LogLevelJSON (LevelOther t)) = String t--instance FromJSON LogLevelJSON where- parseJSON (String "debug") = return $ LogLevelJSON LevelDebug- parseJSON (String "info") = return $ LogLevelJSON LevelInfo- parseJSON (String "warn") = return $ LogLevelJSON LevelWarn- parseJSON (String "error") = return $ LogLevelJSON LevelError- parseJSON (String x) = return $ LogLevelJSON (LevelOther x)- parseJSON _ = mzero--instance Default Config where- def = either throw id $ decodeEither' "{}"--instance FromJSON Config where- parseJSON = withObject "config" $ \o' -> do- let defValue = either throw id $ decodeEither' configBS- (Object o) = mergeValues defValue (Object o')- configPath = Nothing- configFile <- o .: "config-file"- configRcptFee <- o .: "recipient-fee"- configCount <- o .: "output-size"- configMinConf <- o .: "minimum-confirmations"- configSignTx <- o .: "sign-transactions"- configFee <- o .: "transaction-fee"- configAddrType <- o .: "address-type"- configOffline <- o .: "offline"- configReversePaging <- o .: "reverse-paging"- configFormat <- o .: "display-format"- configConnect <- o .: "connect-uri"- configConnectNotif <- o .: "connect-uri-notif"- configDetach <- o .: "detach-server"- configTestnet <- o .: "use-testnet"- configDir <- o .: "work-dir"- configBind <- o .: "bind-socket"- configBindNotif <- o .: "bind-socket-notif"- configBTCNodes <- o .: "bitcoin-full-nodes"- configMode <- o .: "server-mode"- configBloomFP <- o .: "bloom-false-positive"- configLogFile <- o .: "log-file"- configPidFile <- o .: "pid-file"- LogLevelJSON configLogLevel <- o .: "log-level"- configVerbose <- o .: "verbose"- configDatabase <- o .: "database"- configServerKey <- getKey o "server-key"- configServerKeyPub <- getKey o "server-key-public"- configClientKey <- getKey o "client-key"- configClientKeyPub <- getKey o "client-key-public"- return Config {..}- where- getKey o i = o .: i >>= \kM ->- case kM of- Nothing -> return Nothing- Just k ->- case toRestricted $ encodeUtf8 k of- Just k' -> return $ Just k'- Nothing -> fail $ "Invalid " ++ cs k---mergeValues :: Value -> Value -> Value-mergeValues (Object d) (Object c) = Object (unionWith mergeValues d c)-mergeValues _ c = c
@@ -1,1358 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE RecordWildCards #-}-module Network.Haskoin.Wallet.Transaction-(--- *Database transactions- txs-, addrTxs-, accTxsFromBlock-, getTx-, getAccountTx-, importTx-, importNetTx-, signAccountTx-, createWalletTx-, signOfflineTx-, getOfflineTxData-, killTxs-, reviveTx-, getPendingTxs-, deleteTx---- *Database blocks-, importMerkles-, walletBestBlock---- *Database coins and balances-, spendableCoins-, accountBalance-, addressBalances---- *Rescan-, resetRescan---- *Helpers-, InCoinData(..)-) where--import Control.Arrow (second)-import Control.Concurrent.STM (atomically)-import Control.Concurrent.STM.TBMChan (TBMChan, writeTBMChan)-import Control.Exception (throw, throwIO)-import Control.Monad (forM, forM_, unless, when)-import Control.Monad.Base (MonadBase)-import Control.Monad.Catch (MonadThrow, throwM)-import Control.Monad.Trans (MonadIO, liftIO)-import Control.Monad.Trans.Resource (MonadResource)-import Data.Either (rights)-import Data.List (find, nub, nubBy, (\\))-import qualified Data.Map.Strict as M (Map, fromListWith, map,- toList, unionWith)-import Data.Maybe (fromMaybe, isJust, isNothing,- listToMaybe, mapMaybe)-import Data.String.Conversions (cs)-import Data.Time (UTCTime, getCurrentTime)-import Data.Word (Word32, Word64)-import Database.Esqueleto (Entity (..), InnerJoin (..),- LeftOuterJoin (..), OrderBy,- SqlExpr, SqlPersistT,- SqlQuery, Value (..), asc,- case_, coalesceDefault, count,- countDistinct, countRows,- delete, desc, distinct, else_,- from, get, getBy, groupBy,- in_, just, limit, not_, on,- orderBy, replace, select, set,- sub_select, sum_, then_,- unValue, update, val, valList,- when_, where_, (!=.), (&&.),- (-.), (<.), (<=.), (=.),- (==.), (>=.), (?.), (^.),- (||.))-import qualified Database.Esqueleto as E (isNothing)-import qualified Database.Persist as P (Filter, deleteWhere,- insertBy, selectFirst)-import Network.Haskoin.Block-import Network.Haskoin.Constants-import Network.Haskoin.Crypto-import Network.Haskoin.Node.HeaderTree-import Network.Haskoin.Node.STM-import Network.Haskoin.Script-import Network.Haskoin.Transaction-import Network.Haskoin.Util-import Network.Haskoin.Wallet.Accounts-import Network.Haskoin.Wallet.Model-import Network.Haskoin.Wallet.Types---- Input coin type with transaction and address information-data InCoinData = InCoinData- { inCoinDataCoin :: !(Entity WalletCoin)- , inCoinDataTx :: !WalletTx- , inCoinDataAddr :: !WalletAddr- }--instance Coin InCoinData where- coinValue (InCoinData (Entity _ c) _ _) = walletCoinValue c---- Output coin type with address information-data OutCoinData = OutCoinData- { outCoinDataAddr :: !(Entity WalletAddr)- , outCoinDataPos :: !KeyIndex- , outCoinDataValue :: !Word64- , outCoinDataScript :: !ScriptOutput- }--{- List transactions -}---- | Get transactions.-txs :: MonadIO m- => Maybe TxConfidence- -> AccountId -- ^ Account ID- -> ListRequest -- ^ List request- -> SqlPersistT m ([WalletTx], Word32)- -- ^ List result-txs conf ai ListRequest{..} = do- [cnt] <- fmap (map unValue) $ select $ from $ \t -> do- cond t- return countRows- when (listOffset > 0 && listOffset >= cnt) $ throw $ WalletException- "Offset beyond end of data set"- res <- fmap (map entityVal) $ select $ from $ \t -> do- cond t- orderBy [ order (t ^. WalletTxId) ]- limitOffset listLimit listOffset- return t- return (res, cnt)- where- account t = t ^. WalletTxAccount ==. val ai- cond t = where_ $ case conf of- Just n -> account t &&. t ^. WalletTxConfidence ==. val n- Nothing -> account t- order = if listReverse then asc else desc--{- List transactions for an account and address -}--addrTxs :: MonadIO m- => Entity Account -- ^ Account entity- -> Entity WalletAddr -- ^ Address entity- -> ListRequest -- ^ List request- -> SqlPersistT m ([WalletTx], Word32)-addrTxs (Entity ai _) (Entity addrI WalletAddr{..}) ListRequest{..} = do- let joinSpentCoin c2 s =- c2 ?. WalletCoinAccount ==. s ?. SpentCoinAccount- &&. c2 ?. WalletCoinHash ==. s ?. SpentCoinHash- &&. c2 ?. WalletCoinPos ==. s ?. SpentCoinPos- &&. c2 ?. WalletCoinAddr ==. just (val addrI)- joinSpent s t =- s ?. SpentCoinSpendingTx ==. just (t ^. WalletTxId)- joinCoin c t =- c ?. WalletCoinTx ==. just (t ^. WalletTxId)- &&. c ?. WalletCoinAddr ==. just (val addrI)- joinAll t c c2 s = do- on $ joinSpentCoin c2 s- on $ joinSpent s t- on $ joinCoin c t- tables f = from $ \(t `LeftOuterJoin` c `LeftOuterJoin`- s `LeftOuterJoin` c2) -> f t c s c2- query t c s c2 = do- joinAll t c c2 s- where_ ( t ^. WalletTxAccount ==. val ai- &&. ( not_ (E.isNothing (c ?. WalletCoinId))- ||. not_ (E.isNothing (c2 ?. WalletCoinId))- )- )- let order = if listReverse then asc else desc- orderBy [ order (t ^. WalletTxId) ]--- cntRes <- select $ tables $ \t c s c2 -> do- query t c s c2- return $ countDistinct $ t ^. WalletTxId-- let cnt = maybe 0 unValue $ listToMaybe cntRes-- when (listOffset > 0 && listOffset >= cnt) $ throw $ WalletException- "Offset beyond end of data set"-- res <- select $ distinct $ tables $ \t c s c2 -> do- query t c s c2- limitOffset listLimit listOffset- return t-- return (map (updBals . entityVal) res, cnt)-- where- agg = sum . mapMaybe addressInfoValue .- filter ((== walletAddrAddress) . addressInfoAddress)- updBals t =- let- input = agg $ walletTxInputs t- output = agg $ walletTxOutputs t- change = agg $ walletTxChange t- in- t { walletTxInValue = output + change- , walletTxOutValue = input- }--accTxsFromBlock :: (MonadIO m, MonadThrow m)- => AccountId- -> BlockHeight- -> Word32 -- ^ Block count (0 for all)- -> SqlPersistT m [WalletTx]-accTxsFromBlock ai bh n =- fmap (map entityVal) $ select $ from $ \t -> do- query t- orderBy [ asc (t ^. WalletTxConfirmedHeight), asc (t ^. WalletTxId) ]- return t- where- query t- | n == 0 = where_ $- t ^. WalletTxAccount ==. val ai &&.- t ^. WalletTxConfirmedHeight >=. just (val bh)- | otherwise = where_ $- t ^. WalletTxAccount ==. val ai &&.- t ^. WalletTxConfirmedHeight >=. just (val bh) &&.- t ^. WalletTxConfirmedHeight <. just (val $ bh + n)---- Helper function to get a transaction from the wallet database. The function--- will look across all accounts and return the first available transaction. If--- the transaction does not exist, this function will throw a wallet exception.-getTx :: MonadIO m => TxHash -> SqlPersistT m (Maybe Tx)-getTx txid =- fmap (listToMaybe . map unValue) $ select $ from $ \t -> do- where_ $ t ^. WalletTxHash ==. val txid- limit 1- return $ t ^. WalletTxTx--getAccountTx :: MonadIO m- => AccountId -> TxHash -> SqlPersistT m WalletTx-getAccountTx ai txid = do- res <- select $ from $ \t -> do- where_ ( t ^. WalletTxAccount ==. val ai- &&. t ^. WalletTxHash ==. val txid- )- return t- case res of- (Entity _ tx:_) -> return tx- _ -> liftIO . throwIO $ WalletException $ unwords- [ "Transaction does not exist:", cs $ txHashToHex txid ]---- Helper function to get all the pending transactions from the database. It is--- used to re-broadcast pending transactions in the wallet that have not been--- included into blocks yet.-getPendingTxs :: MonadIO m => Int -> SqlPersistT m [TxHash]-getPendingTxs i =- fmap (map unValue) $ select $ from $ \t -> do- where_ $ t ^. WalletTxConfidence ==. val TxPending- when (i > 0) $ limit $ fromIntegral i- return $ t ^. WalletTxHash--{- Transaction Import -}---- | Import a transaction into the wallet from an unknown source. If the--- transaction is standard, valid, all inputs are known and all inputs can be--- spent, then the transaction will be imported as a network transaction.--- Otherwise, the transaction will be imported into the local account as an--- offline transaction.-importTx :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Tx -- ^ Transaction to import- -> Maybe (TBMChan Notif)- -> AccountId -- ^ Account ID- -> SqlPersistT m ([WalletTx], [WalletAddr])- -- ^ New transactions and addresses created-importTx tx notifChanM ai =- importTx' tx notifChanM ai =<< getInCoins tx (Just ai)--importTx' :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Tx -- ^ Transaction to import- -> Maybe (TBMChan Notif)- -> AccountId -- ^ Account ID- -> [InCoinData] -- ^ Input coins- -> SqlPersistT m ([WalletTx], [WalletAddr])- -- ^ Transaction hash (after possible merges)-importTx' origTx notifChanM ai origInCoins = do- -- Merge the transaction with any previously existing transactions- mergeResM <- mergeNoSigHashTxs ai origTx origInCoins- let tx = fromMaybe origTx mergeResM- origTxid = txHash origTx- txid = txHash tx-- -- If the transaction was merged into a new transaction,- -- update the old hashes to the new ones. This allows us to- -- keep the spending information of our coins. It is thus possible- -- to spend partially signed multisignature transactions (as offline- -- transactions) even before all signatures have arrived.- inCoins <- if origTxid == txid then return origInCoins else do- -- Update transactions- update $ \t -> do- set t [ WalletTxHash =. val txid- , WalletTxTx =. val tx- ]- where_ ( t ^. WalletTxAccount ==. val ai- &&. t ^. WalletTxHash ==. val origTxid- )- -- Update coins- update $ \t -> do- set t [ WalletCoinHash =. val txid ]- where_ ( t ^. WalletCoinAccount ==. val ai- &&. t ^. WalletCoinHash ==. val origTxid- )- let f (InCoinData c t x) = if walletTxHash t == origTxid- then InCoinData c- t{ walletTxHash = txid, walletTxTx = tx } x- else InCoinData c t x- return $ map f origInCoins-- spendingTxs <- getSpendingTxs tx (Just ai)-- let validTx = verifyStdTx tx $ map toVerDat inCoins- validIn = length inCoins == length (txIn tx)- && canSpendCoins inCoins spendingTxs False- if validIn && validTx- then importNetTx tx notifChanM- else importOfflineTx tx notifChanM ai inCoins spendingTxs- where- toVerDat (InCoinData (Entity _ c) t _) =- (walletCoinScript c, OutPoint (walletTxHash t) (walletCoinPos c))---- Offline transactions are usually multisignature transactions requiring--- additional signatures. This function will merge the signatures of--- the same offline transactions together into one single transaction.-mergeNoSigHashTxs :: MonadIO m- => AccountId- -> Tx- -> [InCoinData]- -> SqlPersistT m (Maybe Tx)-mergeNoSigHashTxs ai tx inCoins = do- prevM <- getBy $ UniqueAccNoSig ai $ nosigTxHash tx- return $ case prevM of- Just (Entity _ prev) -> case walletTxConfidence prev of- TxOffline -> eitherToMaybe $- mergeTxs [tx, walletTxTx prev] outPoints- _ -> Nothing- -- Nothing to merge. Return the original transaction.- _ -> Nothing- where- buildOutpoint c t = OutPoint (walletTxHash t) (walletCoinPos c)- f (InCoinData (Entity _ c) t _) = (walletCoinScript c, buildOutpoint c t)- outPoints = map f inCoins---- | Import an offline transaction into a specific account. Offline transactions--- are imported either manually or from the wallet when building a partially--- signed multisignature transaction. Offline transactions are only imported--- into one specific account. They will not affect the input or output coins--- of other accounts, including read-only accounts that may watch the same--- addresses as this account.------ We allow transactions to be imported manually by this function (unlike--- `importNetTx` which imports only transactions coming from the network). This--- means that it is possible to import completely crafted and invalid--- transactions into the wallet. It is thus important to limit the scope of--- those transactions to only the specific account in which it was imported.------ This function will not broadcast these transactions to the network as we--- have no idea if they are valid or not. Transactions are broadcast from the--- transaction creation function and only if the transaction is complete.-importOfflineTx- :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Tx- -> Maybe (TBMChan Notif)- -> AccountId- -> [InCoinData]- -> [Entity WalletTx]- -> SqlPersistT m ([WalletTx], [WalletAddr])-importOfflineTx tx notifChanM ai inCoins spendingTxs = do- -- Get all the new coins to be created by this transaction- outCoins <- getNewCoins tx $ Just ai- -- Only continue if the transaction is relevant to the account- when (null inCoins && null outCoins) err- -- Find the details of an existing transaction if it exists.- prevM <- fmap (fmap entityVal) $ getBy $ UniqueAccTx ai txid- -- Check if we can import the transaction- unless (canImport $ walletTxConfidence <$> prevM) err- -- Kill transactions that are spending our coins- killTxIds notifChanM $ map entityKey spendingTxs- -- Create all the transaction records for this account.- -- This will spend the input coins and create the output coins- txsRes <- buildAccTxs notifChanM tx TxOffline inCoins outCoins- -- use the addresses (refill the gap addresses)- newAddrs <- forM (nubBy sameKey $ map outCoinDataAddr outCoins) $- useAddress . entityVal- return (txsRes, concat newAddrs)- where- txid = txHash tx- canImport prevConfM =- -- We can only re-import offline txs through this function.- (isNothing prevConfM || prevConfM == Just TxOffline) &&- -- Check that all coins can be spent. We allow offline- -- coins to be spent by this function unlike importNetTx.- canSpendCoins inCoins spendingTxs True- sameKey e1 e2 = entityKey e1 == entityKey e2- err = liftIO . throwIO $ WalletException- "Could not import offline transaction"---- | Import a transaction from the network into the wallet. This function--- assumes transactions are imported in-order (parents first). It also assumes--- that the confirmations always arrive after the transaction imports. This--- function is idempotent.------ When re-importing an existing transaction, this function will recompute--- the inputs, outputs and transaction details for each account. A non-dead--- transaction could be set to dead due to new inputs being double spent.--- However, we do not allow dead transactions to be revived by reimporting them.--- Transactions can only be revived if they make it into the main chain.------ This function returns the network confidence of the imported transaction.-importNetTx- :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Tx -- Network transaction to import- -> Maybe (TBMChan Notif)- -> SqlPersistT m ([WalletTx], [WalletAddr])- -- ^ Returns the new transactions and addresses created-importNetTx tx notifChanM = do- -- Find all the coins spent by this transaction- inCoins <- getInCoins tx Nothing- -- Get all the new coins to be created by this transaction- outCoins <- getNewCoins tx Nothing- -- Only continue if the transaction is relevant to the wallet- if null inCoins && null outCoins then return ([],[]) else do- -- Update incomplete offline transactions when the completed- -- transaction comes in from the network.- updateNosigHash tx (nosigTxHash tx) txid- -- Get the transaction spending our coins- spendingTxs <- getSpendingTxs tx Nothing- -- Compute the confidence- let confidence | canSpendCoins inCoins spendingTxs False = TxPending- | otherwise = TxDead- -- Kill transactions that are spending our coins if we are not dead- when (confidence /= TxDead) $- killTxIds notifChanM $ map entityKey spendingTxs- -- Create all the transaction records for this account.- -- This will spend the input coins and create the output coins- txRes <- buildAccTxs notifChanM tx confidence inCoins outCoins- -- Use up the addresses of our new coins (replenish gap addresses)- newAddrs <- forM (nubBy sameKey $ map outCoinDataAddr outCoins) $- useAddress . entityVal- forM_ notifChanM $ \notifChan -> forM_ txRes $- \tx' -> do- let ai = walletTxAccount tx'- Account{..} <-- fromMaybe (error "Velociraptors ate you") <$> get ai- liftIO $ atomically $ writeTBMChan notifChan $- NotifTx $ toJsonTx accountName Nothing tx'- return (txRes, concat newAddrs)- where- sameKey e1 e2 = entityKey e1 == entityKey e2- txid = txHash tx--updateNosigHash :: MonadIO m => Tx -> TxHash -> TxHash -> SqlPersistT m ()-updateNosigHash tx nosig txid = do- res <- select $ from $ \t -> do- where_ ( t ^. WalletTxNosigHash ==. val nosig- &&. t ^. WalletTxHash !=. val txid- )- return $ t ^. WalletTxHash- let toUpdate = map unValue res- unless (null toUpdate) $ do- splitUpdate toUpdate $ \hs t -> do- set t [ WalletTxHash =. val txid- , WalletTxTx =. val tx- ]- where_ $ t ^. WalletTxHash `in_` valList hs- splitUpdate toUpdate $ \hs c -> do- set c [ WalletCoinHash =. val txid ]- where_ $ c ^. WalletCoinHash `in_` valList hs---- Check if the given coins can be spent.-canSpendCoins :: [InCoinData]- -> [Entity WalletTx]- -> Bool -- True for offline transactions- -> Bool-canSpendCoins inCoins spendingTxs offline =- all validCoin inCoins &&- all validSpend spendingTxs- where- -- We can only spend pending and building coins- validCoin (InCoinData _ t _)- | offline = walletTxConfidence t /= TxDead- | otherwise = walletTxConfidence t `elem` [TxPending, TxBuilding]- -- All transactions spending the same coins as us should be offline- validSpend = (== TxOffline) . walletTxConfidence . entityVal---- Get the coins in the wallet related to the inputs of a transaction. You--- can optionally provide an account to limit the returned coins to that--- account only.-getInCoins :: MonadIO m- => Tx- -> Maybe AccountId- -> SqlPersistT m [InCoinData]-getInCoins tx aiM = do- res <- splitSelect ops $ \os -> from $ \(c `InnerJoin` t `InnerJoin` x) -> do- on $ x ^. WalletAddrId ==. c ^. WalletCoinAddr- on $ t ^. WalletTxId ==. c ^. WalletCoinTx- where_ $ case aiM of- Just ai ->- c ^. WalletCoinAccount ==. val ai &&. limitOutPoints c os- _ -> limitOutPoints c os- return (c, t, x)- return $ map (\(c, t, x) -> InCoinData c (entityVal t) (entityVal x)) res- where- ops = map prevOutput $ txIn tx- limitOutPoints c os = join2 $ map (f c) os- f c (OutPoint h i) =- c ^. WalletCoinHash ==. val h &&.- c ^. WalletCoinPos ==. val i---- Find all the transactions that are spending the same coins as the given--- transaction. You can optionally provide an account to limit the returned--- transactions to that account only.-getSpendingTxs :: MonadIO m- => Tx- -> Maybe AccountId- -> SqlPersistT m [Entity WalletTx]-getSpendingTxs tx aiM- | null txInputs = return []- | otherwise =- splitSelect txInputs $ \ins -> from $ \(s `InnerJoin` t) -> do- on $ s ^. SpentCoinSpendingTx ==. t ^. WalletTxId- -- Filter out the given transaction- let cond = t ^. WalletTxHash !=. val txid- -- Limit to only the input coins of the given tx- &&. limitSpent s ins- where_ $ case aiM of- Just ai -> cond &&. s ^. SpentCoinAccount ==. val ai- _ -> cond- return t- where- txid = txHash tx- txInputs = map prevOutput $ txIn tx- limitSpent s ins = join2 $ map (f s) ins- f s (OutPoint h i) =- s ^. SpentCoinHash ==. val h &&.- s ^. SpentCoinPos ==. val i---- Returns all the new coins that need to be created from a transaction.--- Also returns the addresses associted with those coins.-getNewCoins :: MonadIO m- => Tx- -> Maybe AccountId- -> SqlPersistT m [OutCoinData]-getNewCoins tx aiM = do- -- Find all the addresses which are in the transaction outputs- addrs <- splitSelect uniqueAddrs $ \as -> from $ \x -> do- let cond = x ^. WalletAddrAddress `in_` valList as- where_ $ case aiM of- Just ai -> cond &&. x ^. WalletAddrAccount ==. val ai- _ -> cond- return x- return $ concatMap toCoins addrs- where- uniqueAddrs = nub $ map (\(addr,_,_,_) -> addr) outList- outList = rights $ map toDat txOutputs- txOutputs = zip (txOut tx) [0..]- toDat (out, pos) = getDataFromOutput out >>= \(addr, so) ->- return (addr, out, pos, so)- toCoins addrEnt@(Entity _ addr) =- let f (a,_,_,_) = a == walletAddrAddress addr- in map (toCoin addrEnt) $ filter f outList- toCoin addrEnt (_, out, pos, so) = OutCoinData- { outCoinDataAddr = addrEnt- , outCoinDataPos = pos- , outCoinDataValue = outValue out- , outCoinDataScript = so- }---- Decode an output and extract an output script and a recipient address-getDataFromOutput :: TxOut -> Either String (Address, ScriptOutput)-getDataFromOutput out = do- so <- decodeOutputBS $ scriptOutput out- addr <- outputAddress so- return (addr, so)--isCoinbaseTx :: Tx -> Bool-isCoinbaseTx tx =- length (txIn tx) == 1 && outPointHash (prevOutput $ head (txIn tx)) ==- "0000000000000000000000000000000000000000000000000000000000000000"---- | Spend the given input coins. We also create dummy coins for the inputs--- in a transaction that do not belong to us. This is to be able to detect--- double spends when reorgs occur.-spendInputs :: MonadIO m- => AccountId- -> WalletTxId- -> Tx- -> SqlPersistT m ()-spendInputs ai ti tx = do- now <- liftIO getCurrentTime- -- Spend the coins by inserting values in SpentCoin- splitInsertMany_ $ map (buildSpentCoin now) txInputs- where- txInputs = map prevOutput $ txIn tx- buildSpentCoin now (OutPoint h p) =- SpentCoin{ spentCoinAccount = ai- , spentCoinHash = h- , spentCoinPos = p- , spentCoinSpendingTx = ti- , spentCoinCreated = now- }---- Build account transaction for the given input and output coins-buildAccTxs :: MonadIO m- => Maybe (TBMChan Notif)- -> Tx- -> TxConfidence- -> [InCoinData]- -> [OutCoinData]- -> SqlPersistT m [WalletTx]-buildAccTxs notifChanM tx confidence inCoins outCoins = do- now <- liftIO getCurrentTime- -- Group the coins by account- let grouped = groupCoinsByAccount inCoins outCoins- forM (M.toList grouped) $ \(ai, (is, os)) -> do- let atx = buildAccTx tx confidence ai is os now- -- Insert the new transaction. If it already exists, update the- -- information with the newly computed values. Also make sure that the- -- confidence is set to the new value (it could have changed to TxDead).- Entity ti newAtx <- P.insertBy atx >>= \resE -> case resE of- Left (Entity ti prev) -> do- let prevConf = walletTxConfidence prev- newConf | confidence == TxDead = TxDead- | prevConf == TxBuilding = TxBuilding- | otherwise = confidence- -- If the transaction already exists, preserve confirmation data- let newAtx = atx- { walletTxConfidence = newConf- , walletTxConfirmedBy = walletTxConfirmedBy prev- , walletTxConfirmedHeight = walletTxConfirmedHeight prev- , walletTxConfirmedDate = walletTxConfirmedDate prev- }- replace ti newAtx- -- Spend inputs only if the previous transaction was dead- when (newConf /= TxDead && prevConf == TxDead) $- spendInputs ai ti tx- -- If the transaction changed from non-dead to dead, kill it.- -- This will remove spent coins and child transactions.- when (prevConf /= TxDead && newConf == TxDead) $- killTxIds notifChanM [ti]- return (Entity ti newAtx)- Right ti -> do- when (confidence /= TxDead) $ spendInputs ai ti tx- return (Entity ti atx)-- -- Insert the output coins with updated accTx key- let newOs = map (toCoin ai ti now) os- forM_ newOs $ \c -> P.insertBy c >>= \resE -> case resE of- Left (Entity ci _) -> replace ci c- _ -> return ()-- -- Return the new transaction record- return newAtx- where- toCoin ai accTxId now (OutCoinData addrEnt pos vl so) = WalletCoin- { walletCoinAccount = ai- , walletCoinHash = txHash tx- , walletCoinPos = pos- , walletCoinTx = accTxId- , walletCoinValue = vl- , walletCoinScript = so- , walletCoinAddr = entityKey addrEnt- , walletCoinCreated = now- }---- | Build an account transaction given the input and output coins relevant to--- this specific account. An account transaction contains the details of how a--- transaction affects one particular account (value sent to and from the--- account). The first value is Maybe an existing transaction in the database--- which is used to get the existing confirmation values.-buildAccTx :: Tx- -> TxConfidence- -> AccountId- -> [InCoinData]- -> [OutCoinData]- -> UTCTime- -> WalletTx-buildAccTx tx confidence ai inCoins outCoins now = WalletTx- { walletTxAccount = ai- , walletTxHash = txHash tx- -- This is a hash of the transaction excluding signatures. This allows us- -- to track the evolution of offline transactions as we add more signatures- -- to them.- , walletTxNosigHash = nosigTxHash tx- , walletTxType = txType- , walletTxInValue = inVal- , walletTxOutValue = outVal- , walletTxInputs =- let f h i (InCoinData (Entity _ c) t _) =- walletTxHash t == h && walletCoinPos c == i- toInfo (a, OutPoint h i) = case find (f h i) inCoins of- Just (InCoinData (Entity _ c) _ _) ->- AddressInfo a (Just $ walletCoinValue c) True- _ -> AddressInfo a Nothing False- in map toInfo allInAddrs- , walletTxOutputs =- let toInfo (a,i,v) = AddressInfo a (Just v) $ ours i- ours i = isJust $ find ((== i) . outCoinDataPos) outCoins- in map toInfo allOutAddrs \\ changeAddrs- , walletTxChange = changeAddrs- , walletTxTx = tx- , walletTxIsCoinbase = isCoinbaseTx tx- , walletTxConfidence = confidence- -- Reuse the confirmation information of the existing transaction if- -- we have it.- , walletTxConfirmedBy = Nothing- , walletTxConfirmedHeight = Nothing- , walletTxConfirmedDate = Nothing- , walletTxCreated = now- }- where- -- The value going into the account is the sum of the output coins- inVal = sum $ map outCoinDataValue outCoins- -- The value going out of the account is the sum on the input coins- outVal = sum $ map coinValue inCoins- allMyCoins = length inCoins == length (txIn tx) &&- length outCoins == length (txOut tx)- txType- -- If all the coins belong to the same account, it is a self- -- transaction (even if a fee was payed).- | allMyCoins = TxSelf- -- This case can happen in complex transactions where the total- -- input/output sum for a given account is 0. In this case, we count- -- that transaction as a TxSelf. This should not happen with simple- -- transactions.- | inVal == outVal = TxSelf- | inVal > outVal = TxIncoming- | otherwise = TxOutgoing- -- List of all the decodable input addresses in the transaction- allInAddrs =- let f inp = do- input <- decodeInputBS (scriptInput inp)- addr <- inputAddress input- return (addr, prevOutput inp)- in rights $ map f $ txIn tx- -- List of all the decodable output addresses in the transaction- allOutAddrs =- let f op i = do- addr <- outputAddress =<< decodeOutputBS (scriptOutput op)- return (addr, i, outValue op)- in rights $ zipWith f (txOut tx) [0..]- changeAddrs- | txType == TxIncoming = []- | otherwise =- let isInternal = (== AddressInternal) . walletAddrType- . entityVal . outCoinDataAddr- f = walletAddrAddress . entityVal . outCoinDataAddr- toInfo c = AddressInfo (f c) (Just $ outCoinDataValue c) True- in map toInfo $ filter isInternal outCoins---- Group all the input and outputs coins from the same account together.-groupCoinsByAccount- :: [InCoinData]- -> [OutCoinData]- -> M.Map AccountId ([InCoinData], [OutCoinData])-groupCoinsByAccount inCoins outCoins =- M.unionWith merge inMap outMap- where- -- Build a map from accounts -> (inCoins, outCoins)- f coin@(InCoinData _ t _) = (walletTxAccount t, [coin])- g coin = (walletAddrAccount $ entityVal $ outCoinDataAddr coin, [coin])- merge (is, _) (_, os) = (is, os)- inMap = M.map (\is -> (is, [])) $ M.fromListWith (++) $ map f inCoins- outMap = M.map (\os -> ([], os)) $ M.fromListWith (++) $ map g outCoins--deleteTx :: (MonadIO m, MonadThrow m) => TxHash -> SqlPersistT m ()-deleteTx txid = do- ts <- select $ from $ \t -> do- where_ $ t ^. WalletTxHash ==. val txid- return t- case ts of- [] -> throwM $ WalletException $ unwords- [ "Cannot delete inexistent transaction"- , cs (txHashToHex txid)- ]- Entity{entityVal = WalletTx{walletTxConfidence = TxBuilding}} : _ ->- throwM $ WalletException $ unwords- [ "Cannot delete confirmed transaction"- , cs (txHashToHex txid)- ]- _ -> return ()- children <- fmap (map unValue) $ select $ from $- \(t `InnerJoin` c `InnerJoin` s `InnerJoin` t2) -> do- on $ s ^. SpentCoinSpendingTx ==. t2 ^. WalletTxId- on ( c ^. WalletCoinAccount ==. t ^. WalletTxAccount- &&. c ^. WalletCoinHash ==. s ^. SpentCoinHash- &&. c ^. WalletCoinPos ==. s ^. SpentCoinPos- )- on $ c ^. WalletCoinTx ==. t ^. WalletTxId- where_ $ t ^. WalletTxHash ==. val txid- return $ t2 ^. WalletTxHash- forM_ children deleteTx- forM_ ts $ \Entity{entityKey = ti} ->- delete $ from $ \s -> where_ $ s ^. SpentCoinSpendingTx ==. val ti- delete $ from $ \s -> where_ $ s ^. SpentCoinHash ==. val txid- forM_ ts $ \Entity{entityKey = ti} -> do- delete $ from $ \c -> where_ $ c ^. WalletCoinTx ==. val ti- delete $ from $ \t -> where_ $ t ^. WalletTxId ==. val ti---- Kill transactions and their children by ids.-killTxIds :: MonadIO m- => Maybe (TBMChan Notif)- -> [WalletTxId]- -> SqlPersistT m ()-killTxIds notifChanM txIds = do- -- Find all the transactions spending the coins of these transactions- -- (Find all the child transactions)- children <- splitSelect txIds $ \ts -> from $ \(t `InnerJoin` s) -> do- on ( s ^. SpentCoinAccount ==. t ^. WalletTxAccount- &&. s ^. SpentCoinHash ==. t ^. WalletTxHash- )- where_ $ t ^. WalletTxId `in_` valList ts- return $ s ^. SpentCoinSpendingTx-- -- Kill these transactions- splitUpdate txIds $ \ts t -> do- set t [ WalletTxConfidence =. val TxDead ]- where_ $ t ^. WalletTxId `in_` valList ts-- case notifChanM of- Nothing -> return ()- Just notifChan -> do- ts' <- fmap (map entityVal) $- splitSelect txIds $ \ts -> from $ \t -> do- where_ $ t ^. WalletTxId `in_` valList ts- return t- forM_ ts' $ \tx -> do- let ai = walletTxAccount tx- Account{..} <-- fromMaybe (error "More velociraptors coming") <$> get ai- liftIO $ atomically $ writeTBMChan notifChan $- NotifTx $ toJsonTx accountName Nothing tx--- -- This transaction doesn't spend any coins- splitDelete txIds $ \ts -> from $ \s ->- where_ $ s ^. SpentCoinSpendingTx `in_` valList ts-- -- Recursively kill all the child transactions.- -- (Recurse at the end in case there are closed loops)- unless (null children) $ killTxIds notifChanM $ nub $ map unValue children---- Kill transactions and their child transactions by hashes.-killTxs :: MonadIO m- => Maybe (TBMChan Notif)- -> [TxHash]- -> SqlPersistT m ()-killTxs notifChanM txHashes = do- res <- splitSelect txHashes $ \hs -> from $ \t -> do- where_ $ t ^. WalletTxHash `in_` valList hs- return $ t ^. WalletTxId- killTxIds notifChanM $ map unValue res--{- Confirmations -}--importMerkles :: MonadIO m- => BlockChainAction- -> [MerkleTxs]- -> Maybe (TBMChan Notif)- -> SqlPersistT m ()-importMerkles action expTxsLs notifChanM =- when (isBestChain action || isChainReorg action) $ do- case action of- ChainReorg _ os _ ->- -- Unconfirm transactions from the old chain.- let hs = map (Just . nodeHash) os- in splitUpdate hs $ \h t -> do- set t [ WalletTxConfidence =. val TxPending- , WalletTxConfirmedBy =. val Nothing- , WalletTxConfirmedHeight =. val Nothing- , WalletTxConfirmedDate =. val Nothing- ]- where_ $ t ^. WalletTxConfirmedBy `in_` valList h- _ -> return ()-- -- Find all the dead transactions which need to be revived- deadTxs <- splitSelect (concat expTxsLs) $ \ts -> from $ \t -> do- where_ ( t ^. WalletTxHash `in_` valList ts- &&. t ^. WalletTxConfidence ==. val TxDead- )- return $ t ^. WalletTxTx-- -- Revive dead transactions (in no particular order)- forM_ deadTxs $ reviveTx notifChanM . unValue-- -- Confirm the transactions- forM_ (zip (actionNodes action) expTxsLs) $ \(node, hs) -> do- let hash = nodeHash node- height = nodeBlockHeight node-- splitUpdate hs $ \h t -> do- set t [ WalletTxConfidence =. val TxBuilding- , WalletTxConfirmedBy =. val (Just hash)- , WalletTxConfirmedHeight =.- val (Just height)- , WalletTxConfirmedDate =.- val (Just $ nodeTimestamp node)- ]- where_ $ t ^. WalletTxHash `in_` valList h-- ts <- fmap (map entityVal) $ splitSelect hs $ \h -> from $ \t -> do- where_ $ t ^. WalletTxHash `in_` valList h- return t-- -- Update the best height in the wallet (used to compute the number- -- of confirmations of transactions)- setBestBlock hash height-- -- Send notification for block- forM_ notifChanM $ \notifChan -> do- liftIO $ atomically $ writeTBMChan notifChan $- NotifBlock JsonBlock- { jsonBlockHash = hash- , jsonBlockHeight = height- , jsonBlockPrev = nodePrev node- }- sendTxs notifChan ts hash height- where- sendTxs notifChan ts hash height = forM_ ts $ \tx -> do- let ai = walletTxAccount tx- Account{..} <- fromMaybe (error "Dino crisis") <$> get ai- liftIO $ atomically $ writeTBMChan notifChan $- NotifTx $ toJsonTx accountName (Just (hash, height)) tx---- Helper function to set the best block and best block height in the DB.-setBestBlock :: MonadIO m => BlockHash -> Word32 -> SqlPersistT m ()-setBestBlock bid i = update $ \t -> set t [ WalletStateBlock =. val bid- , WalletStateHeight =. val i- ]---- Helper function to get the best block and best block height from the DB-walletBestBlock :: MonadIO m => SqlPersistT m (BlockHash, Word32)-walletBestBlock = do- cfgM <- fmap entityVal <$> P.selectFirst [] []- return $ case cfgM of- Just WalletState{..} -> (walletStateBlock, walletStateHeight)- Nothing -> throw $ WalletException $ unwords- [ "Could not get the best block."- , "Wallet database is probably not initialized"- ]---- Revive a dead transaction. All transactions that are in conflict with this--- one will be killed.-reviveTx :: MonadIO m- => Maybe (TBMChan Notif)- -> Tx- -> SqlPersistT m ()-reviveTx notifChanM tx = do- -- Kill all transactions spending our coins- spendingTxs <- getSpendingTxs tx Nothing- killTxIds notifChanM $ map entityKey spendingTxs-- -- Find all the WalletTxId that have to be revived- ids <- select $ from $ \t -> do- where_ $ t ^. WalletTxHash ==. val (txHash tx)- &&. t ^. WalletTxConfidence ==. val TxDead- return (t ^. WalletTxAccount, t ^. WalletTxId)-- -- Spend the inputs for all our transactions- forM_ ids $ \(Value ai, Value ti) -> spendInputs ai ti tx-- let ids' = map (unValue . snd) ids- -- Update the transactions- splitUpdate ids' $ \is t -> do- set t [ WalletTxConfidence =. val TxPending- , WalletTxConfirmedBy =. val Nothing- , WalletTxConfirmedHeight =. val Nothing- , WalletTxConfirmedDate =. val Nothing- ]- where_ $ t ^. WalletTxId `in_` valList is-- case notifChanM of- Nothing -> return ()- Just notifChan -> do- ts' <- fmap (map entityVal) $- splitSelect ids' $ \ts -> from $ \t -> do- where_ $ t ^. WalletTxId `in_` valList ts- return t- forM_ ts' $ \tx' -> do- let ai = walletTxAccount tx'- Account{..} <-- fromMaybe (error "Tyranossaurus Rex attacks") <$> get ai- liftIO $ atomically $ writeTBMChan notifChan $- NotifTx $ toJsonTx accountName Nothing tx'--{- Transaction creation and signing (local wallet functions) -}---- | Create a transaction sending some coins to a list of recipient addresses.-createWalletTx- :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account -- ^ Account Entity- -> Maybe (TBMChan Notif) -- ^ Notification channel- -> Maybe XPrvKey -- ^ Key if not provided by account- -> [(Address,Word64)] -- ^ List of recipient addresses and amounts- -> Word64 -- ^ Fee per 1000 bytes- -> Word32 -- ^ Minimum confirmations- -> Bool -- ^ Should fee be paid by recipient- -> Bool -- ^ Should the transaction be signed- -> SqlPersistT m (WalletTx, [WalletAddr])- -- ^ (New transaction hash, Completed flag)-createWalletTx accE@(Entity ai acc) notifM masterM dests fee minConf rcptFee sign = do- -- Build an unsigned transaction from the given recipient values and fee- (unsignedTx, inCoins, newChangeAddrs) <-- buildUnsignedTx accE dests fee minConf rcptFee- -- Sign our new transaction if signing was requested- let dat = map toCoinSignData inCoins- tx | sign = signOfflineTx acc masterM unsignedTx dat- | otherwise = unsignedTx- -- Import the transaction in the wallet either as a network transaction if- -- it is complete, or as an offline transaction otherwise.- (res, newAddrs) <- importTx' tx notifM ai inCoins- case res of- (txRes:_) -> return (txRes, newAddrs ++ newChangeAddrs)- _ -> liftIO . throwIO $ WalletException- "Error while importing the new transaction"--toCoinSignData :: InCoinData -> CoinSignData-toCoinSignData (InCoinData (Entity _ c) t x) =- CoinSignData (OutPoint (walletTxHash t) (walletCoinPos c))- (walletCoinScript c)- deriv- where- deriv = Deriv :/ addrTypeIndex (walletAddrType x) :/ walletAddrIndex x---- Build an unsigned transaction given a list of recipients and a fee. Returns--- the unsigned transaction together with the input coins that have been--- selected or spending.-buildUnsignedTx- :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account- -> [(Address, Word64)]- -> Word64- -> Word32- -> Bool- -> SqlPersistT m (Tx, [InCoinData], [WalletAddr])- -- ^ Generated change addresses-buildUnsignedTx _ [] _ _ _ = liftIO . throwIO $ WalletException- "buildUnsignedTx: No transaction recipients have been provided"-buildUnsignedTx accE@(Entity ai acc) origDests origFee minConf rcptFee = do- let p = case accountType acc of- AccountMultisig m n -> (m, n)- _ -> throw . WalletException $ "Invalid account type"- fee = if rcptFee then 0 else origFee- coins | isMultisigAccount acc = chooseMSCoins tot fee p True- | otherwise = chooseCoins tot fee True- -- TODO: Add more policies like confirmations or coin age- -- Sort coins by their values in descending order- orderPolicy c _ = [desc $ c ^. WalletCoinValue]- -- Find the spendable coins in the given account with the required number- -- of minimum confirmations.- selectRes <- spendableCoins ai minConf orderPolicy- -- Find a selection of spendable coins that matches our target value- let (selected, change) = either (throw . WalletException) id $ coins selectRes- totFee | isMultisigAccount acc = getMSFee origFee p (length selected)- | otherwise = getFee origFee (length selected)- -- Subtract fees from first destination if rcptFee- value = snd $ head origDests-- -- First output must not be dust after deducting fees- when (rcptFee && value < totFee + 5430) $ throw $ WalletException- "First recipient cannot cover transaction fees"-- -- Subtract fees from first destination if rcptFee- let dests | rcptFee =- second (const $ value - totFee) (head origDests) :- tail origDests- | otherwise = origDests-- -- Make sure the first recipient has enough funds to cover the fee- when (snd (head dests) <= 0) $ throw $- WalletException "Transaction fees too high"-- -- If the change amount is not dust, we need to add a change address to- -- our list of recipients.- -- TODO: Put the dust value in a constant somewhere. We also need a more- -- general way of detecting dust such as our transactions are not- -- rejected by full nodes.- (allDests, addrs) <- if change < 5430- then return (dests, [])- else do- (addr, chng) <- newChangeAddr change- return ((walletAddrAddress addr, chng) : dests, [addr])-- case buildAddrTx (map toOutPoint selected) $ map toBase58 allDests of- Right tx -> return (tx, selected, addrs)- Left err -> liftIO . throwIO $ WalletException err- where- tot = sum $ map snd origDests- toBase58 (a, v) = (addrToBase58 a, v)- toOutPoint (InCoinData (Entity _ c) t _) =- OutPoint (walletTxHash t) (walletCoinPos c)- newChangeAddr change = do- let lq = ListRequest 0 0 False- (as, _) <- unusedAddresses accE AddressInternal lq- case as of- (a:_) -> do- -- Use the address to prevent reusing it again- _ <- useAddress a- -- TODO: Randomize the change position- return (a, change)- _ -> liftIO . throwIO $ WalletException- "No unused addresses available"--signAccountTx :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => Entity Account- -> Maybe (TBMChan Notif)- -> Maybe XPrvKey- -> TxHash- -> SqlPersistT m ([WalletTx], [WalletAddr])-signAccountTx (Entity ai acc) notifChanM masterM txid = do- (OfflineTxData tx dat, inCoins) <- getOfflineTxData ai txid- let signedTx = signOfflineTx acc masterM tx dat- importTx' signedTx notifChanM ai inCoins--getOfflineTxData- :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => AccountId- -> TxHash- -> SqlPersistT m (OfflineTxData, [InCoinData])-getOfflineTxData ai txid = do- txM <- getBy $ UniqueAccTx ai txid- case txM of- Just (Entity _ tx) -> do- unless (walletTxConfidence tx == TxOffline) $ liftIO . throwIO $- WalletException "Can only sign offline transactions."- inCoins <- getInCoins (walletTxTx tx) $ Just ai- return- ( OfflineTxData (walletTxTx tx) $ map toCoinSignData inCoins- , inCoins- )- _ -> liftIO . throwIO $ WalletException $ unwords- [ "Invalid txid", cs $ txHashToHex txid ]---- Sign a transaction using a list of CoinSignData. This allows an offline--- signer without access to the coins to sign a given transaction.-signOfflineTx :: Account -- ^ Account used for signing- -> Maybe XPrvKey -- ^ Key if not provided in account- -> Tx -- ^ Transaction to sign- -> [CoinSignData] -- ^ Input signing data- -> Tx-signOfflineTx acc masterM tx coinSignData- | not validMaster = throw $ WalletException- "Master key not valid"- -- Sign the transaction deterministically- | otherwise = either (throw . WalletException) id $- signTx tx sigData $ map (toPrvKeyG . xPrvKey) prvKeys- where- -- Compute all the SigInputs- sigData = map (toSigData acc) coinSignData- -- Compute all the private keys- prvKeys = map toPrvKey coinSignData- -- Build a SigInput from a CoinSignData- toSigData acc' (CoinSignData op so deriv) =- -- TODO: Here we override the SigHash to be SigAll False all the time.- -- Should we be more flexible?- SigInput so op (SigAll False) $- if isMultisigAccount acc- then Just $ getPathRedeem acc' deriv- else Nothing- toPrvKey (CoinSignData _ _ deriv) = derivePath deriv master- master = case masterM of- Just m -> case accountDerivation acc of- Just d -> derivePath d m- Nothing -> m- Nothing -> fromMaybe- (throw $ WalletException "No extended private key available")- (accountMaster acc)- validMaster = deriveXPubKey master `elem` accountKeys acc---- Returns unspent coins that can be spent in an account that have a minimum--- number of confirmations. Coinbase coins can only be spent after 100--- confirmations.-spendableCoins- :: (MonadIO m, MonadThrow m, MonadBase IO m, MonadResource m)- => AccountId -- ^ Account key- -> Word32 -- ^ Minimum confirmations- -> ( SqlExpr (Entity WalletCoin)- -> SqlExpr (Entity WalletTx)- -> [SqlExpr OrderBy]- )- -- ^ Coin ordering policy- -> SqlPersistT m [InCoinData] -- ^ Spendable coins-spendableCoins ai minConf orderPolicy =- fmap (map f) $ select $ spendableCoinsFrom ai minConf orderPolicy- where- f (c, t, x) = InCoinData c (entityVal t) (entityVal x)--spendableCoinsFrom- :: AccountId -- ^ Account key- -> Word32 -- ^ Minimum confirmations- -> ( SqlExpr (Entity WalletCoin)- -> SqlExpr (Entity WalletTx)- -> [SqlExpr OrderBy]- )- -- ^ Coin ordering policy- -> SqlQuery ( SqlExpr (Entity WalletCoin)- , SqlExpr (Entity WalletTx)- , SqlExpr (Entity WalletAddr)- )-spendableCoinsFrom ai minConf orderPolicy =- from $ \(c `InnerJoin` t `InnerJoin` x `LeftOuterJoin` s) -> do- -- Joins have to be set in reverse order !- -- Left outer join on spent coins- on ( s ?. SpentCoinAccount ==. just (c ^. WalletCoinAccount)- &&. s ?. SpentCoinHash ==. just (c ^. WalletCoinHash)- &&. s ?. SpentCoinPos ==. just (c ^. WalletCoinPos)- )- on $ x ^. WalletAddrId ==. c ^. WalletCoinAddr- -- Inner join on coins and transactions- on $ t ^. WalletTxId ==. c ^. WalletCoinTx- where_ ( c ^. WalletCoinAccount ==. val ai- &&. t ^. WalletTxConfidence- `in_` valList [ TxPending, TxBuilding ]- -- We only want unspent coins- &&. E.isNothing (s ?. SpentCoinId)- &&. limitConfirmations (Right t) minConf- )- orderBy (orderPolicy c t)- return (c, t, x)---- If the current height is 200 and a coin was confirmed at height 198, then it--- has 3 confirmations. So, if we require 3 confirmations, we want coins with a--- confirmed height of 198 or less (200 - 3 + 1).-limitConfirmations :: Either (SqlExpr (Maybe (Entity WalletTx)))- (SqlExpr (Entity WalletTx))- -> Word32- -> SqlExpr (Value Bool)-limitConfirmations txE minconf- | minconf == 0 = limitCoinbase- | minconf < 100 = limitConfs minconf &&. limitCoinbase- | otherwise = limitConfs minconf- where- limitConfs i = case txE of- Left t -> t ?. WalletTxConfirmedHeight- <=. just (just (selectHeight -. val (i - 1)))- Right t -> t ^. WalletTxConfirmedHeight- <=. just (selectHeight -. val (i - 1))- -- Coinbase transactions require 100 confirmations- limitCoinbase = case txE of- Left t ->- not_ (coalesceDefault [t ?. WalletTxIsCoinbase] (val False)) ||.- limitConfs 100- Right t ->- not_ (t ^. WalletTxIsCoinbase) ||. limitConfs 100- selectHeight :: SqlExpr (Value Word32)- selectHeight = sub_select $ from $ \co -> do- limit 1- return $ co ^. WalletStateHeight--{- Balances -}--accountBalance :: MonadIO m- => AccountId- -> Word32- -> Bool- -> SqlPersistT m Word64-accountBalance ai minconf offline = do- res <- select $ from $ \(c `InnerJoin`- t `LeftOuterJoin` s `LeftOuterJoin` st) -> do- on $ st ?. WalletTxId ==. s ?. SpentCoinSpendingTx- on ( s ?. SpentCoinAccount ==. just (c ^. WalletCoinAccount)- &&. s ?. SpentCoinHash ==. just (c ^. WalletCoinHash)- &&. s ?. SpentCoinPos ==. just (c ^. WalletCoinPos)- )- on $ t ^. WalletTxId ==. c ^. WalletCoinTx- let unspent = E.isNothing ( s ?. SpentCoinId )- spentOffline = st ?. WalletTxConfidence ==. just (val TxOffline)- cond = c ^. WalletCoinAccount ==. val ai- &&. t ^. WalletTxConfidence `in_` valList validConfidence- -- For non-offline balances, we have to take into account- -- the coins which are spent by offline transactions.- &&. if offline then unspent else unspent ||. spentOffline- where_ $ if minconf == 0- then cond- else cond &&. limitConfirmations (Right t) minconf- return $ sum_ (c ^. WalletCoinValue)- case res of- (Value (Just s):_) -> return $ floor (s :: Double)- _ -> return 0- where- validConfidence = TxPending : TxBuilding : [ TxOffline | offline ]--addressBalances :: MonadIO m- => Entity Account- -> KeyIndex- -> KeyIndex- -> AddressType- -> Word32- -> Bool- -> SqlPersistT m [(KeyIndex, BalanceInfo)]-addressBalances accE@(Entity ai _) iMin iMax addrType minconf offline = do- -- We keep our joins flat to improve performance in SQLite.- res <- select $ from $ \(x `LeftOuterJoin` c `LeftOuterJoin`- t `LeftOuterJoin` s `LeftOuterJoin` st) -> do- let joinCond = st ?. WalletTxId ==. s ?. SpentCoinSpendingTx- -- Do not join the spending information for offline transactions if we- -- request the online balances. This will count the coin as unspent.- on $ if offline- then joinCond- else joinCond &&.- st ?. WalletTxConfidence !=. just (val TxOffline)- on $ s ?. SpentCoinAccount ==. c ?. WalletCoinAccount- &&. s ?. SpentCoinHash ==. c ?. WalletCoinHash- &&. s ?. SpentCoinPos ==. c ?. WalletCoinPos- let txJoin = t ?. WalletTxId ==. c ?. WalletCoinTx- &&. t ?. WalletTxConfidence `in_` valList validConfidence- on $ if minconf == 0- then txJoin- else txJoin &&. limitConfirmations (Left t) minconf- on $ c ?. WalletCoinAddr ==. just (x ^. WalletAddrId)- let limitIndex- | iMin == iMax = x ^. WalletAddrIndex ==. val iMin- | otherwise = x ^. WalletAddrIndex >=. val iMin- &&. x ^. WalletAddrIndex <=. val iMax- where_ ( x ^. WalletAddrAccount ==. val ai- &&. limitIndex- &&. x ^. WalletAddrIndex <. subSelectAddrCount accE addrType- &&. x ^. WalletAddrType ==. val addrType- )- groupBy $ x ^. WalletAddrIndex- let unspent = E.isNothing $ st ?. WalletTxId- invalidTx = E.isNothing $ t ?. WalletTxId- return ( x ^. WalletAddrIndex -- Address index- , sum_ $ case_- [ when_ invalidTx- then_ (val (Just 0))- ] (else_ $ c ?. WalletCoinValue) -- Out value- , sum_ $ case_- [ when_ (unspent ||. invalidTx)- then_ (val (Just 0))- ] (else_ $ c ?. WalletCoinValue) -- Out value- , count $ t ?. WalletTxId -- New coins- , count $ case_- [ when_ invalidTx- then_ (val Nothing)- ] (else_ $ st ?. WalletTxId) -- Spent coins- )- return $ map f res- where- validConfidence = Just TxPending : Just TxBuilding :- [ Just TxOffline | offline ]- f (Value i, Value inM, Value outM, Value newC, Value spentC) =- let b = BalanceInfo- { balanceInfoInBalance =- floor $ fromMaybe (0 :: Double) inM- , balanceInfoOutBalance =- floor $ fromMaybe (0 :: Double) outM- , balanceInfoCoins = newC- , balanceInfoSpentCoins = spentC- }- in (i, b)--{- Rescans -}--resetRescan :: MonadIO m => SqlPersistT m ()-resetRescan = do- P.deleteWhere ([] :: [P.Filter WalletCoin])- P.deleteWhere ([] :: [P.Filter SpentCoin])- P.deleteWhere ([] :: [P.Filter WalletTx])- setBestBlock (headerHash genesisHeader) 0-
@@ -1,790 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}-module Network.Haskoin.Wallet.Types-( AccountName---- JSON Types-, JsonAccount(..)-, JsonAddr(..)-, JsonCoin(..)-, JsonTx(..)---- Request Types-, WalletRequest(..)-, ListRequest(..)-, NewAccount(..)-, SetAccountGap(..)-, OfflineTxData(..)-, CoinSignData(..)-, TxAction(..)-, AddressLabel(..)-, NodeAction(..)-, AccountType(..)-, AddressType(..)-, addrTypeIndex-, TxType(..)-, TxConfidence(..)-, AddressInfo(..)-, BalanceInfo(..)---- Response Types-, WalletResponse(..)-, TxCompleteRes(..)-, ListResult(..)-, RescanRes(..)-, JsonSyncBlock(..)-, JsonBlock(..)-, Notif(..)-, BlockInfo(..)---- Helper Types-, WalletException(..)-, BTCNode(..)---- *Helpers-, splitSelect-, splitUpdate-, splitDelete-, splitInsertMany_-, join2-, limitOffset-) where--import Control.DeepSeq (NFData (..))-import Control.Exception (Exception)-import Control.Monad (forM, forM_, mzero, when)-import Control.Monad.Trans (MonadIO)-import Data.Aeson (FromJSON, ToJSON, Value (..),- decodeStrict', object,- parseJSON, toJSON, withObject,- (.!=), (.:), (.:?), (.=))-import qualified Data.Aeson as Aeson-import Data.Aeson.TH (deriveJSON)-import Data.Aeson.Types (Options (..),- SumEncoding (..),- defaultOptions,- defaultTaggedObject)-import Data.Char (toLower)-import Data.Int (Int64)-import Data.List.Split (chunksOf)-import Data.Maybe (maybeToList)-import Data.Serialize (Serialize, decode, encode)-import Data.String (fromString)-import Data.String.Conversions (cs)-import Data.Text (Text)-import Data.Time (UTCTime)-import Data.Typeable (Typeable)-import Data.Word (Word32, Word64)-import Database.Esqueleto (Entity (..), SqlBackend,- SqlExpr, SqlPersistT,- SqlQuery, limit, offset,- select, update, val, (||.))-import qualified Database.Esqueleto as E (Value, delete)-import Database.Esqueleto.Internal.Sql (SqlSelect)-import qualified Database.Persist as P (PersistEntity,- PersistEntityBackend,- insertMany_)-import Database.Persist.Class (PersistField,- fromPersistValue,- toPersistValue)-import Database.Persist.Sql (PersistFieldSql, SqlType (..),- sqlType)-import Database.Persist.Types (PersistValue (..))-import GHC.Generics (Generic)-import Network.Haskoin.Block-import Network.Haskoin.Crypto-import Network.Haskoin.Node-import Network.Haskoin.Node.HeaderTree-import Network.Haskoin.Script-import Network.Haskoin.Transaction-import Network.Haskoin.Util-import Network.Haskoin.Wallet.Database-import Network.Haskoin.Wallet.Types.BlockInfo--type AccountName = Text---- TODO: Add NFData instances for all those types--{- Request Types -}--data TxType- = TxIncoming- | TxOutgoing- | TxSelf- deriving (Eq, Show, Read)--instance NFData TxType where- rnf x = x `seq` ()--$(deriveJSON (dropSumLabels 2 0 "") ''TxType)--data TxConfidence- = TxOffline- | TxDead- | TxPending- | TxBuilding- deriving (Eq, Show, Read)--instance NFData TxConfidence where- rnf x = x `seq` ()--$(deriveJSON (dropSumLabels 2 0 "") ''TxConfidence)--data AddressInfo = AddressInfo- { addressInfoAddress :: !Address- , addressInfoValue :: !(Maybe Word64)- , addressInfoIsLocal :: !Bool- }- deriving (Eq, Show, Read, Generic)--instance Serialize AddressInfo--$(deriveJSON (dropFieldLabel 11) ''AddressInfo)--instance NFData AddressInfo where- rnf AddressInfo{..} =- rnf addressInfoAddress `seq`- rnf addressInfoValue `seq`- rnf addressInfoIsLocal--data BalanceInfo = BalanceInfo- { balanceInfoInBalance :: !Word64- , balanceInfoOutBalance :: !Word64- , balanceInfoCoins :: !Int- , balanceInfoSpentCoins :: !Int- }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 11) ''BalanceInfo)--instance NFData BalanceInfo where- rnf BalanceInfo{..} =- rnf balanceInfoInBalance `seq`- rnf balanceInfoOutBalance `seq`- rnf balanceInfoCoins `seq`- rnf balanceInfoSpentCoins--data AccountType- = AccountRegular- | AccountMultisig- { accountTypeRequiredSigs :: !Int- , accountTypeTotalKeys :: !Int- }- deriving (Eq, Show, Read)--instance NFData AccountType where- rnf t = case t of- AccountRegular -> ()- AccountMultisig m n -> rnf m `seq` rnf n--instance ToJSON AccountType where- toJSON accType = case accType of- AccountRegular -> object- [ "type" .= String "regular" ]- AccountMultisig m n -> object- [ "type" .= String "multisig"- , "requiredsigs" .= m- , "totalkeys" .= n- ]--instance FromJSON AccountType where- parseJSON = withObject "AccountType" $ \o ->- o .: "type" >>= \t -> case (t :: Text) of- "regular" -> return AccountRegular- "multisig" -> AccountMultisig <$> o .: "requiredsigs"- <*> o .: "totalkeys"- _ -> mzero--data NewAccount = NewAccount- { newAccountName :: !AccountName- , newAccountType :: !AccountType- , newAccountMnemonic :: !(Maybe Text)- , newAccountMaster :: !(Maybe XPrvKey)- , newAccountDeriv :: !(Maybe HardPath)- , newAccountKeys :: ![XPubKey]- , newAccountReadOnly :: !Bool- }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 10) ''NewAccount)--data SetAccountGap = SetAccountGap { getAccountGap :: !Word32 }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 10) ''SetAccountGap)--data ListRequest = ListRequest- { listOffset :: !Word32- , listLimit :: !Word32- , listReverse :: !Bool- }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 4) ''ListRequest)--data CoinSignData = CoinSignData- { coinSignOutPoint :: !OutPoint- , coinSignScriptOutput :: !ScriptOutput- , coinSignDeriv :: !SoftPath- }- deriving (Eq, Show)--$(deriveJSON (dropFieldLabel 8) ''CoinSignData)--data OfflineTxData = OfflineTxData- { offlineTxDataTx :: !Tx- , offlineTxDataCoinData :: ![CoinSignData]- }--$(deriveJSON (dropFieldLabel 13) ''OfflineTxData)--data TxAction- = CreateTx- { accTxActionRecipients :: ![(Address, Word64)]- , accTxActionFee :: !Word64- , accTxActionMinConf :: !Word32- , accTxActionRcptFee :: !Bool- , accTxActionSign :: !Bool- }- | ImportTx- { accTxActionTx :: !Tx }- | SignTx- { accTxActionHash :: !TxHash }- deriving (Eq, Show)--instance ToJSON TxAction where- toJSON (CreateTx recipients fee minConf rcptFee sign) = object $- [ "type" .= ("createtx" :: Text)- , "recipients" .= recipients- , "fee" .= fee- , "minconf" .= minConf- , "sign" .= sign- ] ++ [ "rcptfee" .= True | rcptFee ]- toJSON (ImportTx tx) = object- [ "type" .= ("importtx" :: Text)- , "tx" .= tx- ]- toJSON (SignTx txid) = object- [ "type" .= ("signtx" :: Text)- , "hash" .= txid- ]--instance FromJSON TxAction where- parseJSON = withObject "TxAction" $ \o -> do- t <- o .: "type"- case (t :: Text) of- "createtx" -> do- recipients <- o .: "recipients"- fee <- o .: "fee"- minConf <- o .: "minconf"- sign <- o .: "sign"- rcptFee <- o .:? "rcptfee" .!= False- return (CreateTx recipients fee minConf rcptFee sign)- "importtx" -> do- tx <- o .: "tx"- return (ImportTx tx)- "signtx" -> do- txid <- o .: "hash"- return (SignTx txid)- _ -> mzero--data AddressLabel = AddressLabel { addressLabelLabel :: !Text }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 12) ''AddressLabel)--data NodeAction- = NodeActionRescan { nodeActionTimestamp :: !(Maybe Word32) }- | NodeActionStatus- deriving (Eq, Show, Read)--instance ToJSON NodeAction where- toJSON na = case na of- NodeActionRescan tM -> object $- ("type" .= String "rescan") : (("timestamp" .=) <$> maybeToList tM)- NodeActionStatus -> object [ "type" .= String "status" ]--instance FromJSON NodeAction where- parseJSON = withObject "NodeAction" $ \o -> do- String t <- o .: "type"- case t of- "rescan" -> NodeActionRescan <$> o .:? "timestamp"- "status" -> return NodeActionStatus- _ -> mzero--data AddressType- = AddressInternal- | AddressExternal- deriving (Eq, Show, Read)--$(deriveJSON (dropSumLabels 7 0 "") ''AddressType)--instance NFData AddressType where- rnf x = x `seq` ()--addrTypeIndex :: AddressType -> KeyIndex-addrTypeIndex AddressExternal = 0-addrTypeIndex AddressInternal = 1--data WalletRequest- = GetAccountsR !ListRequest- | PostAccountsR !NewAccount- | PostAccountRenameR !AccountName !AccountName- | GetAccountR !AccountName- | PostAccountKeysR !AccountName ![XPubKey]- | PostAccountGapR !AccountName !SetAccountGap- | GetAddressesR !AccountName !AddressType !Word32 !Bool !ListRequest- | GetAddressesUnusedR !AccountName !AddressType !ListRequest- | GetAddressR !AccountName !KeyIndex !AddressType !Word32 !Bool- | GetIndexR !AccountName !PubKeyC !AddressType- | PutAddressR !AccountName !KeyIndex !AddressType !AddressLabel- | PostAddressesR !AccountName !KeyIndex !AddressType- | GetTxsR !AccountName !ListRequest- | GetAddrTxsR !AccountName !KeyIndex !AddressType !ListRequest- | PostTxsR !AccountName !(Maybe XPrvKey) !TxAction- | GetTxR !AccountName !TxHash- | GetOfflineTxR !AccountName !TxHash- | PostOfflineTxR !AccountName !(Maybe XPrvKey) !Tx ![CoinSignData]- | GetBalanceR !AccountName !Word32 !Bool- | PostNodeR !NodeAction- | DeleteTxIdR !TxHash- | GetSyncR !AccountName !BlockHash !ListRequest- | GetSyncHeightR !AccountName !BlockHeight !ListRequest- | GetPendingR !AccountName !ListRequest- | GetDeadR !AccountName !ListRequest- | GetBlockInfoR ![BlockHash]- | StopServerR- deriving (Show, Eq)---- TODO: Set omitEmptyContents on aeson-0.9-$(deriveJSON- defaultOptions- { constructorTagModifier = map toLower . init- , sumEncoding = defaultTaggedObject- { tagFieldName = "method"- , contentsFieldName = "request"- }- }- ''WalletRequest- )--{- JSON Types -}--data JsonAccount = JsonAccount- { jsonAccountName :: !Text- , jsonAccountType :: !AccountType- , jsonAccountMaster :: !(Maybe XPrvKey)- , jsonAccountMnemonic :: !(Maybe Text)- , jsonAccountDerivation :: !(Maybe HardPath)- , jsonAccountKeys :: ![XPubKey]- , jsonAccountGap :: !Word32- , jsonAccountCreated :: !UTCTime- }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 11) ''JsonAccount)--data JsonAddr = JsonAddr- { jsonAddrAddress :: !Address- , jsonAddrIndex :: !KeyIndex- , jsonAddrType :: !AddressType- , jsonAddrLabel :: !Text- , jsonAddrRedeem :: !(Maybe ScriptOutput)- , jsonAddrKey :: !(Maybe PubKeyC)- , jsonAddrCreated :: !UTCTime- -- Optional Balance- , jsonAddrBalance :: !(Maybe BalanceInfo)- }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 8) ''JsonAddr)--data JsonTx = JsonTx- { jsonTxHash :: !TxHash- , jsonTxNosigHash :: !TxHash- , jsonTxType :: !TxType- , jsonTxInValue :: !Word64- , jsonTxOutValue :: !Word64- , jsonTxValue :: !Int64- , jsonTxInputs :: ![AddressInfo]- , jsonTxOutputs :: ![AddressInfo]- , jsonTxChange :: ![AddressInfo]- , jsonTxTx :: !Tx- , jsonTxIsCoinbase :: !Bool- , jsonTxConfidence :: !TxConfidence- , jsonTxConfirmedBy :: !(Maybe BlockHash)- , jsonTxConfirmedHeight :: !(Maybe Word32)- , jsonTxConfirmedDate :: !(Maybe Word32)- , jsonTxCreated :: !UTCTime- , jsonTxAccount :: !AccountName- -- Optional confirmation- , jsonTxConfirmations :: !(Maybe Word32)- , jsonTxBestBlock :: !(Maybe BlockHash)- , jsonTxBestBlockHeight :: !(Maybe BlockHeight)- }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 6) ''JsonTx)--data JsonCoin = JsonCoin- { jsonCoinHash :: !TxHash- , jsonCoinPos :: !Word32- , jsonCoinValue :: !Word64- , jsonCoinScript :: !ScriptOutput- , jsonCoinCreated :: !UTCTime- -- Optional Tx- , jsonCoinTx :: !(Maybe JsonTx)- -- Optional Address- , jsonCoinAddress :: !(Maybe JsonAddr)- -- Optional spender- , jsonCoinSpendingTx :: !(Maybe JsonTx)- }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 8) ''JsonCoin)--{- Response Types -}--data TxCompleteRes = TxCompleteRes- { txCompleteTx :: !Tx- , txCompleteComplete :: !Bool- } deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 10) ''TxCompleteRes)--data ListResult a = ListResult- { listResultItems :: ![a]- , listResultTotal :: !Word32- }--$(deriveJSON (dropFieldLabel 10) ''ListResult)--data RescanRes = RescanRes { rescanTimestamp :: !Word32 }- deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 6) ''RescanRes)--data WalletResponse a- = ResponseError { responseError :: !Text }- | ResponseValid { responseResult :: !(Maybe a) }- deriving (Eq, Show)--$(deriveJSON (dropSumLabels 8 8 "status") ''WalletResponse)--data JsonSyncBlock = JsonSyncBlock- { jsonSyncBlockHash :: !BlockHash- , jsonSyncBlockHeight :: !BlockHeight- , jsonSyncBlockPrev :: !BlockHash- , jsonSyncBlockTxs :: ![JsonTx]- } deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 13) ''JsonSyncBlock)--data JsonBlock = JsonBlock- { jsonBlockHash :: !BlockHash- , jsonBlockHeight :: !BlockHeight- , jsonBlockPrev :: !BlockHash- } deriving (Eq, Show, Read)--$(deriveJSON (dropFieldLabel 9) ''JsonBlock)--data Notif- = NotifBlock !JsonBlock- | NotifTx !JsonTx- deriving (Eq, Show, Read)--$(deriveJSON (dropSumLabels 5 5 "type") ''Notif)--{- Helper Types -}--data WalletException = WalletException !String- deriving (Eq, Read, Show, Typeable)--instance Exception WalletException--data BTCNode = BTCNode { btcNodeHost :: !String, btcNodePort :: !Int }- deriving (Eq, Read, Show)--$(deriveJSON (dropFieldLabel 7) ''BTCNode)--{- Persistent Instances -}--instance PersistField XPrvKey where- toPersistValue = PersistText . cs . xPrvExport- fromPersistValue (PersistText txt) =- maybeToEither "Invalid Persistent XPrvKey" $ xPrvImport $ cs txt- fromPersistValue (PersistByteString bs) =- maybeToEither "Invalid Persistent XPrvKey" $ xPrvImport bs- fromPersistValue _ = Left "Invalid Persistent XPrvKey"--instance PersistFieldSql XPrvKey where- sqlType _ = SqlString--instance PersistField [XPubKey] where- toPersistValue = PersistText . cs . Aeson.encode- fromPersistValue (PersistText txt) =- maybeToEither "Invalid Persistent XPubKey" $ decodeStrict' $ cs txt- fromPersistValue (PersistByteString bs) =- maybeToEither "Invalid Persistent XPubKey" $ decodeStrict' bs- fromPersistValue _ = Left "Invalid Persistent XPubKey"--instance PersistFieldSql [XPubKey] where- sqlType _ = SqlString--instance PersistField DerivPath where- toPersistValue = PersistText . cs . pathToStr- fromPersistValue (PersistText txt) = maybeToEither- "Invalid Persistent DerivPath" . fmap getParsedPath .- parsePath . cs $ txt- fromPersistValue (PersistByteString bs) = maybeToEither- "Invalid Persistent DerivPath" . fmap getParsedPath .- parsePath . cs $ bs- fromPersistValue _ = Left "Invalid Persistent DerivPath"--instance PersistFieldSql DerivPath where- sqlType _ = SqlString--instance PersistField HardPath where- toPersistValue = PersistText . cs . pathToStr- fromPersistValue (PersistText txt) = maybeToEither- "Invalid Persistent HardPath" $ parseHard $ cs txt- fromPersistValue (PersistByteString bs) = maybeToEither- "Invalid Persistent HardPath" $ parseHard $ cs bs- fromPersistValue _ = Left "Invalid Persistent HardPath"--instance PersistFieldSql HardPath where- sqlType _ = SqlString--instance PersistField SoftPath where- toPersistValue = PersistText . cs . pathToStr- fromPersistValue (PersistText txt) = maybeToEither- "Invalid Persistent SoftPath" $ parseSoft $ cs txt- fromPersistValue (PersistByteString bs) = maybeToEither- "Invalid Persistent SoftPath" $ parseSoft $ cs bs- fromPersistValue _ = Left "Invalid Persistent SoftPath"--instance PersistFieldSql SoftPath where- sqlType _ = SqlString--instance PersistField AccountType where- toPersistValue = PersistText . cs . Aeson.encode- fromPersistValue (PersistText txt) = maybeToEither- "Invalid Persistent AccountType" $ decodeStrict' $ cs txt- fromPersistValue (PersistByteString bs) = maybeToEither- "Invalid Persistent AccountType" $ decodeStrict' bs- fromPersistValue _ = Left "Invalid Persistent AccountType"--instance PersistFieldSql AccountType where- sqlType _ = SqlString--instance PersistField AddressType where- toPersistValue ts = PersistBool $ case ts of- AddressExternal -> True- AddressInternal -> False-- fromPersistValue (PersistBool b) = return $- if b then AddressExternal else AddressInternal-- fromPersistValue (PersistInt64 i) = return $ case i of- 0 -> AddressInternal- _ -> AddressExternal-- fromPersistValue _ = Left "Invalid Persistent AddressType"--instance PersistFieldSql AddressType where- sqlType _ = SqlBool--instance PersistField TxType where- toPersistValue ts = PersistText $ case ts of- TxIncoming -> "incoming"- TxOutgoing -> "outgoing"- TxSelf -> "self"-- fromPersistValue (PersistText txt) = case txt of- "incoming" -> return TxIncoming- "outgoing" -> return TxOutgoing- "self" -> return TxSelf- _ -> Left "Invalid Persistent TxType"-- fromPersistValue (PersistByteString bs) = case bs of- "incoming" -> return TxIncoming- "outgoing" -> return TxOutgoing- "self" -> return TxSelf- _ -> Left "Invalid Persistent TxType"-- fromPersistValue _ = Left "Invalid Persistent TxType"--instance PersistFieldSql TxType where- sqlType _ = SqlString--instance PersistField Address where- toPersistValue = PersistText . cs . addrToBase58- fromPersistValue (PersistText a) =- maybeToEither "Invalid Persistent Address" $ base58ToAddr $ cs a- fromPersistValue (PersistByteString a) =- maybeToEither "Invalid Persistent Address" $ base58ToAddr a- fromPersistValue _ = Left "Invalid Persistent Address"--instance PersistFieldSql Address where- sqlType _ = SqlString--instance PersistField BloomFilter where- toPersistValue = PersistByteString . encode- fromPersistValue (PersistByteString bs) =- case decode bs of- Right x -> Right x- Left e -> Left (fromString e)- fromPersistValue _ = Left "Invalid Persistent BloomFilter"--instance PersistFieldSql BloomFilter where- sqlType _ = SqlBlob--instance PersistField BlockHash where- toPersistValue = PersistText . cs . blockHashToHex- fromPersistValue (PersistText h) =- maybeToEither "Could not decode BlockHash" $ hexToBlockHash $ cs h- fromPersistValue (PersistByteString h) =- maybeToEither "Could not decode BlockHash" $ hexToBlockHash h- fromPersistValue _ = Left "Invalid Persistent BlockHash"--instance PersistFieldSql BlockHash where- sqlType _ = SqlString--instance PersistField TxHash where- toPersistValue = PersistText . cs . txHashToHex- fromPersistValue (PersistText h) =- maybeToEither "Invalid Persistent TxHash" $ hexToTxHash $ cs h- fromPersistValue (PersistByteString h) =- maybeToEither "Invalid Persistent TxHash" $ hexToTxHash h- fromPersistValue _ = Left "Invalid Persistent TxHash"--instance PersistFieldSql TxHash where- sqlType _ = SqlString--instance PersistField TxConfidence where- toPersistValue tc = PersistText $ case tc of- TxOffline -> "offline"- TxDead -> "dead"- TxPending -> "pending"- TxBuilding -> "building"-- fromPersistValue (PersistText txt) = case txt of- "offline" -> return TxOffline- "dead" -> return TxDead- "pending" -> return TxPending- "building" -> return TxBuilding- _ -> Left "Invalid Persistent TxConfidence"-- fromPersistValue (PersistByteString bs) = case bs of- "offline" -> return TxOffline- "dead" -> return TxDead- "pending" -> return TxPending- "building" -> return TxBuilding- _ -> Left "Invalid Persistent TxConfidence"-- fromPersistValue _ = Left "Invalid Persistent TxConfidence"--instance PersistFieldSql TxConfidence where- sqlType _ = SqlString--instance PersistField Tx where- toPersistValue = PersistByteString . encode- fromPersistValue (PersistByteString bs) =- case decode bs of- Right x -> Right x- Left e -> Left (fromString e)- fromPersistValue _ = Left "Invalid Persistent Tx"--instance PersistFieldSql Tx where- sqlType _ = SqlOther "MEDIUMBLOB"--instance PersistField PubKeyC where- toPersistValue = PersistText . cs . encodeHex . encode- fromPersistValue (PersistText txt) =- case hex >>= decode of- Right x -> Right x- Left e -> Left (fromString e)- where- hex = maybeToEither "Could not decode hex" (decodeHex (cs txt))- fromPersistValue (PersistByteString bs) =- case hex >>= decode of- Right x -> Right x- Left e -> Left (fromString e)- where- hex = maybeToEither "Could not decode hex" (decodeHex bs)- fromPersistValue _ = Left "Invalid Persistent PubKeyC"--instance PersistFieldSql PubKeyC where- sqlType _ = SqlString--instance PersistField ScriptOutput where- toPersistValue = PersistByteString . encodeOutputBS- fromPersistValue (PersistByteString bs) =- case decodeOutputBS bs of- Right x -> Right x- Left e -> Left (fromString e)- fromPersistValue _ = Left "Invalid Persistent ScriptOutput"--instance PersistFieldSql ScriptOutput where- sqlType _ = SqlBlob--instance PersistField [AddressInfo] where- toPersistValue = PersistByteString . encode- fromPersistValue (PersistByteString bs) =- case decode bs of- Right x -> Right x- Left e -> Left (fromString e)- fromPersistValue _ = Left "Invalid Persistent AddressInfo"--instance PersistFieldSql [AddressInfo] where- sqlType _ = SqlOther "MEDIUMBLOB"--{- Helpers -}---- Join AND expressions with OR conditions in a binary way-join2 :: [SqlExpr (E.Value Bool)] -> SqlExpr (E.Value Bool)-join2 xs = case xs of- [] -> val False- [x] -> x- _ -> let (ls,rs) = splitAt (length xs `div` 2) xs- in join2 ls ||. join2 rs--splitSelect :: (SqlSelect a r, MonadIO m)- => [t]- -> ([t] -> SqlQuery a)- -> SqlPersistT m [r]-splitSelect ts queryF =- fmap concat $ forM vals $ select . queryF- where- vals = chunksOf paramLimit ts--splitUpdate :: ( MonadIO m- , P.PersistEntity val- , P.PersistEntityBackend val ~ SqlBackend- )- => [t]- -> ([t] -> SqlExpr (Entity val) -> SqlQuery ())- -> SqlPersistT m ()-splitUpdate ts updateF =- forM_ vals $ update . updateF- where- vals = chunksOf paramLimit ts--splitDelete :: MonadIO m => [t] -> ([t] -> SqlQuery ()) -> SqlPersistT m ()-splitDelete ts deleteF =- forM_ vals $ E.delete . deleteF- where- vals = chunksOf paramLimit ts--splitInsertMany_ :: ( MonadIO m- , P.PersistEntity val- , P.PersistEntityBackend val ~ SqlBackend- )- => [val] -> SqlPersistT m ()-splitInsertMany_ = mapM_ P.insertMany_ . chunksOf paramLimit--limitOffset :: Word32 -> Word32 -> SqlQuery ()-limitOffset l o = do- when (l > 0) $ limit $ fromIntegral l- when (o > 0) $ offset $ fromIntegral o-
@@ -1,77 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}-module Network.Haskoin.Wallet.Types.BlockInfo-(- BlockInfo(..)-, JsonHash256-, fromNodeBlock-)-where--import Data.String.Conversions (cs, ConvertibleStrings(..))-import Data.Maybe (fromMaybe)-import Data.Aeson.TH (deriveJSON)-import Data.Word (Word32)-import Data.Time (UTCTime)-import Data.ByteString (ByteString)-import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Data.Aeson (FromJSON, ToJSON, Value (String),- parseJSON, toJSON, withText)--import Network.Haskoin.Block-import Network.Haskoin.Node.HeaderTree-import Network.Haskoin.Crypto-import Network.Haskoin.Util---newtype JsonHash256 = JsonHash256 { jsonGetHash256 :: Hash256 }- deriving (Eq, Show)--jsonHashToHex :: JsonHash256 -> ByteString-jsonHashToHex = blockHashToHex . BlockHash . jsonGetHash256--instance ToJSON JsonHash256 where- toJSON = String . cs . jsonHashToHex--instance FromJSON JsonHash256 where- parseJSON = withText "JsonHash256" $- return . JsonHash256 . getBlockHash <$>- fromMaybe (error "Invalid 256 bit hash") .- hexToBlockHash . cs--instance ConvertibleStrings JsonHash256 String where- convertString = cs . jsonHashToHex--data BlockInfo = BlockInfo- { blockInfoHash :: !BlockHash- , blockInfoHeight :: !BlockHeight- , blockInfoVersion :: !Word32- , blockInfoTimestamp :: !UTCTime- , blockInfoPrevBlock :: !BlockHash- , blockInfoMerkleRoot :: !JsonHash256- , blockInfoBits :: !Word32- , blockInfoNonce :: !Word32- , blockInfoChain :: !Word32- , blockInfoChainWork :: !Double- } deriving (Eq, Show)--$(deriveJSON (dropFieldLabel 9) ''BlockInfo)--fromNodeBlock :: NodeBlock -> BlockInfo-fromNodeBlock nb =- BlockInfo- { blockInfoHash = headerHash header- , blockInfoVersion = blockVersion header- , blockInfoPrevBlock = prevBlock header- , blockInfoNonce = bhNonce header- , blockInfoBits = blockBits header- , blockInfoMerkleRoot = JsonHash256 $ merkleRoot header- , blockInfoTimestamp = utcTimestamp- , blockInfoChainWork = nodeWork nb- , blockInfoHeight = nodeHeight nb- , blockInfoChain = nodeChain nb- }- where- header = nodeHeader nb- utcTimestamp = posixSecondsToUTCTime . realToFrac .- blockTimestamp $ header-
@@ -1,6 +1,4 @@-module Main where--import Network.Haskoin.Wallet.Client+import Haskoin.Wallet.Main main :: IO () main = clientMain
@@ -1,118 +0,0 @@-# ZeroMQ socket on which to listen to. Either absolute path or relative to-# work-dir/network.-bind-socket: ipc://hw.sock-bind-socket-notif: ipc://notif.sock--# ZeroMQ socket to communicate with the server.-# Either absolute path or relative to the server work-dir/network path.-connect-uri: ipc://hw.sock-connect-uri-notif: ipc://notif.sock--# Server mode. Can be either online or offline. In offline mode, the SPV-# daemon does not start and only the local wallet is available to query.-server-mode: online--# False positive rate for the bloom filters.-bloom-false-positive: 0.00001--# Database connection information.-database:- testnet:- database: hw-wallet.sqlite3- poolsize: 1- prodnet:- database: hw-wallet.sqlite3- poolsize: 1--# List of trusted bitcoin full-nodes to connect to.-bitcoin-full-nodes:- testnet:- - host: testnet-seed.alexykot.me- port: 18333- - host: testnet-seed.bitcoin.petertodd.org- port: 18333- - host: testnet-seed.bluematt.me- port: 18333- - host: testnet-seed.bitcoin.schildbach.de- port: 18333- prodnet:- - host: seed.bitcoin.sipa.be- port: 8333- - host: dnsseed.bluematt.me- port: 8333- - host: dnsseed.bitcoin.dashjr.org- port: 8333- - host: seed.bitcoinstats.com- port: 8333- - host: bitseed.xf2.org- port: 8333- - host: seed.bitcoin.jonasschnelli.ch- port: 8333---# Log file name. Either absolute path or relative to work-dir/network-log-file: hw.log--# PID file name. Either absolute path or relative to work-dir/network.-pid-file: hw.pid--# Compile time configuration value. Either absolute path or relative to-# work-dir. Can only be set as environment variable.-config-file: config.yml--# Default output size for commands such as page sizes.-output-size: 10--# Type of addresses to display. Example: external, internal-address-type: external--# Use reverse paging for diplaying addresses and txs when set to True.-reverse-paging: false--# Sign new and imported transactions.-sign-transactions: true--# Default fee to pay (in satoshi) for every 1000 bytes.-transaction-fee: 10000--# Minimum number of confirmations for spending coins and displaying balances.-minimum-confirmations: 0--# Display the balance including offline transactions-offline: false--# How command-line output should be displayed. Supported values are:-# normal, json or yaml.-display-format: normal--# Detach the SPV server from the terminal when launched-detach-server: false--# Use Testnet3-use-testnet: false--# Haskoin working directory. Either absolute path or relative to user’s home.-# Defaults to an appropriate OS-specific value.-work-dir: ""--# Log level. Valid values are debug, info, warn and error.-log-level: info--# Print verbose-verbose: false--# Recipient pays transaction fee. DANGEROUS.-recipient-fee: false--# Configuration file name. Only set at compile time.-config-file: config.yml--# Server key for authentication and encryption (server config).-server-key:-# Server public key for authentication and encryption (client config).-server-key-public:--# Client key for authentication and encryption (client config).-client-key:-# Client public key for aunthentication and encryption (client + server config).-client-key-public:
@@ -1,52 +0,0 @@-Server commands:- start [--detach] Start the haskoin daemon- stop Stop the haskoin daemon- status [--verbose] Display node runtime information--Account commands:- newacc name Create a new account- newread name Create a new read-only account- newms name M N Create a new multisig account- newreadms name M N Create a new read-only ms account- addkey acc Add pubkeys to a multisig account- setgap acc gap Set the address gap for an account- accounts [page] [-c pagesize] [-r] List all accounts in a keyring- account acc Display an account by name- rename old new Rename account--Address commands:- list acc [page] [-c pagesize] [-r] Display addresses by page- pubkeys acc [page] [-c pagesize] [-r] Display address public keys- unused acc [page] [-c pagesize] [-r] Display unused addresses- label acc index label Set the label of an address- addrtxs acc index [page] [-c pagesize] Display txs related to an address- getindex acc key Get key index by pubkey- genaddrs acc index [--internal] Generate addresses up to this index--Transaction commands:- txs acc [page] [-c pagesize] [-r] Display all transactions by page- pending acc [page] [-c pagesize] [-r] Display pending transactions by page- dead acc [page] [-c pagesize] [-r] Display dead transactions by page- send acc addr amount [-S] Send coins to an address- sendmany acc {addr:amount...} [-S] Send coins to many addresses- import acc Import a transaction (does not sign)- sign acc txid Sign one of your offline transactions- balance acc [--minconf] [--offline] Display account balance- gettx acc txid Get a transaction by txid- deletetx txid Delete unconfirmed transaction- sync acc {hash|height} [page] Get blocks following specified one--Offline tx signing commands:- getoffline acc txhash Get data to sign a tx offline- signoffline acc tx coindata Sign a tx with offline signing data--Utility commands:- decodetx Decode HEX transaction- rescan [timestamp] Rescan the wallet- keypair Get curve key pair for ØMQ auth- monitor [account] Monitor events (no account: blocks)- blockinfo [hash1] [hash2] [...] Get block header information by hash--Other commands:- version Display version information- help Display this help information
@@ -1,84 +0,0 @@-Account- name Text maxlen=200- type AccountType maxlen=64- derivation HardPath Maybe maxlen=200- master XPrvKey Maybe- keys [XPubKey]- gap Word32- created UTCTime- UniqueAccount name- deriving Show--WalletAddr- account AccountId- address Address maxlen=64- index KeyIndex- type AddressType maxlen=16- label Text- redeem ScriptOutput Maybe- key PubKeyC Maybe maxlen=66- created UTCTime- UniqueAddr account address- UniqueAddrRev address account- UniqueAddrIndex account type index- deriving Show--WalletTx- account AccountId- hash TxHash maxlen=64- nosigHash TxHash maxlen=64- type TxType maxlen=16- inValue Word64- outValue Word64- inputs [AddressInfo]- outputs [AddressInfo]- change [AddressInfo]- tx Tx- isCoinbase Bool- confidence TxConfidence maxlen=16- confirmedBy BlockHash Maybe maxlen=64- confirmedHeight BlockHeight Maybe- confirmedDate Timestamp Maybe- created UTCTime- UniqueAccTx account hash- UniqueAccTxRev hash account- UniqueAccNoSig account nosigHash- UniqueAccNoSigRev nosigHash account- deriving Show--WalletCoin- account AccountId- hash TxHash maxlen=64- pos Word32- tx WalletTxId- addr WalletAddrId- value Word64- script ScriptOutput- created UTCTime- UniqueCoin account hash pos- UniqueCoinRev hash pos account- UniqueCoinTx tx pos- UniqueCoinTxRev pos tx- deriving Show--SpentCoin- account AccountId- hash TxHash maxlen=64- pos Word32- spendingTx WalletTxId- created UTCTime- UniqueSpentCoins account hash pos- UniqueSpentCoinsRev hash pos account- UniqueSpentTx spendingTx hash pos- UniqueSpentTxRev hash pos spendingTx- deriving Show--WalletState- height BlockHeight- block BlockHash maxlen=64- bloomFilter BloomFilter- bloomElems Int- bloomFp Double- version Int- created UTCTime- deriving Show
@@ -1,183 +0,0 @@-module Main where--import Network.Haskoin.Wallet (Config(..),- WalletRequest(..), WalletResponse(..),- AddressType(..), OutputFormat(..),- SPVMode(..), NodeAction(..))--import Network.Haskoin.Wallet.Server (runSPVServerWithContext)-import Network.Haskoin.Wallet.Internals (BTCNode(..), Notif(..))-import qualified Network.Haskoin.Node.STM as Node--import Data.String.Conversions (cs)-import qualified System.ZMQ4 as ZMQ-import qualified Control.Monad.Logger as Log-import qualified Data.HashMap.Strict as HM-import qualified Database.Persist.Sqlite as DB-import qualified Control.Monad.Trans.Resource as Resource-import qualified Data.Aeson as JSON-import qualified Control.Concurrent as Con-import qualified Data.Aeson.Encode.Pretty as PrettyJSON-import qualified Control.Monad as M-import qualified Control.Exception as Except---databaseConf :: DB.SqliteConf-databaseConf = DB.SqliteConf "/tmp/tmpdb" 1--cmdSocket :: String-cmdSocket = "inproc://cmd"--notifSocket :: String-notifSocket = "inproc://notif"----- |Simple example app that embeds a haskoin-wallet server.--- Start wallet server + notification thread, and execute Status command when pressing ENTER-main :: IO ()-main = ZMQ.withContext $ \ctx -> do- -- Server- putStrLn "Starting server..."- _ <- Con.forkIO $ runWallet walletServerConf ctx- -- Notify thread- putStrLn "Starting notification thread..."- _ <- Con.forkIO $ notifyThread ctx notifyHandler- -- Status loop- M.forever $ do- putStrLn "Press ENTER to get server status..."- _ <- getLine- cmdGetStatus ctx >>= printStatusJSON- where- printStatusJSON = putStrLn . cs . PrettyJSON.encodePretty- notifyHandler notif =- putStrLn $ "NOTIFY: New block: " ++ cs (PrettyJSON.encodePretty notif)---- |Run haskoin-wallet using the specified ZeroMQ Context,--- and log to stderr.-runWallet :: Config -> ZMQ.Context -> IO ()-runWallet cfg ctx = run $ runSPVServerWithContext cfg ctx- where run = Resource.runResourceT . runLogging- runLogging = Log.runStderrLoggingT . Log.filterLogger logFilter- logFilter _ l = l >= configLogLevel cfg--cmdGetStatus :: ZMQ.Context -> IO Node.NodeStatus-cmdGetStatus ctx =- sendCmdOrFail (PostNodeR NodeActionStatus) ctx >>=- \res -> case res of- Nothing -> error "ERROR: Status command: no response."- Just status -> return status--sendCmdOrFail :: (JSON.FromJSON a, JSON.ToJSON a)- => WalletRequest- -> ZMQ.Context- -> IO (Maybe a)-sendCmdOrFail cmd ctx =- sendCmd cmd ctx >>=- either error return >>=- \res -> case res of- ResponseError e -> error $ "ERROR: Send cmd, ResponseError: " ++ cs e- ResponseValid r -> return r--sendCmd :: (JSON.FromJSON a, JSON.ToJSON a)- => WalletRequest- -> ZMQ.Context- -> IO (Either String (WalletResponse a))-sendCmd req ctx =- ZMQ.withSocket ctx ZMQ.Req $ \sock -> do- ZMQ.setLinger (ZMQ.restrict (0 :: Int)) sock- ZMQ.connect sock cmdSocket- ZMQ.send sock [] (cs $ JSON.encode req)- JSON.eitherDecode . cs <$> ZMQ.receive sock---- |Connect to notify socket, subscribe to new blocks,--- and execute the supplied handler for each new block as it arrives.-notifyThread :: ZMQ.Context -> (Notif -> IO ()) -> IO ()-notifyThread ctx handler = waitAndCatch $- ZMQ.withSocket ctx ZMQ.Sub $ \sock -> do- ZMQ.setLinger (ZMQ.restrict (0 :: Int)) sock- ZMQ.connect sock notifSocket- ZMQ.subscribe sock "[block]"- putStrLn "NOTIFY: Connected. Subscribed to new blocks."- M.forever $ do- [_,m] <- ZMQ.receiveMulti sock- notif <- either failOnErr return $ JSON.eitherDecode (cs m)- handler notif- where- failOnErr = fail . ("NOTIFY: ERROR: recv failed: " ++)- waitAndCatch ioa = Con.threadDelay 10000 >> ioa `Except.finally` waitAndCatch ioa--btcNodes :: [BTCNode]-btcNodes =- [ BTCNode "dnsseed.bluematt.me" 8333- , BTCNode "dnsseed.bitcoin.dashjr.org" 8333- , BTCNode "dnsseed.bluematt.me" 8333- , BTCNode "seed.bitcoinstats.com" 8333- , BTCNode "seed.bitcoin.jonasschnelli.ch" 8333- , BTCNode "seed.bitcoin.sipa.be" 8333- , BTCNode "seed.bitnodes.io" 8333- , BTCNode "seed.btcc.com" 8333- ]--walletServerConf :: Config-walletServerConf = Config- { configCount = 100- -- ^ Output size of commands- , configMinConf = 6- -- ^ Minimum number of confirmations- , configSignTx = True- -- ^ Sign transactions- , configFee = 50000- -- ^ Fee to pay per 1000 bytes when creating new transactions- , configRcptFee = False- -- ^ Recipient pays fee (dangerous, no config file setting)- , configAddrType = AddressExternal- -- ^ Return internal instead of external addresses- , configOffline = False- -- ^ Display the balance including offline transactions- , configReversePaging = False- -- ^ Use reverse paging for displaying addresses and transactions- , configPath = Nothing- -- ^ Derivation path when creating account- , configFormat = OutputNormal- -- ^ How to format the command-line results- , configConnect = cmdSocket- -- ^ ZeroMQ socket to connect to (location of the server)- , configConnectNotif = notifSocket- -- ^ ZeroMQ socket to connect for notifications- , configDetach = False- -- ^ Detach server when launched from command-line- , configFile = ""- -- ^ Configuration file- , configTestnet = False- -- ^ Use Testnet3 network- , configDir = ""- -- ^ Working directory- , configBind = cmdSocket- -- ^ Bind address for the ZeroMQ socket- , configBindNotif = notifSocket- -- ^ Bind address for ZeroMQ notifications- , configBTCNodes = HM.fromList [ ( "prodnet", btcNodes ) ]- -- ^ Trusted Bitcoin full nodes to connect to- , configMode = SPVOnline- -- ^ Operation mode of the SPV node.- , configBloomFP = 0.00001- -- ^ False positive rate for the bloom filter.- , configDatabase = HM.fromList [ ( "prodnet", databaseConf ) ]- -- ^ Database configuration- , configLogFile = ""- -- ^ Log file- , configPidFile = ""- -- ^ PID File- , configLogLevel = Log.LevelInfo- -- ^ Log level- , configVerbose = True- -- ^ Verbose- , configServerKey = Nothing- -- ^ Server key for authentication and encryption (server config)- , configServerKeyPub = Nothing- -- ^ Server public key for authentication and encryption (client config)- , configClientKey = Nothing- -- ^ Client key for authentication and encryption (client config)- , configClientKeyPub = Nothing- -- ^ Client public key for authentication and encryption- }
@@ -1,180 +1,195 @@-name: haskoin-wallet-version: 0.4.2-synopsis:- Implementation of a Bitcoin SPV Wallet with BIP32 and multisig support.-description:- This package provides a SPV (simple payment verification) wallet- implementation. It features BIP32 key management, deterministic signatures- (RFC-6979) and first order support for multi-signature transactions. You- can communicate with the wallet process through a ZeroMQ API or through a- command-line tool called "hw" which is also provided in this package.-homepage: http://github.com/haskoin/haskoin-bug-reports: http://github.com/haskoin/haskoin/issues-tested-with: GHC==8.0.2-license: PublicDomain-license-file: UNLICENSE-author: Philippe Laprade, Jean-Pierre Rupp-maintainer: xenog@protonmail.com-category: Bitcoin, Finance, Network-build-type: Simple-cabal-version: >= 1.9.2-extra-source-files: config/help, config/config.yml, config/models--source-repository head- type: git- location: git://github.com/haskoin/haskoin.git--Flag library-only- Description: Do not build the executables- Default: False--library- exposed-modules: Network.Haskoin.Wallet- Network.Haskoin.Wallet.Model- Network.Haskoin.Wallet.Client- Network.Haskoin.Wallet.Server- Network.Haskoin.Wallet.Settings- Network.Haskoin.Wallet.Internals+cabal-version: 1.12 - other-modules: Network.Haskoin.Wallet.Types- Network.Haskoin.Wallet.Types.BlockInfo- Network.Haskoin.Wallet.Accounts- Network.Haskoin.Wallet.Transaction- Network.Haskoin.Wallet.Block- Network.Haskoin.Wallet.Server.Handler- Network.Haskoin.Wallet.Client.Commands- Network.Haskoin.Wallet.Client.PrettyJson- Network.Haskoin.Wallet.Database+-- This file has been generated from package.yaml by hpack version 0.38.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 3e39505c3793edd3fb6ec57bd710521a7ac480b59144d1b82744f777acfbf6d8 - extensions: TemplateHaskell- QuasiQuotes- OverloadedStrings- MultiParamTypeClasses- TypeFamilies- GADTs- FlexibleContexts- FlexibleInstances- EmptyDataDecls- DeriveDataTypeable- RecordWildCards- GeneralizedNewtypeDeriving+name: haskoin-wallet+version: 0.9.4+synopsis: Lightweight CLI wallet for Bitcoin and Bitcoin Cash+description: Lightweight command-line interface offline-friendly hierarchical-deterministic BTC/BCH wallet compatible with BIP44 and BIP39.+category: Bitcoin, Finance, Network+homepage: http://github.com/haskoin/haskoin-wallet+bug-reports: http://github.com/haskoin/haskoin-wallet/issues+author: JP Rupp+maintainer: jprupp@protonmail.ch+license: PublicDomain+license-file: UNLICENSE+build-type: Simple - build-depends: aeson >= 0.7 && < 1.1- , aeson-pretty >= 0.7 && < 0.9- , base >= 4.8 && < 5- , bytestring >= 0.10 && < 0.11- , cereal >= 0.5 && < 0.6- , containers >= 0.5 && < 0.6- , conduit >= 1.2 && < 1.3- , deepseq >= 1.4 && < 1.5- , data-default >= 0.5 && < 0.8- , directory >= 1.2 && < 1.4- , daemons >= 0.2 && < 0.3- , exceptions >= 0.6 && < 0.9- , esqueleto >= 2.4 && < 2.6- , file-embed >= 0.0 && < 0.1- , filepath >= 1.4 && < 1.5- , haskeline- , haskoin-core >= 0.3 && < 0.5- , haskoin-node >= 0.3 && < 0.5- , lifted-async >= 0.2 && < 0.10- , lifted-base >= 0.2 && < 0.3- , monad-logger >= 0.3.13 && < 0.4- , monad-control >= 1.0 && < 1.1- , mtl >= 2.1 && < 2.3- , persistent >= 2.2 && < 2.7- , persistent-template >= 2.1 && < 2.6- , persistent-sqlite >= 2.2 && < 2.7- , resourcet >= 1.1 && < 1.2- , semigroups- , split >= 0.2 && < 0.3- , stm >= 2.4 && < 2.5- , stm-chans >= 3.0 && < 3.1- , stm-conduit >= 2.6 && < 3.1- , string-conversions >= 0.4 && < 0.5- , text >= 0.11 && < 1.3- , time >= 1.5 && < 1.7- , transformers-base >= 0.4 && < 0.5- , unix >= 2.6 && < 2.8- , unordered-containers >= 0.2 && < 0.3- , yaml >= 0.8 && < 0.9- , zeromq4-haskell >= 0.6 && < 0.7+source-repository head+ type: git+ location: https://github.com/haskoin/haskoin-wallet.git - ghc-options: -Wall+library+ hs-source-dirs:+ src+ ghc-options: -Wall+ build-depends:+ Decimal >=0.5.1+ , aeson >=1.4.6.0+ , aeson-pretty >=0.8.8+ , ansi-terminal >=1.0+ , base >=4.9 && <5+ , base16-bytestring >=1.0.0.0+ , base64-bytestring >=1.0.0.3+ , bytestring >=0.10.10.0+ , cereal >=0.5.8.1+ , conduit >=1.3.5+ , containers >=0.6.2.1+ , data-default >=0.7.1.1+ , directory >=1.3.6.0+ , entropy >=0.4.1.6+ , esqueleto >=3.5.10.1+ , haskeline >=0.7.5.0+ , haskoin-core >=1.0.4+ , haskoin-store-data >=1.2.2+ , http-types >=0.12.3+ , lens >=4.18.1+ , lens-aeson >=1.1+ , monad-logger >=0.3.40+ , mtl >=2.2.2+ , optparse-applicative >=0.15.1.0+ , persistent >=2.14.5.1+ , persistent-sqlite >=2.13.1.1+ , pretty >=1.1.3.6+ , random >=1.1+ , raw-strings-qq >=1.1+ , secp256k1-haskell >=1.1.0+ , split >=0.2.3.5+ , string-conversions >=0.4.0.1+ , text >=1.2.4.0+ , time >=1.12.2+ , transformers >=0.5.6.2+ , unordered-containers >=0.2.10.0+ , wreq >=0.5.3.2+ exposed-modules:+ Haskoin.Wallet+ Haskoin.Wallet.Amounts+ Haskoin.Wallet.Backup+ Haskoin.Wallet.Commands+ Haskoin.Wallet.Config+ Haskoin.Wallet.Database+ Haskoin.Wallet.Entropy+ Haskoin.Wallet.FileIO+ Haskoin.Wallet.Main+ Haskoin.Wallet.Migration+ Haskoin.Wallet.Migration.SemVersion+ Haskoin.Wallet.Migration.V0_9_0+ Haskoin.Wallet.Parser+ Haskoin.Wallet.PrettyPrinter+ Haskoin.Wallet.Signing+ Haskoin.Wallet.TxInfo+ Haskoin.Wallet.Util+ other-modules:+ Paths_haskoin_wallet+ default-language: Haskell2010 executable hw- if flag(library-only)- Buildable: False- main-is: Main.hs- build-depends: base, haskoin-wallet- hs-source-dirs: app- ghc-options: -Wall- -O3- -threaded- -eventlog- -rtsopts- -with-rtsopts=-N4--test-suite test-haskoin-wallet- type: exitcode-stdio-1.0- main-is: Main.hs-- other-modules: Network.Haskoin.Wallet.Arbitrary- , Network.Haskoin.Wallet.Tests- , Network.Haskoin.Wallet.Units -- extensions: RecordWildCards- OverloadedStrings-- build-depends: aeson >= 0.7 && < 1.1- , base >= 4.8 && < 5- , bytestring >= 0.10 && < 0.11- , containers >= 0.5 && < 0.6- , directory >= 1.2 && < 1.4- , haskoin-core >= 0.3 && < 0.5- , haskoin-node >= 0.3 && < 0.5- , haskoin-wallet- , monad-logger >= 0.3 && < 0.4- , mtl >= 2.1 && < 2.3- , persistent >= 2.2 && < 2.7- , persistent-sqlite >= 2.2 && < 2.7- , resourcet >= 1.1 && < 1.2- , text >= 0.11 && < 1.3- , unordered-containers >= 0.2 && < 0.3- , HUnit >= 1.2 && < 1.6- , QuickCheck >= 2.8 && < 2.10- , stm >= 2.4 && < 2.5- , stm-chans >= 3.0 && < 3.1- , string-conversions >= 0.4 && < 0.5- , test-framework >= 0.8 && < 0.9- , test-framework-quickcheck2 >= 0.3 && < 0.4- , test-framework-hunit >= 0.3 && < 0.4-- hs-source-dirs: tests- ghc-options: -Wall--executable example-inproc-wallet-server- if flag(library-only)- Buildable: False-- main-is: Main.hs- hs-source-dirs: examples/embedded-inproc-wallet-server/- extensions: OverloadedStrings-- ghc-options: -Wall- -O3- -threaded- -rtsopts- -with-rtsopts=-N4+ main-is: Main.hs+ hs-source-dirs:+ app+ ghc-options: -Wall+ build-depends:+ Decimal >=0.5.1+ , aeson >=1.4.6.0+ , aeson-pretty >=0.8.8+ , ansi-terminal >=1.0+ , base >=4.9 && <5+ , base16-bytestring >=1.0.0.0+ , base64-bytestring >=1.0.0.3+ , bytestring >=0.10.10.0+ , cereal >=0.5.8.1+ , conduit >=1.3.5+ , containers >=0.6.2.1+ , data-default >=0.7.1.1+ , directory >=1.3.6.0+ , entropy >=0.4.1.6+ , esqueleto >=3.5.10.1+ , haskeline >=0.7.5.0+ , haskoin-core >=1.0.4+ , haskoin-store-data >=1.2.2+ , haskoin-wallet+ , http-types >=0.12.3+ , lens >=4.18.1+ , lens-aeson >=1.1+ , monad-logger >=0.3.40+ , mtl >=2.2.2+ , optparse-applicative >=0.15.1.0+ , persistent >=2.14.5.1+ , persistent-sqlite >=2.13.1.1+ , pretty >=1.1.3.6+ , random >=1.1+ , raw-strings-qq >=1.1+ , secp256k1-haskell >=1.1.0+ , split >=0.2.3.5+ , string-conversions >=0.4.0.1+ , text >=1.2.4.0+ , time >=1.12.2+ , transformers >=0.5.6.2+ , unordered-containers >=0.2.10.0+ , wreq >=0.5.3.2+ other-modules:+ Paths_haskoin_wallet+ default-language: Haskell2010 - build-depends: base >= 4.8 && < 5- , aeson >= 0.7 && < 1.1- , aeson-pretty >= 0.7 && < 0.9- , haskoin-node >= 0.3 && < 0.5- , haskoin-wallet- , monad-logger >= 0.3 && < 0.4- , persistent-sqlite >= 2.2 && < 2.7- , resourcet >= 1.1 && < 1.2- , unordered-containers >= 0.2 && < 0.3- , string-conversions >= 0.4 && < 0.5- , zeromq4-haskell >= 0.6 && < 0.7+test-suite spec+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Haskoin.Wallet.AmountsSpec+ Haskoin.Wallet.CommandsSpec+ Haskoin.Wallet.EntropySpec+ Haskoin.Wallet.SigningSpec+ Haskoin.Wallet.TestUtils+ Haskoin.Wallet.VersionSpec+ Paths_haskoin_wallet+ hs-source-dirs:+ test+ ghc-options: -Wall+ build-depends:+ Decimal >=0.5.1+ , HUnit >=1.6.0.0+ , QuickCheck >=2.13.2+ , aeson >=1.4.6.0+ , aeson-pretty >=0.8.8+ , ansi-terminal >=1.0+ , base >=4.9 && <5+ , base16-bytestring >=1.0.0.0+ , base64-bytestring >=1.0.0.3+ , bytestring >=0.10.10.0+ , cereal >=0.5.8.1+ , conduit >=1.3.5+ , containers >=0.6.2.1+ , data-default >=0.7.1.1+ , directory >=1.3.6.0+ , entropy >=0.4.1.6+ , esqueleto >=3.5.10.1+ , haskeline >=0.7.5.0+ , haskoin-core >=1.0.4+ , haskoin-store-data >=1.2.2+ , haskoin-wallet+ , hspec >=2.7.1+ , http-types >=0.12.3+ , lens >=4.18.1+ , lens-aeson >=1.1+ , monad-logger >=0.3.40+ , mtl >=2.2.2+ , optparse-applicative >=0.15.1.0+ , persistent >=2.14.5.1+ , persistent-sqlite >=2.13.1.1+ , pretty >=1.1.3.6+ , random >=1.1+ , raw-strings-qq >=1.1+ , secp256k1-haskell >=1.1.0+ , split >=0.2.3.5+ , string-conversions >=0.4.0.1+ , text >=1.2.4.0+ , time >=1.12.2+ , transformers >=0.5.6.2+ , unordered-containers >=0.2.10.0+ , wreq >=0.5.3.2+ default-language: Haskell2010+ build-tool-depends: hspec-discover:hspec-discover
@@ -0,0 +1,24 @@+module Haskoin.Wallet+ ( module Amounts,+ module Commands,+ module Config,+ module Database,+ module Entropy,+ module FileIO,+ module Parser,+ module Signing,+ module TxInfo,+ module Util,+ )+where++import Haskoin.Wallet.Amounts as Amounts+import Haskoin.Wallet.Commands as Commands+import Haskoin.Wallet.Config as Config+import Haskoin.Wallet.Database as Database+import Haskoin.Wallet.Entropy as Entropy+import Haskoin.Wallet.FileIO as FileIO+import Haskoin.Wallet.Parser as Parser+import Haskoin.Wallet.Signing as Signing+import Haskoin.Wallet.TxInfo as TxInfo+import Haskoin.Wallet.Util as Util
@@ -0,0 +1,88 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}++module Haskoin.Wallet.Amounts where++import Control.Arrow (second)+import Control.Monad (guard)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Text.Read as Read+import Haskoin.Wallet.Util+import Numeric.Natural (Natural)++data AmountUnit+ = UnitBitcoin+ | UnitBit+ | UnitSatoshi+ deriving (Eq, Show)++-- Amount Parsing --++showUnit :: AmountUnit -> Integer -> Text+showUnit unit amnt+ | unit == UnitSatoshi = strUnit -- satoshi is always singular+ | abs amnt == 1 = strUnit+ | otherwise = strUnit <> "s" -- plural form bitcoins and bits+ where+ strUnit =+ case unit of+ UnitBitcoin -> "bitcoin"+ UnitBit -> "bit"+ UnitSatoshi -> "satoshi"++showAmount :: AmountUnit -> Natural -> Text+showAmount unit amnt =+ case unit of+ UnitBitcoin ->+ let (q, r) = amnt `divMod` 100000000+ in addSep (showT q) <> "." <> removeEnd (padStart 8 "0" (showT r))+ UnitBit ->+ let (q, r) = amnt `divMod` 100+ in addSep (showT q) <> "." <> padStart 2 "0" (showT r)+ UnitSatoshi -> addSep (showT amnt)+ where+ removeEnd = dropPatternEnd "0000" . dropPatternEnd "000000"+ addSep = Text.intercalate "'" . chunksOfEnd 3+ showT = Text.pack . show++readAmount :: AmountUnit -> Text -> Maybe Natural+readAmount unit amntStr =+ case unit of+ UnitBitcoin -> do+ guard $ Text.length r <= 8+ a <- readNatural q+ b <- readNatural $ padEnd 8 "0" r+ return $ a * 100000000 + b+ UnitBit -> do+ guard $ Text.length r <= 2+ a <- readNatural q+ b <- readNatural $ padEnd 2 "0" r+ return $ a * 100 + b+ UnitSatoshi -> readNatural str+ where+ str = dropAmountSep amntStr+ (q, r) = second (Text.drop 1) $ Text.breakOn "." str++readNatural :: Text -> Maybe Natural+readNatural txt =+ case Read.decimal txt of+ Right (res, "") -> Just res+ _ -> Nothing++dropAmountSep :: Text -> Text+dropAmountSep = Text.filter (`notElem` [' ', '_', '\''])++-- | Like 'showAmount' but will display a minus sign for negative amounts+showIntegerAmount :: AmountUnit -> Integer -> Text+showIntegerAmount unit i+ | i < 0 = "-" <> showAmount unit (fromIntegral $ abs i)+ | otherwise = showAmount unit $ fromIntegral i++-- | Like 'readAmount' but can parse a negative amount+readIntegerAmount :: AmountUnit -> Text -> Maybe Integer+readIntegerAmount unit txt =+ case Text.uncons txt of+ Just ('-', rest) -> negate . toInteger <$> readAmount unit rest+ _ -> toInteger <$> readAmount unit txt
@@ -0,0 +1,358 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Haskoin.Wallet.Backup where++import Conduit (MonadUnliftIO)+import Control.Monad+import Control.Monad.Except+import Control.Monad.Reader (MonadIO (..), MonadTrans (lift))+import Data.Aeson (object, (.:), (.=))+import qualified Data.Aeson as Json+import Data.Default (def)+import Data.List (nub, sort, (\\))+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (fromJust)+import qualified Data.Serialize as S+import Data.Text (Text)+import Data.Time (UTCTime)+import Database.Esqueleto.Legacy as E+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Store.WebClient+import Haskoin.Wallet.Config+import Haskoin.Wallet.Database+import Haskoin.Wallet.TxInfo+import Haskoin.Wallet.Util+import Numeric.Natural (Natural)++newtype WalletBackup = WalletBackup [AccountBackup]+ deriving (Eq, Show)++instance MarshalJSON Ctx WalletBackup where+ marshalValue ctx (WalletBackup accs) =+ object ["accounts" .= (marshalValue ctx <$> accs)]+ unmarshalValue ctx =+ Json.withObject "WalletBackup" $ \o -> do+ accs <- mapM (unmarshalValue ctx) =<< o .: "accounts"+ return $ WalletBackup accs++data AccountBackup = AccountBackup+ { accountBackupName :: !Text,+ accountBackupWallet :: !Fingerprint,+ accountBackupPubKey :: !XPubKey,+ accountBackupNetwork :: !Network,+ accountBackupExternal :: !Natural,+ accountBackupInternal :: !Natural,+ accountBackupLabels :: Map Address Text,+ accountBackupFree :: [Address],+ accountBackupCreated :: !UTCTime+ }+ deriving (Eq, Show)++instance MarshalJSON Ctx AccountBackup where+ marshalValue ctx AccountBackup {..} =+ let net = accountBackupNetwork+ f = Map.mapKeys (fromJust . addrToText net)+ in object+ [ "name" .= accountBackupName,+ "wallet" .= accountBackupWallet,+ "xpubkey"+ .= marshalValue (accountBackupNetwork, ctx) accountBackupPubKey,+ "network" .= accountBackupNetwork.name,+ "external" .= accountBackupExternal,+ "internal" .= accountBackupInternal,+ "labels" .= f accountBackupLabels,+ "free" .= (fromJust . addrToText net <$> accountBackupFree),+ "created" .= accountBackupCreated+ ]+ unmarshalValue ctx =+ Json.withObject "AccountBackup" $ \o -> do+ net <- maybe mzero pure . netByName =<< o .: "network"+ let f = Map.mapKeys (fromJust . textToAddr net)+ AccountBackup+ <$> o .: "name"+ <*> o .: "wallet"+ <*> (unmarshalValue (net, ctx) =<< o .: "xpubkey")+ <*> pure net+ <*> o .: "external"+ <*> o .: "internal"+ <*> (f <$> o .: "labels")+ <*> ((fromJust . textToAddr net <$>) <$> o .: "free")+ <*> o .: "created"++createBackup :: (MonadUnliftIO m) => Ctx -> ExceptT String (DB m) WalletBackup+createBackup ctx = do+ accs <- lift getAccounts+ backs <-+ forM accs $ \(_, acc@DBAccount {..}) -> do+ let net = accountNetwork acc+ (DBWalletKey fpT) = dBAccountWallet+ fp <- liftEither $ textToFingerprint fpT+ labelVals <-+ lift . select . from $ \a -> do+ where_ $+ a ^. DBAddressAccountWallet ==. val dBAccountWallet+ &&. a ^. DBAddressAccountDerivation ==. val dBAccountDerivation+ &&. a ^. DBAddressInternal ==. val False+ &&. a ^. DBAddressLabel !=. val ""+ return (a ^. DBAddressAddress, a ^. DBAddressLabel)+ labels <- forM labelVals $ \(Value at, Value l) -> do+ a <- liftMaybe "Address" $ textToAddr net at+ return (a, l)+ freeVals <-+ lift . select . from $ \a -> do+ where_ $+ a ^. DBAddressAccountWallet ==. val dBAccountWallet+ &&. a ^. DBAddressAccountDerivation ==. val dBAccountDerivation+ &&. a ^. DBAddressInternal ==. val True+ &&. a ^. DBAddressFree ==. val True+ return $ a ^. DBAddressAddress+ free <- mapM (liftMaybe "Address" . textToAddr net . unValue) freeVals+ return+ AccountBackup+ { accountBackupName = dBAccountName,+ accountBackupWallet = fp,+ accountBackupPubKey = accountXPubKey ctx acc,+ accountBackupNetwork = accountNetwork acc,+ accountBackupExternal = fromIntegral dBAccountExternal,+ accountBackupInternal = fromIntegral dBAccountInternal,+ accountBackupLabels = Map.fromList labels,+ accountBackupFree = free,+ accountBackupCreated = dBAccountCreated+ }+ return $ WalletBackup backs++data SyncRes = SyncRes+ { syncResAccount :: !DBAccount,+ syncResBlockHash :: !BlockHash,+ syncResBlockHeight :: !BlockHeight,+ syncResTxUpdates :: !Natural,+ syncResCoinUpdates :: !Natural+ }+ deriving (Eq, Show)++restoreBackup ::+ (MonadUnliftIO m) =>+ Ctx ->+ Config ->+ WalletBackup ->+ ExceptT String (DB m) [SyncRes]+restoreBackup ctx cfg (WalletBackup accs) =+ mapM (restoreAccount ctx cfg) accs++restoreAccount ::+ (MonadUnliftIO m) =>+ Ctx ->+ Config ->+ AccountBackup ->+ ExceptT String (DB m) SyncRes+restoreAccount ctx cfg AccountBackup {..} = do+ -- Insert the account+ (accId, acc) <-+ insertAccount+ accountBackupNetwork+ ctx+ accountBackupWallet+ accountBackupName+ accountBackupPubKey+ let net = accountBackupNetwork+ -- Set the external and internal derivation indices+ (e, i) <- discoverAddrs net ctx cfg accountBackupPubKey+ let idxE = max e accountBackupExternal+ idxI = max i accountBackupInternal+ discoverAccGenAddrs ctx cfg accId AddrExternal $ fromIntegral idxE+ discoverAccGenAddrs ctx cfg accId AddrInternal $ fromIntegral idxI+ -- Perform an account sync+ syncRes <- sync ctx cfg net accId True+ -- Set address labels+ forM_ (Map.assocs accountBackupLabels) $ \(addr, l) -> do+ at <- liftMaybe "Address" $ addrToText net addr+ lift . update $ \a -> do+ set a [DBAddressLabel =. val l]+ where_ $ a ^. DBAddressAddress ==. val at+ -- Set Free internal addresses+ addrsT <- mapM (liftMaybe "Address" . addrToText net) accountBackupFree+ _ <- lift $ setAddrsFree AddrFree addrsT+ -- Set the creation time+ lift . update $ \a -> do+ set a [DBAccountCreated =. val accountBackupCreated]+ where_ $+ a ^. DBAccountWallet ==. val (dBAccountWallet acc)+ &&. a ^. DBAccountDerivation ==. val (dBAccountDerivation acc)+ acc' <- getAccountById accId+ return syncRes {syncResAccount = acc'}++discoverAddrs ::+ (MonadIO m) =>+ Network ->+ Ctx ->+ Config ->+ XPubKey ->+ ExceptT String m (Natural, Natural)+discoverAddrs net ctx cfg pub = do+ let recoveryGap = configRecoveryGap cfg+ e <- go extDeriv 0 (Page recoveryGap 0)+ i <- go intDeriv 0 (Page recoveryGap 0)+ return (fromIntegral e, fromIntegral i)+ where+ go path d page@(Page lim off) = do+ let addrs = addrsDerivPage ctx path page pub+ req = GetAddrsBalance $ fst <$> addrs+ let host = apiHost net cfg+ Store.SerialList bals <- liftExcept $ apiCall ctx host req+ let vBals = filter ((/= 0) . (.txs)) bals+ if null vBals+ then return d+ else do+ let dMax = findMax addrs $ (.address) <$> vBals+ go path (dMax + 1) (Page lim (off + lim))+ -- Find the largest ID amongst the addresses that have a positive balance+ findMax :: [(Address, SoftPath)] -> [Address] -> Int+ findMax addrs balAddrs =+ let fAddrs = filter ((`elem` balAddrs) . fst) addrs+ in fromIntegral $ maximum $ last . pathToList . snd <$> fAddrs++sync ::+ (MonadUnliftIO m) =>+ Ctx ->+ Config ->+ Network ->+ DBAccountId ->+ Bool ->+ ExceptT String (DB m) SyncRes+sync ctx cfg net accId full = do+ let host = apiHost net cfg+ -- Check API health+ checkHealth ctx net cfg+ -- Get the new best block before starting the sync+ best <- liftExcept $ apiCall ctx host (GetBlockBest def)+ -- Get the addresses from our local database+ (addrPathMap, addrBalMap) <- allAddressesMap net accId+ -- Fetch the address balances online+ Store.SerialList storeBals <-+ liftExcept . apiBatch ctx (configAddrBatch cfg) host $+ GetAddrsBalance (Map.keys addrBalMap)+ -- Filter only those addresses whose balances have changed+ balsToUpdate <-+ if full+ then return storeBals+ else liftEither $ filterAddresses storeBals addrBalMap+ let addrsToUpdate = (.address) <$> balsToUpdate+ -- Update balances+ updateAddressBalances net balsToUpdate+ newAcc <- lift $ updateAccountBalances accId+ -- Get a list of our confirmed txs in the local database+ -- Use an empty list when doing a full sync+ confirmedTxs <- if full then return [] else getConfirmedTxs accId True+ -- Fetch the txids of the addresses to update+ aTids <- searchAddrTxs net ctx cfg confirmedTxs addrsToUpdate+ -- We also want to check if there is any change in unconfirmed txs+ uTids <- getConfirmedTxs accId False+ let tids = nub $ uTids <> aTids+ -- Fetch the full transactions+ Store.SerialList txs <-+ liftExcept $ apiBatch ctx (configTxFullBatch cfg) host (GetTxs tids)+ -- Convert them to TxInfo and store them in the local database+ let txInfos = storeToTxInfo addrPathMap (fromIntegral best.height) <$> txs+ resTxInfo <- forM txInfos $ repsertTxInfo net ctx accId+ -- Fetch and update coins+ Store.SerialList storeCoins <-+ liftExcept . apiBatch ctx (configCoinBatch cfg) host $+ GetAddrsUnspent addrsToUpdate def+ (coinCount, newCoins) <- refreshCoins net accId addrsToUpdate storeCoins+ -- Get the dependent tranactions of the new coins+ depTxsHash <-+ if full+ then return $ (.outpoint.hash) <$> storeCoins+ else mapM (liftEither . coinToTxHash) newCoins+ Store.RawResultList rawTxs <-+ liftExcept+ . apiBatch ctx (configTxFullBatch cfg) host+ $ GetTxsRaw+ $ nub depTxsHash+ lift $ forM_ rawTxs insertRawTx+ -- Remove pending transactions if they are online+ pendingTids <- pendingTxHashes accId+ let toRemove = filter ((`elem` tids) . fst) pendingTids+ forM_ toRemove $ \(_, key) -> lift $ deletePendingTxOnline key+ -- Update the best block for this network+ lift $ updateBest net (headerHash best.header) best.height+ return $+ SyncRes+ newAcc+ (headerHash best.header)+ (fromIntegral best.height)+ (fromIntegral $ length $ filter id $ snd <$> resTxInfo)+ (fromIntegral coinCount)++-- Filter addresses that need to be updated+filterAddresses ::+ [Store.Balance] ->+ Map Address AddressBalance ->+ Either String [Store.Balance]+filterAddresses sBals aMap+ | sort ((.address) <$> sBals) /= sort (Map.keys aMap) =+ Left "Sync: addresses do not match"+ | otherwise =+ Right $ filter f sBals+ where+ f s =+ let b = fromJust $ s.address `Map.lookup` aMap+ in s.txs /= addrBalanceTxs b+ || s.confirmed /= addrBalanceConfirmed b+ || s.unconfirmed /= addrBalanceUnconfirmed b+ || s.utxo /= addrBalanceCoins b++searchAddrTxs ::+ (MonadIO m) =>+ Network ->+ Ctx ->+ Config ->+ [TxHash] ->+ [Address] ->+ ExceptT String m [TxHash]+searchAddrTxs _ _ _ _ [] = return []+searchAddrTxs net ctx cfg confirmedTxs as+ | length as > fromIntegral (configAddrBatch cfg) =+ nub . concat <$> mapM (go Nothing 0) (chunksOf (configAddrBatch cfg) as)+ | otherwise =+ nub <$> go Nothing 0 as+ where+ go hashM offset' xs = do+ Store.SerialList txRefs <-+ liftExcept $+ apiCall+ ctx+ (apiHost net cfg)+ ( GetAddrsTxs+ xs+ def+ { limit = Just $ fromIntegral (configTxBatch cfg),+ start = StartParamHash <$> hashM,+ offset = offset'+ }+ )+ -- Remove txs that we already have+ let tids = ((.txid) <$> txRefs) \\ confirmedTxs+ -- Either we have reached the end of the stream, or we have hit some+ -- txs in confirmedTxs. In both cases, we can stop the search.+ if length tids < fromIntegral (configTxBatch cfg)+ then return tids+ else do+ let lastId = (last tids).get+ rest <- go (Just lastId) 1 xs+ return $ tids <> rest++coinToTxHash :: DBCoin -> Either String TxHash+coinToTxHash coin =+ maybeToEither "coinToTxHash: Invalid outpoint" $ do+ bs <- decodeHex $ dBCoinOutpoint coin+ op <- eitherToMaybe (S.decode bs) :: Maybe OutPoint+ return op.hash
@@ -0,0 +1,833 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}++module Haskoin.Wallet.Commands where++import Conduit (MonadUnliftIO)+import Control.Monad+import Control.Monad.Except+import Control.Monad.Reader (MonadIO (..), MonadTrans (lift))+import Data.Aeson (object, (.:), (.=))+import qualified Data.Aeson as Json+import qualified Data.ByteString as BS+import Data.Foldable (for_)+import Data.Maybe (fromMaybe, isJust)+import Data.String.Conversions (cs)+import Data.Text (Text)+import qualified Data.Text as Text+import Database.Persist.Sqlite (transactionUndo)+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Store.WebClient+import Haskoin.Wallet.Amounts+import Haskoin.Wallet.Backup+import Haskoin.Wallet.Config+import Haskoin.Wallet.Database+import Haskoin.Wallet.Entropy+import Haskoin.Wallet.FileIO+import Haskoin.Wallet.Migration.SemVersion+import Haskoin.Wallet.Parser+import Haskoin.Wallet.Signing+import Haskoin.Wallet.TxInfo+import Haskoin.Wallet.Util+import Numeric.Natural (Natural)+import qualified System.Console.Haskeline as Haskeline+import qualified System.Directory as D+import System.Random (initStdGen)++data Response+ = ResponseError+ { responseError :: !Text+ }+ | ResponseMnemonic+ { responseEntropySource :: !Text,+ responseMnemonic :: ![Text],+ responseSplitMnemonic :: ![[Text]]+ }+ | ResponseAccount+ { responseAccount :: !DBAccount+ }+ | ResponseAccResult+ { responseAccount :: !DBAccount,+ responseResult :: !Bool,+ responseText :: !Text+ }+ | ResponseFile+ { responseTxFile :: !FilePath+ }+ | ResponseAccounts+ { responseAccounts :: ![DBAccount]+ }+ | ResponseAddress+ { responseAccount :: !DBAccount,+ responseAddress :: !DBAddress+ }+ | ResponseAddresses+ { responseAccount :: !DBAccount,+ responseAddresses :: ![DBAddress]+ }+ | ResponseTxs+ { responseAccount :: !DBAccount,+ responseTxs :: ![TxInfo]+ }+ | ResponseTx+ { responseAccount :: !DBAccount,+ responsePendingTx :: !TxInfo+ }+ | ResponseDeleteTx+ { responseNoSigHash :: !TxHash,+ responseFreedCoins :: !Natural,+ responseFreedAddrs :: !Natural+ }+ | ResponseCoins+ { responseAccount :: !DBAccount,+ responseCoins :: ![JsonCoin]+ }+ | ResponseSync {responseSync :: [SyncRes]}+ | ResponseRestore+ { responseRestore :: ![(DBAccount, Natural, Natural)]+ }+ | ResponseVersion+ { responseVersion :: !Text,+ responseDBVersion :: !Text+ }+ | ResponseRollDice+ { responseRollDice :: ![Natural],+ responseEntropySource :: !Text+ }+ deriving (Eq, Show)++jsonError :: String -> Json.Value+jsonError err = object ["type" .= Json.String "error", "error" .= err]++instance MarshalJSON Ctx Response where+ marshalValue ctx =+ \case+ ResponseError err -> jsonError $ cs err+ ResponseMnemonic e w ws ->+ object+ [ "type" .= Json.String "mnemonic",+ "entropysource" .= e,+ "mnemonic" .= w,+ "splitmnemonic" .= ws+ ]+ ResponseAccount a ->+ object+ [ "type" .= Json.String "account",+ "account" .= a+ ]+ ResponseAccResult a b t ->+ object+ [ "type" .= Json.String "accresult",+ "account" .= a,+ "result" .= b,+ "text" .= t+ ]+ ResponseFile f ->+ object+ [ "type" .= Json.String "file",+ "file" .= f+ ]+ ResponseAccounts as ->+ object+ [ "type" .= Json.String "accounts",+ "accounts" .= as+ ]+ ResponseAddress a addr ->+ object+ [ "type" .= Json.String "address",+ "account" .= a,+ "address" .= addr+ ]+ ResponseAddresses a adrs ->+ object+ [ "type" .= Json.String "addresses",+ "account" .= a,+ "addresses" .= adrs+ ]+ ResponseTxs a txs ->+ object+ [ "type" .= Json.String "txs",+ "account" .= a,+ "txs" .= (marshalValue (accountNetwork a, ctx) <$> txs)+ ]+ ResponseTx a t -> do+ let net = accountNetwork a+ object+ [ "type" .= Json.String "tx",+ "account" .= a,+ "tx" .= marshalValue (net, ctx) t+ ]+ ResponseDeleteTx h c a ->+ object+ [ "type" .= Json.String "deletetx",+ "nosighash" .= h,+ "freedcoins" .= c,+ "freedaddrs" .= a+ ]+ ResponseCoins a coins -> do+ let net = accountNetwork a+ object+ [ "type" .= Json.String "coins",+ "account" .= a,+ "coins" .= (marshalValue net <$> coins)+ ]+ ResponseSync xs -> do+ let f (SyncRes as bb bh tc cc) =+ object+ [ "account" .= as,+ "bestblock" .= bb,+ "bestheight" .= bh,+ "txupdates" .= tc,+ "coinupdates" .= cc+ ]+ object+ [ "type" .= Json.String "sync",+ "syncres" .= (f <$> xs)+ ]+ ResponseRestore xs ->+ let f (acc, t, c) =+ object+ [ "account" .= acc,+ "txupdates" .= t,+ "coinupdates" .= c+ ]+ in object+ [ "type" .= Json.String "restore",+ "restore" .= (f <$> xs)+ ]+ ResponseVersion v dbv ->+ object+ [ "type" .= Json.String "version",+ "version" .= v,+ "dbversion" .= dbv+ ]+ ResponseRollDice ns e ->+ object+ ["type" .= Json.String "rolldice", "entropysource" .= e, "dice" .= ns]+ unmarshalValue ctx =+ Json.withObject "response" $ \o -> do+ Json.String resType <- o .: "type"+ case resType of+ "error" -> ResponseError <$> o .: "error"+ "mnemonic" ->+ ResponseMnemonic+ <$> o .: "entropysource"+ <*> o .: "mnemonic"+ <*> o .: "splitmnemonic"+ "account" ->+ ResponseAccount+ <$> o .: "account"+ "accresult" ->+ ResponseAccResult+ <$> o .: "account"+ <*> o .: "result"+ <*> o .: "text"+ "file" ->+ ResponseFile+ <$> o .: "file"+ "accounts" ->+ ResponseAccounts+ <$> o .: "accounts"+ "address" ->+ ResponseAddress+ <$> o .: "account"+ <*> o .: "address"+ "addresses" ->+ ResponseAddresses+ <$> o .: "account"+ <*> o .: "addresses"+ "txs" -> do+ a <- o .: "account"+ let net = accountNetwork a+ txs <- mapM (unmarshalValue (net, ctx)) =<< o .: "txs"+ return $ ResponseTxs a txs+ "tx" -> do+ a <- o .: "account"+ let net = accountNetwork a+ t <- unmarshalValue (net, ctx) =<< o .: "tx"+ return $ ResponseTx a t+ "deletetx" ->+ ResponseDeleteTx+ <$> o .: "nosighash"+ <*> o .: "freedcoins"+ <*> o .: "freedaddrs"+ "coins" -> do+ a <- o .: "account"+ xs <- o .: "coins"+ coins <- mapM (unmarshalValue (accountNetwork a)) xs+ return $ ResponseCoins a coins+ "sync" -> do+ xs <- o .: "syncres"+ res <- forM xs $ \x ->+ SyncRes+ <$> x .: "account"+ <*> x .: "bestblock"+ <*> x .: "bestheight"+ <*> x .: "txupdates"+ <*> x .: "coinupdates"+ return $ ResponseSync res+ "restore" -> do+ let f =+ Json.withObject "account" $ \o' ->+ (,,)+ <$> o' .: "account"+ <*> o' .: "txupdates"+ <*> o' .: "coinupdates"+ ResponseRestore <$> (mapM f =<< o .: "restore")+ "version" ->+ ResponseVersion+ <$> o .: "version"+ <*> o .: "dbversion"+ "rolldice" ->+ ResponseRollDice+ <$> o .: "dice"+ <*> o .: "entropysource"+ _ -> fail "Invalid JSON response type"++runDBResponse ::+ (MonadUnliftIO m) => Config -> ExceptT String (DB m) Response -> m Response+runDBResponse cfg action = do+ runDB cfg $ do+ resE <- runExceptT action+ case resE of+ Left err -> do+ transactionUndo -- Roll back the current sqlite transaction+ return $ ResponseError $ cs err+ Right res -> return res++catchResponseError :: (Monad m) => ExceptT String m Response -> m Response+catchResponseError m = do+ resE <- runExceptT m+ case resE of+ Left err -> return $ ResponseError $ cs err+ Right res -> return res++commandResponse :: Ctx -> Config -> AmountUnit -> Command -> IO Response+commandResponse ctx cfg unit cmd =+ case cmd of+ -- Mnemonic and account management+ CommandMnemonic e d s -> cmdMnemonic e d s+ CommandCreateAcc t n dM s -> cmdCreateAcc ctx cfg t n dM s+ CommandTestAcc nameM s -> cmdTestAcc ctx cfg nameM s+ CommandRenameAcc old new -> cmdRenameAcc cfg old new+ CommandAccounts nameM -> cmdAccounts cfg nameM+ CommandSyncAcc nameM full -> cmdSyncAcc ctx cfg nameM full+ CommandDeleteAcc name net deriv -> cmdDeleteAcc cfg name net deriv+ -- Address management+ CommandReceive nameM labM -> cmdReceive ctx cfg nameM labM+ CommandAddrs nameM p -> cmdAddrs cfg nameM p+ CommandLabel nameM i l -> cmdLabel cfg nameM i l+ -- Transaction management+ CommandTxs nameM p -> cmdTxs ctx cfg nameM p+ CommandPrepareTx rcpts nameM fee dust rcptPay minConf o ->+ cmdPrepareTx ctx cfg rcpts nameM unit fee dust rcptPay minConf o+ CommandPendingTxs nameM p -> cmdPendingTxs ctx cfg nameM p+ CommandSignTx nameM h i o s -> cmdSignTx ctx cfg nameM h i o s+ CommandSendTx h -> cmdSendTx ctx cfg h+ CommandDeleteTx h -> cmdDeleteTx ctx cfg h+ CommandCoins nameM p -> cmdCoins cfg nameM p+ -- Import/export commands+ CommandExportAcc nameM f -> cmdExportAcc ctx cfg nameM f+ CommandImportAcc f -> cmdImportAcc ctx cfg f+ CommandReviewTx nameM file -> cmdReviewTx ctx cfg nameM file+ CommandExportTx h f -> cmdExportTx cfg h f+ CommandImportTx nameM file -> cmdImportTx ctx cfg nameM file+ -- Backup and Restore+ CommandBackup f -> cmdBackup ctx cfg f+ CommandRestore f -> cmdRestore ctx cfg f+ CommandDiscoverAcc nameM -> cmdDiscoverAccount ctx cfg nameM+ -- Utilities+ CommandVersion -> cmdVersion cfg+ CommandPrepareSweep nameM prvKey st outputM f d ->+ prepareSweep ctx cfg nameM prvKey st outputM f d+ CommandSignSweep nameM h i o k -> signSweep ctx cfg nameM h i o k+ CommandRollDice n -> rollDice n++-- runDBResponse Monad Stack:+-- ExceptT String (ReaderT SqlBackend (NoLoggingT (ResourceT (IO))))++liftEitherIO :: (MonadIO m) => IO (Either String a) -> ExceptT String m a+liftEitherIO = liftEither <=< liftIO++cmdMnemonic :: Natural -> Bool -> Natural -> IO Response+cmdMnemonic ent useDice splitMnemIn =+ catchResponseError $ do+ (orig, ms, splitMs) <- genMnemonic ent useDice splitMnemIn+ return $ ResponseMnemonic orig (Text.words ms) (Text.words <$> splitMs)++cmdCreateAcc ::+ Ctx -> Config -> Text -> Network -> Maybe Natural -> Natural -> IO Response+cmdCreateAcc ctx cfg name net derivM splitMnemIn = do+ runDBResponse cfg $ do+ mnem <- askMnemonicPass splitMnemIn+ walletFP <- liftEither $ walletFingerprint net ctx mnem+ d <- maybe (lift $ nextAccountDeriv walletFP net) return derivM+ prvKey <- liftEither $ signingKey net ctx mnem d+ let xpub = deriveXPubKey ctx prvKey+ (_, acc) <- insertAccount net ctx walletFP name xpub+ return $ ResponseAccount acc++cmdTestAcc :: Ctx -> Config -> Maybe Text -> Natural -> IO Response+cmdTestAcc ctx cfg nameM splitMnemIn =+ runDBResponse cfg $ do+ (_, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ xPubKey = accountXPubKey ctx acc+ d = accountIndex acc+ mnem <- askMnemonicPass splitMnemIn+ xPrvKey <- liftEither $ signingKey net ctx mnem d+ return $+ if deriveXPubKey ctx xPrvKey == xPubKey+ then+ ResponseAccResult+ { responseAccount = acc,+ responseResult = True,+ responseText =+ "The mnemonic and passphrase matched the account"+ }+ else+ ResponseAccResult+ { responseAccount = acc,+ responseResult = False,+ responseText =+ "The mnemonic and passphrase did not match the account"+ }++cmdImportAcc :: Ctx -> Config -> FilePath -> IO Response+cmdImportAcc ctx cfg fp =+ runDBResponse cfg $ do+ (PubKeyDoc xpub net name wallet) <- liftEitherIO $ readMarshalFile ctx fp+ (_, acc) <- insertAccount net ctx wallet name xpub+ return $ ResponseAccount acc++cmdExportAcc :: Ctx -> Config -> Maybe Text -> FilePath -> IO Response+cmdExportAcc ctx cfg nameM file =+ runDBResponse cfg $ do+ (_, acc) <- getAccountByName nameM+ checkPathFree file+ let xpub = accountXPubKey ctx acc+ net = accountNetwork acc+ name = dBAccountName acc+ wallet = accountWallet acc+ doc = PubKeyDoc xpub net name wallet+ liftIO $ writeMarshalFile ctx file doc+ return $ ResponseFile file++cmdRenameAcc :: Config -> Text -> Text -> IO Response+cmdRenameAcc cfg oldName newName =+ runDBResponse cfg $ do+ acc <- renameAccount oldName newName+ return $ ResponseAccount acc++cmdDeleteAcc :: Config -> Text -> Network -> HardPath -> IO Response+cmdDeleteAcc cfg name net path =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName $ Just name+ unless (accountNetwork acc == net) $+ throwError "The network of the account to delete did not match"+ accPath <- liftMaybe "HardPath" $ parseHard $ cs $ dBAccountDerivation acc+ unless (path == accPath) $+ throwError+ "The full derivation path of the account to delete did not match"+ lift $ deleteAccount accId+ return $+ ResponseAccResult acc True $+ "The account " <> name <> " has been deleted"++cmdAccounts :: Config -> Maybe Text -> IO Response+cmdAccounts cfg nameM =+ runDBResponse cfg $ do+ case nameM of+ Just _ -> do+ (_, acc) <- getAccountByName nameM+ return $ ResponseAccounts [acc]+ _ -> do+ accs <- lift getAccounts+ return $ ResponseAccounts $ snd <$> accs++cmdReceive :: Ctx -> Config -> Maybe Text -> Maybe Text -> IO Response+cmdReceive ctx cfg nameM labelM =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ addr <- genExtAddress ctx cfg accId $ fromMaybe "" labelM+ return $ ResponseAddress acc addr++cmdAddrs :: Config -> Maybe Text -> Page -> IO Response+cmdAddrs cfg nameM page =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ as <- lift $ addressPage accId page+ return $ ResponseAddresses acc as++cmdLabel :: Config -> Maybe Text -> Natural -> Text -> IO Response+cmdLabel cfg nameM idx lab =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ adr <- setAddrLabel accId (fromIntegral idx) lab+ return $ ResponseAddress acc adr++cmdTxs :: Ctx -> Config -> Maybe Text -> Page -> IO Response+cmdTxs ctx cfg nameM page =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ txInfos <- txsPage ctx accId page+ return $ ResponseTxs acc txInfos++cmdPrepareTx ::+ Ctx ->+ Config ->+ [(Text, Text)] ->+ Maybe Text ->+ AmountUnit ->+ Natural ->+ Natural ->+ Bool ->+ Natural ->+ Maybe FilePath ->+ IO Response+cmdPrepareTx ctx cfg rcpTxt nameM unit feeByte dust rcptPay minConf fileM =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ rcpts <- liftEither $ mapM (toRecipient net) rcpTxt+ gen <- liftIO initStdGen+ signDat <- buildTxSignData net ctx cfg gen accId rcpts feeByte dust rcptPay minConf+ txInfo <- liftEither $ parseTxSignData net ctx pub signDat+ txInfoL <- lift $ fillTxInfoLabels net txInfo+ for_ fileM checkPathFree+ _ <- importPendingTx net ctx accId signDat+ for_ fileM $ \file -> liftIO $ writeJsonFile file $ Json.toJSON signDat+ newAcc <- getAccountById accId+ return $ ResponseTx newAcc txInfoL+ where+ toRecipient net (a, v) = do+ addr <- textToAddrE net a+ val <- maybeToEither (cs $ badAmnt v) (readAmount unit v)+ return (addr, val)+ badAmnt v =+ "Could not parse the amount " <> v <> " as " <> showUnit unit 1++cmdPendingTxs :: Ctx -> Config -> Maybe Text -> Page -> IO Response+cmdPendingTxs ctx cfg nameM page =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ txInfos <- pendingTxPage ctx accId page+ return $ ResponseTxs acc txInfos++cmdReviewTx :: Ctx -> Config -> Maybe Text -> FilePath -> IO Response+cmdReviewTx ctx cfg nameM fp =+ runDBResponse cfg $ do+ (_, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ tsd <- liftEitherIO $ readJsonFile fp+ txInfo <- liftEither $ parseTxSignData net ctx pub tsd+ txInfoL <- lift $ fillTxInfoLabels net txInfo+ return $ ResponseTx acc txInfoL++cmdExportTx :: Config -> TxHash -> FilePath -> IO Response+cmdExportTx cfg nosigH fp =+ runDBResponse cfg $ do+ pendingTxM <- lift $ getPendingTx nosigH+ case pendingTxM of+ Just (_, tsd, _) -> do+ checkPathFree fp+ liftIO $ writeJsonFile fp $ Json.toJSON tsd+ return $ ResponseFile fp+ _ -> throwError "The pending transaction does not exist"++cmdImportTx :: Ctx -> Config -> Maybe Text -> FilePath -> IO Response+cmdImportTx ctx cfg nameM fp =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ tsd <- liftEitherIO $ readJsonFile fp+ txInfo <- liftEither $ parseTxSignData net ctx pub tsd+ txInfoL <- lift $ fillTxInfoLabels net txInfo+ _ <- importPendingTx net ctx accId tsd+ return $ ResponseTx acc txInfoL++cmdDeleteTx :: Ctx -> Config -> TxHash -> IO Response+cmdDeleteTx ctx cfg nosigH =+ runDBResponse cfg $ do+ (coins, addrs) <- deletePendingTx ctx nosigH+ return $ ResponseDeleteTx nosigH coins addrs++cmdSignTx ::+ Ctx ->+ Config ->+ Maybe Text ->+ Maybe TxHash ->+ Maybe FilePath ->+ Maybe FilePath ->+ Natural ->+ IO Response+cmdSignTx ctx cfg nameM nosigHM inputM outputM splitMnemIn =+ runDBResponse cfg $ do+ (tsd, online) <- parseSignInput nosigHM inputM outputM+ when online $+ throwError "The transaction is already online"+ when (txSignDataSigned tsd) $+ throwError "The transaction is already signed"+ (accId, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ idx = fromIntegral $ dBAccountIndex acc+ accPub = accountXPubKey ctx acc+ for_ outputM checkPathFree+ mnem <- askMnemonicPass splitMnemIn+ prvKey <- liftEither $ signingKey net ctx mnem idx+ let xpub = deriveXPubKey ctx prvKey+ unless (accPub == xpub) $+ throwError "The mnemonic did not match the provided account"+ (newSignData, txInfo) <- liftEither $ signWalletTx net ctx tsd prvKey+ txInfoL <- lift $ fillTxInfoLabels net txInfo+ when (isJust nosigHM) $ void $ importPendingTx net ctx accId newSignData+ for_ outputM $ \o -> liftIO $ writeJsonFile o $ Json.toJSON newSignData+ return $ ResponseTx acc txInfoL++parseSignInput ::+ (MonadUnliftIO m) =>+ Maybe TxHash ->+ Maybe FilePath ->+ Maybe FilePath ->+ ExceptT String (DB m) (TxSignData, Bool)+parseSignInput nosigHM inputM outputM =+ case (nosigHM, inputM, outputM) of+ (Nothing, Nothing, _) ->+ throwError+ "Provide either a TXHASH or both a --input file and a --output file"+ (Just _, Just _, _) ->+ throwError "Can not specify both a TXHASH and a --input file"+ (_, Just _, Nothing) ->+ throwError "When using a --input file, also provide a --output file"+ (Just h, _, _) -> do+ resM <- lift $ getPendingTx h+ case resM of+ Just (_, t, TxInfoPending _ _ online) -> return (t, online)+ _ -> throwError "The nosigHash does not exist in the wallet"+ (_, Just i, _) -> do+ exist <- liftIO $ D.doesFileExist i+ unless exist $ throwError "Input file does not exist"+ tsd <- liftEitherIO (readJsonFile i)+ return (tsd, False)++cmdCoins :: Config -> Maybe Text -> Page -> IO Response+cmdCoins cfg nameM page =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ coins <- coinPage net accId page+ return $ ResponseCoins acc coins++cmdSendTx :: Ctx -> Config -> TxHash -> IO Response+cmdSendTx ctx cfg nosigH =+ runDBResponse cfg $ do+ tsdM <- lift $ getPendingTx nosigH+ case tsdM of+ Just (accId, tsd@(TxSignData signedTx _ _ _ signed), _) -> do+ acc <- getAccountById accId+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ txInfo <- liftEither $ parseTxSignData net ctx pub tsd+ txInfoL <- lift $ fillTxInfoLabels net txInfo+ let verify = verifyTxInfo net ctx signedTx txInfoL+ unless (signed && verify) $ throwError "The transaction is not signed"+ checkHealth ctx net cfg+ let host = apiHost net cfg+ Store.TxId netTxId <- liftExcept $ apiCall ctx host (PostTx signedTx)+ unless (netTxId == txHash signedTx) $+ throwError $+ "The server returned the wrong TxHash: "+ <> cs (txHashToHex netTxId)+ _ <- lift $ setPendingTxOnline nosigH+ lift $ insertRawTx signedTx+ return $ ResponseTx acc $ setTxInfoOnline txInfoL+ _ -> throwError "The nosigHash does not exist in the wallet"++setTxInfoOnline :: TxInfo -> TxInfo+setTxInfoOnline txInfo =+ case txInfoPending txInfo of+ Just p -> txInfo {txInfoPending = Just p {pendingOnline = True}}+ _ -> txInfo++cmdSyncAcc :: Ctx -> Config -> Maybe Text -> Bool -> IO Response+cmdSyncAcc ctx cfg nameM full =+ runDBResponse cfg $ do+ accs <-+ case nameM of+ Just _ -> (:[]) <$> getAccountByName nameM+ _ -> lift getAccounts+ when (null accs) $ throwError "There are no accounts in the wallet"+ res <- forM accs $ \(accId, acc) -> do+ let net = accountNetwork acc+ sync ctx cfg net accId full+ return $ ResponseSync res++cmdDiscoverAccount :: Ctx -> Config -> Maybe Text -> IO Response+cmdDiscoverAccount ctx cfg nameM = do+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ checkHealth ctx net cfg+ (e, i) <- discoverAddrs net ctx cfg pub+ discoverAccGenAddrs ctx cfg accId AddrExternal $ fromIntegral e+ discoverAccGenAddrs ctx cfg accId AddrInternal $ fromIntegral i+ -- Perform a full sync after discovery+ res <- sync ctx cfg net accId True+ return $ ResponseSync [res]++cmdBackup :: Ctx -> Config -> FilePath -> IO Response+cmdBackup ctx cfg fp =+ runDBResponse cfg $ do+ backup <- createBackup ctx+ checkPathFree fp+ liftIO $ writeJsonFile fp $ marshalValue ctx backup+ return $ ResponseFile fp++cmdRestore :: Ctx -> Config -> FilePath -> IO Response+cmdRestore ctx cfg fp =+ runDBResponse cfg $ do+ backup <- liftEitherIO $ readMarshalFile ctx fp+ let f (SyncRes a _ _ t c) = (a, t, c)+ ResponseRestore . (f <$>) <$> restoreBackup ctx cfg backup++cmdVersion :: Config -> IO Response+cmdVersion cfg = do+ runDBResponse cfg $ do+ dbv <- lift getVersion+ return $ ResponseVersion currentVersionStr (cs $ verString dbv)++prepareSweep ::+ Ctx ->+ Config ->+ Maybe Text ->+ FilePath ->+ [Text] ->+ Maybe FilePath ->+ Natural ->+ Natural ->+ IO Response+prepareSweep ctx cfg nameM prvKeyFile sweepToT outputM feeByte dust =+ runDBResponse cfg $ do+ (accId, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ secKeys <- parseSecKeysFile net <$> liftIO (readFileWords prvKeyFile)+ sweepTo <- liftEither $ mapM (textToAddrE net) sweepToT+ checkHealth ctx net cfg+ tsd <- buildSweepSignData net ctx cfg accId secKeys sweepTo feeByte dust+ txInfo <- liftEither $ parseTxSignData net ctx pub tsd+ txInfoL <- lift $ fillTxInfoLabels net txInfo+ for_ outputM checkPathFree+ _ <- importPendingTx net ctx accId tsd+ for_ outputM $ \file -> liftIO $ writeJsonFile file $ Json.toJSON tsd+ return $ ResponseTx acc txInfoL++signSweep ::+ Ctx ->+ Config ->+ Maybe Text ->+ Maybe TxHash ->+ Maybe FilePath ->+ Maybe FilePath ->+ FilePath ->+ IO Response+signSweep ctx cfg nameM nosigHM inputM outputM keyFile =+ runDBResponse cfg $ do+ (tsd, online) <- parseSignInput nosigHM inputM outputM+ when online $+ throwError "The transaction is already online"+ when (txSignDataSigned tsd) $+ throwError "The transaction is already signed"+ (accId, acc) <- getAccountByName nameM+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ for_ outputM checkPathFree+ -- Read the file containing the private keys+ secKeys <- parseSecKeysFile net <$> liftIO (readFileWords keyFile)+ when (null secKeys) $ throwError "No private keys to sign"+ -- Sign the transactions+ (newTsd, txInfo) <- liftEither $ signTxWithKeys net ctx tsd pub secKeys+ txInfoL <- lift $ fillTxInfoLabels net txInfo+ when (isJust nosigHM) $ void $ importPendingTx net ctx accId newTsd+ for_ outputM $ \o -> liftIO $ writeJsonFile o $ Json.toJSON newTsd+ return $ ResponseTx acc txInfoL++rollDice :: Natural -> IO Response+rollDice n = do+ (res, origEnt) <- go [] ""+ return $ ResponseRollDice (take (fromIntegral n) res) origEnt+ where+ go acc orig+ | length acc >= fromIntegral n = return (acc, orig)+ | otherwise = do+ (origEnt, sysEnt) <- systemEntropy 1+ go (word8ToBase6 (head $ BS.unpack sysEnt) <> acc) origEnt++-- Haskeline Helpers --++askInputLineHidden :: String -> IO String+askInputLineHidden message = do+ inputM <-+ Haskeline.runInputT Haskeline.defaultSettings $+ Haskeline.getPassword (Just '*') message+ maybe+ (error "No action due to EOF")+ return+ inputM++askInputLine :: String -> IO String+askInputLine message = do+ inputM <-+ Haskeline.runInputT Haskeline.defaultSettings $+ Haskeline.getInputLine message+ maybe+ (error "No action due to EOF")+ return+ inputM++askMnemonicWords :: String -> IO Mnemonic+askMnemonicWords txt = do+ mnm <- askInputLineHidden txt+ case fromMnemonic (cs mnm) of -- validate the mnemonic+ Right _ -> return $ cs mnm+ Left _ -> do+ liftIO $ putStrLn "Invalid mnemonic"+ askMnemonicWords txt++askMnemonicPass :: (MonadError String m, MonadIO m) => Natural -> m MnemonicPass+askMnemonicPass splitMnemIn = do+ mnm <-+ if splitMnemIn == 1+ then liftIO $ askMnemonicWords "Enter your mnemonic words: "+ else do+ ms <- forM [1 .. splitMnemIn] $ \n ->+ liftIO $ askMnemonicWords $ "Split mnemonic part #" <> show n <> ": "+ liftEither $ mergeMnemonicParts ms+ passStr <- liftIO askPassword+ return+ MnemonicPass+ { mnemonicWords = mnm,+ mnemonicPass = cs passStr+ }++askPassword :: IO String+askPassword = do+ pass <- askInputLineHidden "Mnemonic passphrase or leave empty: "+ if null pass+ then return pass+ else do+ pass2 <- askInputLineHidden "Repeat your mnemonic passphrase: "+ if pass == pass2+ then return pass+ else do+ putStrLn "The passphrases did not match"+ askPassword
@@ -0,0 +1,114 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}++module Haskoin.Wallet.Config where++import Control.Monad (unless)+import Control.Monad.Except+import Control.Monad.Reader (MonadIO (..))+import Data.Aeson+import Data.Default+import Data.String.Conversions (cs)+import Data.Text (Text)+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Store.WebClient+import Haskoin.Wallet.FileIO+import Haskoin.Wallet.Util+import Numeric.Natural (Natural)+import qualified System.Directory as D++hwDataDirectory :: IO FilePath+hwDataDirectory = do+ dir <- D.getAppUserDataDirectory "hw"+ D.createDirectoryIfMissing True dir+ return dir++databaseFile :: Config -> IO FilePath+databaseFile cfg =+ case configDatabaseFile cfg of+ Just dbFile -> return dbFile+ _ -> do+ dir <- hwDataDirectory+ return $ dir </> "accounts.sqlite"++data Config = Config+ { configHost :: Text,+ configGap :: Natural,+ configRecoveryGap :: Natural,+ configAddrBatch :: Natural,+ configTxBatch :: Natural,+ configCoinBatch :: Natural,+ configTxFullBatch :: Natural,+ configDatabaseFile :: Maybe FilePath+ }+ deriving (Eq, Show)++instance FromJSON Config where+ parseJSON =+ withObject "config" $ \o ->+ Config+ <$> o .: "host"+ <*> o .: "gap"+ <*> o .: "recovery-gap"+ <*> o .: "addr-batch"+ <*> o .: "tx-batch"+ <*> o .: "coin-batch"+ <*> o .: "tx-full-batch"+ <*> o .:? "database-file"++instance ToJSON Config where+ toJSON cfg =+ object+ [ "host" .= configHost cfg,+ "gap" .= configGap cfg,+ "recovery-gap" .= configRecoveryGap cfg,+ "addr-batch" .= configAddrBatch cfg,+ "tx-batch" .= configTxBatch cfg,+ "coin-batch" .= configCoinBatch cfg,+ "tx-full-batch" .= configTxFullBatch cfg,+ "database-file" .= configDatabaseFile cfg+ ]++instance Default Config where+ def =+ Config+ { configHost = cs (def :: ApiConfig).host,+ configGap = 20,+ configRecoveryGap = 40,+ configAddrBatch = 100,+ configTxBatch = 100,+ configCoinBatch = 100,+ configTxFullBatch = 100,+ configDatabaseFile = Nothing+ }++initConfig :: IO Config+initConfig = do+ dir <- hwDataDirectory+ let configFile = dir </> "config.json"+ exists <- D.doesFileExist configFile+ unless exists $ writeJsonFile configFile $ toJSON (def :: Config)+ resE <- readJsonFile configFile+ either (error "Could not read config.json") return resE++apiHost :: Network -> Config -> ApiConfig+apiHost net = ApiConfig net . cs . configHost++-- Utilities --++checkHealth ::+ (MonadIO m) =>+ Ctx ->+ Network ->+ Config ->+ ExceptT String m ()+checkHealth ctx net cfg = do+ let host = apiHost net cfg+ health <- liftExcept $ apiCall ctx host GetHealth+ unless (Store.isOK health) $+ throwError "The indexer health check has failed"
@@ -0,0 +1,1424 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE EmptyDataDecls #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Haskoin.Wallet.Database where++import Conduit (MonadUnliftIO, ResourceT)+import Control.Arrow (Arrow (second))+import Control.Monad+import Control.Monad.Except+import Control.Monad.IO.Class (MonadIO (liftIO))+import Control.Monad.Logger (NoLoggingT)+import Control.Monad.Reader (MonadTrans (lift), ReaderT)+import Control.Monad.Trans.Maybe+import Data.Aeson+import qualified Data.Aeson as Json+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.Either (fromRight)+import Data.List (find, nub, partition)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (fromJust, fromMaybe, mapMaybe)+import qualified Data.Serialize as S+import Data.String.Conversions (cs)+import Data.Text (Text)+import Data.Time (UTCTime, getCurrentTime)+import Data.Word (Word64)+import Database.Esqueleto.Legacy as E+import qualified Database.Persist as P+import Database.Persist.Sqlite (runSqlite)+import Database.Persist.TH+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Wallet.Config+import Haskoin.Wallet.FileIO+import Haskoin.Wallet.Migration.SemVersion+import Haskoin.Wallet.TxInfo+import Haskoin.Wallet.Util (Page (Page), textToAddrE)+import Numeric.Natural (Natural)++{- SQL Table Definitions -}++share+ [mkPersist sqlSettings, mkMigrate "migrateAll"]+ [persistLowerCase|++DBVersion+ version Text+ Primary version+ deriving Show++DBWallet+ fingerprint Text+ Primary fingerprint+ deriving Show++DBAccount+ name Text+ wallet DBWalletId+ index Int+ network Text+ derivation Text+ external Int+ internal Int+ xPubKey Text+ balanceConfirmed Word64+ balanceUnconfirmed Word64+ balanceCoins Word64+ created UTCTime default=CURRENT_TIME+ Primary wallet derivation+ UniqueName name+ UniqueXPubKey xPubKey+ UniqueNetworkId wallet network index+ deriving Show+ deriving Eq++DBAddress+ index Int+ accountWallet DBWalletId+ accountDerivation Text+ derivation Text+ address Text+ label Text+ balanceConfirmed Word64+ balanceUnconfirmed Word64+ balanceCoins Word64+ balanceTxs Word64+ balanceReceived Word64+ internal Bool+ free Bool+ created UTCTime default=CURRENT_TIME+ Primary accountWallet accountDerivation derivation+ UniqueAddress address+ Foreign DBAccount fk_wallet_derivation accountWallet accountDerivation+ deriving Show+ deriving Eq++DBTxInfo+ accountWallet DBWalletId+ accountDerivation Text+ txid Text+ blockRef ByteString+ confirmed Bool+ blob ByteString+ created UTCTime default=CURRENT_TIME+ Primary accountWallet accountDerivation txid+ Foreign DBAccount fk_wallet_derivation accountWallet accountDerivation+ deriving Show+ deriving Eq++DBCoin+ accountWallet DBWalletId+ accountDerivation Text+ outpoint Text+ address Text+ value Word64+ blockRef ByteString+ blob ByteString+ confirmed Bool+ locked Bool+ created UTCTime default=CURRENT_TIME+ Primary outpoint+ Foreign DBAccount fk_wallet_derivation accountWallet accountDerivation+ deriving Show+ deriving Eq++DBRawTx+ hash Text+ blob ByteString+ Primary hash+ deriving Show+ deriving Eq++DBPendingTx+ accountWallet DBWalletId+ accountDerivation Text+ nosigHash Text+ blob ByteString+ online Bool+ created UTCTime default=CURRENT_TIME+ Primary nosigHash+ Foreign DBAccount fk_wallet_derivation accountWallet accountDerivation+ deriving Show+ deriving Eq++DBBest+ network Text+ bestBlock Text+ bestHeight Int+ Primary network+ deriving Show+|]++type DB m = ReaderT SqlBackend (NoLoggingT (ResourceT m))++runDB :: (MonadUnliftIO m) => Config -> DB m a -> m a+runDB cfg action = do+ dbFile <- liftIO $ databaseFile cfg+ runSqlite (cs dbFile) action++globalMigration :: (MonadUnliftIO m) => DB m ()+globalMigration = void $ runMigrationQuiet migrateAll++{- Meta -}++-- Database versions use major.minor only. Patch versions are ignored.+setVersion :: (MonadUnliftIO m) => SemVersion -> DB m ()+setVersion semVer = do+ verM <- selectOne $ from return+ let verTxt = cs $ verString $ toMinor semVer+ case verM of+ Nothing -> P.insert_ $ DBVersion verTxt+ Just (Entity k _) -> P.update k [DBVersionVersion P.=. verTxt]++getVersion :: (MonadUnliftIO m) => DB m SemVersion+getVersion = do+ resM <- selectOne . from $ \v -> return $ v ^. DBVersionVersion+ return $ parseSemVersion . cs . unValue $ fromJust resM++updateBest ::+ (MonadUnliftIO m) => Network -> BlockHash -> BlockHeight -> DB m ()+updateBest net hash height = do+ let netT = cs net.name+ key = DBBestKey netT+ P.repsert key $ DBBest netT (blockHashToHex hash) (fromIntegral height)++getBest ::+ (MonadUnliftIO m) => Network -> DB m (Maybe (BlockHash, BlockHeight))+getBest net = do+ let netT = cs net.name+ resM <- P.get $ DBBestKey netT+ return $ do+ DBBest _ a b <- resM+ hash <- hexToBlockHash a+ return (hash, fromIntegral b)++{- Accounts -}++instance ToJSON DBAccount where+ toJSON acc =+ object+ [ "name" .= dBAccountName acc,+ "wallet" .= dBAccountWallet acc,+ "index" .= dBAccountIndex acc,+ "network" .= dBAccountNetwork acc,+ "derivation" .= dBAccountDerivation acc,+ "external" .= dBAccountExternal acc,+ "internal" .= dBAccountInternal acc,+ "xPubKey" .= dBAccountXPubKey acc,+ "balance" .= toJSON (AccountBalance confirm unconfirm coins),+ "created" .= dBAccountCreated acc+ ]+ where+ confirm = dBAccountBalanceConfirmed acc+ unconfirm = dBAccountBalanceUnconfirmed acc+ coins = dBAccountBalanceCoins acc++instance FromJSON DBAccount where+ parseJSON =+ withObject "DBAccount" $ \o -> do+ bal <- o .: "balance"+ DBAccount+ <$> o .: "name"+ <*> o .: "wallet"+ <*> o .: "index"+ <*> o .: "network"+ <*> o .: "derivation"+ <*> o .: "external"+ <*> o .: "internal"+ <*> o .: "xPubKey"+ <*> (bal .: "confirmed")+ <*> (bal .: "unconfirmed")+ <*> (bal .: "coins")+ <*> o .: "created"++data AccountBalance = AccountBalance+ { -- | confirmed balance+ accBalanceConfirmed :: !Word64,+ -- | unconfirmed balance+ accBalanceUnconfirmed :: !Word64,+ -- | number of unspent outputs+ accBalanceCoins :: !Word64+ }+ deriving (Show, Read, Eq, Ord)++instance ToJSON AccountBalance where+ toJSON b =+ object+ [ "confirmed" .= accBalanceConfirmed b,+ "unconfirmed" .= accBalanceUnconfirmed b,+ "coins" .= accBalanceCoins b+ ]++instance FromJSON AccountBalance where+ parseJSON =+ withObject "accountbalance" $ \o ->+ AccountBalance+ <$> o .: "confirmed"+ <*> o .: "unconfirmed"+ <*> o .: "coins"++accountIndex :: DBAccount -> Natural+accountIndex = fromIntegral . dBAccountIndex++accountNetwork :: DBAccount -> Network+accountNetwork =+ fromMaybe (error "Invalid Network in database")+ . netByName+ . cs+ . dBAccountNetwork++accountWallet :: DBAccount -> Fingerprint+accountWallet acc =+ let (DBWalletKey walletFP) = dBAccountWallet acc+ in fromRight (error "Invalid WalletId in database") $+ textToFingerprint walletFP++accountXPubKey :: Ctx -> DBAccount -> XPubKey+accountXPubKey ctx acc =+ fromMaybe (error "Invalid XPubKey in database") $+ xPubImport (accountNetwork acc) ctx (dBAccountXPubKey acc)++nextAccountDeriv :: (MonadUnliftIO m) => Fingerprint -> Network -> DB m Natural+nextAccountDeriv walletFP net = do+ let walletId = DBWalletKey $ fingerprintToText walletFP+ idxs <-+ select . from $ \a -> do+ where_ $+ a ^. DBAccountNetwork ==. val (cs net.name)+ &&. a ^. DBAccountWallet ==. val walletId+ return $ a ^. DBAccountIndex+ return $ smallestUnused $ fromIntegral . unValue <$> idxs++smallestUnused :: [Natural] -> Natural+smallestUnused xs = fromJust $ find (not . (`elem` xs)) [0 ..]++existsAccount :: (MonadUnliftIO m) => Text -> DB m Bool+existsAccount name = P.existsBy $ UniqueName name++existsXPubKey :: (MonadUnliftIO m) => Network -> Ctx -> XPubKey -> DB m Bool+existsXPubKey net ctx key = P.existsBy $ UniqueXPubKey $ xPubExport net ctx key++getWalletOrCreate :: (MonadUnliftIO m) => Fingerprint -> DB m DBWalletId+getWalletOrCreate fp = do+ let key = DBWalletKey $ fingerprintToText fp+ walletM <- P.get key+ case walletM of+ Just _ -> return key+ _ -> P.insert $ DBWallet $ fingerprintToText fp++insertAccount ::+ (MonadUnliftIO m) =>+ Network ->+ Ctx ->+ Fingerprint ->+ Text ->+ XPubKey ->+ ExceptT String (DB m) (DBAccountId, DBAccount)+insertAccount net ctx walletFP name xpub = do+ existsName <- lift $ existsAccount name+ if existsName+ then throwError $ "Account " <> cs name <> " already exists"+ else do+ existsKey <- lift $ existsXPubKey net ctx xpub+ if existsKey+ then throwError "The XPubKey already exists"+ else do+ time <- liftIO getCurrentTime+ walletId <- lift $ getWalletOrCreate walletFP+ let path = bip44Deriv net $ fromIntegral $ xPubChild xpub+ idx = fromIntegral $ xPubIndex xpub+ account =+ DBAccount+ { dBAccountName = name,+ dBAccountWallet = walletId,+ dBAccountIndex = idx,+ dBAccountNetwork = cs net.name,+ dBAccountDerivation = cs $ pathToStr path,+ dBAccountExternal = 0,+ dBAccountInternal = 0,+ dBAccountXPubKey = xPubExport net ctx xpub,+ dBAccountBalanceConfirmed = 0,+ dBAccountBalanceUnconfirmed = 0,+ dBAccountBalanceCoins = 0,+ dBAccountCreated = time+ }+ key <- lift $ P.insert account+ return (key, account)++deleteAccount :: (MonadUnliftIO m) => DBAccountId -> DB m ()+deleteAccount accId@(DBAccountKey accWallet accDeriv) = do+ delete . from $ \a ->+ where_ $+ a ^. DBAddressAccountWallet ==. val accWallet+ &&. a ^. DBAddressAccountDerivation ==. val accDeriv+ delete . from $ \t ->+ where_ $+ t ^. DBTxInfoAccountWallet ==. val accWallet+ &&. t ^. DBTxInfoAccountDerivation ==. val accDeriv+ delete . from $ \c ->+ where_ $+ c ^. DBCoinAccountWallet ==. val accWallet+ &&. c ^. DBCoinAccountDerivation ==. val accDeriv+ delete . from $ \p ->+ where_ $+ p ^. DBPendingTxAccountWallet ==. val accWallet+ &&. p ^. DBPendingTxAccountDerivation ==. val accDeriv+ P.delete accId++-- When a name is provided, get that account or throw an error if it doesn't+-- exist. When no name is provided, return the account only if there is one+-- account.+getAccountByName ::+ (MonadUnliftIO m) =>+ Maybe Text ->+ ExceptT String (DB m) (DBAccountId, DBAccount)+getAccountByName (Just name) = do+ aM <- lift $ P.getBy $ UniqueName name+ case aM of+ Just a -> return (entityKey a, entityVal a)+ _ -> throwError $ "The account " <> cs name <> " does not exist"+getAccountByName Nothing = do+ as <- lift getAccounts+ case as of+ [a] -> return a+ [] -> throwError "There are no accounts in the wallet"+ _ -> throwError "Specify which account to use"++getAccountById ::+ (MonadUnliftIO m) => DBAccountId -> ExceptT String (DB m) DBAccount+getAccountById accId = liftMaybe "Invalid account" =<< lift (P.get accId)++getAccounts :: (MonadUnliftIO m) => DB m [(DBAccountId, DBAccount)]+getAccounts =+ (go <$>)+ <$> P.selectList+ []+ [P.Asc DBAccountWallet, P.Asc DBAccountNetwork, P.Asc DBAccountIndex]+ where+ go a = (entityKey a, entityVal a)++getAccountNames :: (MonadUnliftIO m) => DB m [Text]+getAccountNames = do+ res <- select . from $ \a -> do+ orderBy [asc $ a ^. DBAccountCreated]+ return $ a ^. DBAccountName+ return $ unValue <$> res++renameAccount ::+ (MonadUnliftIO m) => Text -> Text -> ExceptT String (DB m) DBAccount+renameAccount oldName newName+ | oldName == newName = throwError "Old and new names are the same"+ | otherwise = do+ e <- lift $ existsAccount newName+ if e+ then throwError $ "The account " <> cs newName <> " already exists"+ else do+ c <-+ lift . updateCount $ \a -> do+ set a [DBAccountName =. val newName]+ where_ $ a ^. DBAccountName ==. val oldName+ if c == 0+ then throwError $ "The account " <> cs oldName <> " does not exist"+ else snd <$> getAccountByName (Just newName)++updateAccountBalances :: (MonadUnliftIO m) => DBAccountId -> DB m DBAccount+updateAccountBalances accId@(DBAccountKey wallet accDeriv) = do+ confirm <- selectSum DBAddressBalanceConfirmed+ unconfirm <- selectSum DBAddressBalanceUnconfirmed+ coins <- selectSum DBAddressBalanceCoins+ update $ \a -> do+ set+ a+ [ DBAccountBalanceConfirmed =. val (unpack confirm),+ DBAccountBalanceUnconfirmed =. val (unpack unconfirm),+ DBAccountBalanceCoins =. val (unpack coins)+ ]+ where_ $ a ^. DBAccountId ==. val accId+ fromJust <$> P.get accId+ where+ unpack m = fromMaybe 0 $ unValue $ fromMaybe (Value $ Just 0) m+ selectSum field =+ selectOne . from $ \a -> do+ where_ $+ a ^. DBAddressAccountWallet ==. val wallet+ &&. a ^. DBAddressAccountDerivation ==. val accDeriv+ return $ sum_ $ a ^. field++{- Addresses -}++data AddrType = AddrInternal | AddrExternal+ deriving (Eq, Show)++isInternal :: AddrType -> Bool+isInternal AddrInternal = True+isInternal AddrExternal = False++dBAccountCount :: AddrType -> DBAccount -> Int+dBAccountCount AddrInternal = dBAccountInternal+dBAccountCount AddrExternal = dBAccountExternal++dBAccountField :: AddrType -> EntityField DBAccount Int+dBAccountField AddrInternal = DBAccountInternal+dBAccountField AddrExternal = DBAccountExternal++addrDeriv :: AddrType -> SoftPath+addrDeriv AddrInternal = intDeriv+addrDeriv AddrExternal = extDeriv++data AddrFree = AddrFree | AddrBusy+ deriving (Eq, Show)++isAddrFree :: AddrFree -> Bool+isAddrFree AddrFree = True+isAddrFree AddrBusy = False++instance ToJSON DBAddress where+ toJSON addr =+ object+ [ "index" .= dBAddressIndex addr,+ "wallet" .= dBAddressAccountWallet addr,+ "account" .= dBAddressAccountDerivation addr,+ "derivation" .= dBAddressDerivation addr,+ "address" .= dBAddressAddress addr,+ "label" .= dBAddressLabel addr,+ "balance"+ .= AddressBalance+ (dBAddressBalanceConfirmed addr)+ (dBAddressBalanceUnconfirmed addr)+ (dBAddressBalanceCoins addr)+ (dBAddressBalanceTxs addr)+ (dBAddressBalanceReceived addr),+ "internal" .= dBAddressInternal addr,+ "free" .= dBAddressFree addr,+ "created" .= dBAddressCreated addr+ ]++instance FromJSON DBAddress where+ parseJSON =+ withObject "DBAddress" $ \o -> do+ bal <- o .: "balance"+ DBAddress+ <$> o .: "index"+ <*> o .: "wallet"+ <*> o .: "account"+ <*> o .: "derivation"+ <*> o .: "address"+ <*> o .: "label"+ <*> bal .: "confirmed"+ <*> bal .: "unconfirmed"+ <*> bal .: "coins"+ <*> bal .: "txs"+ <*> bal .: "received"+ <*> o .: "internal"+ <*> o .: "free"+ <*> o .: "created"++data AddressBalance = AddressBalance+ { -- | confirmed balance+ addrBalanceConfirmed :: !Word64,+ -- | unconfirmed balance+ addrBalanceUnconfirmed :: !Word64,+ -- | number of unspent outputs+ addrBalanceCoins :: !Word64,+ -- | number of transactions+ addrBalanceTxs :: !Word64,+ -- | total amount from all outputs in this address+ addrBalanceReceived :: !Word64+ }+ deriving (Show, Read, Eq, Ord)++instance ToJSON AddressBalance where+ toJSON b =+ object+ [ "confirmed" .= addrBalanceConfirmed b,+ "unconfirmed" .= addrBalanceUnconfirmed b,+ "coins" .= addrBalanceCoins b,+ "txs" .= addrBalanceTxs b,+ "received" .= addrBalanceReceived b+ ]++instance FromJSON AddressBalance where+ parseJSON =+ withObject "accountbalance" $ \o ->+ AddressBalance+ <$> o .: "confirmed"+ <*> o .: "unconfirmed"+ <*> o .: "coins"+ <*> o .: "txs"+ <*> o .: "received"++updateAddressBalances ::+ (MonadUnliftIO m) =>+ Network ->+ [Store.Balance] ->+ ExceptT String (DB m) ()+updateAddressBalances net storeBals =+ forM_ storeBals $ \s -> do+ addrT <-+ liftEither $ maybeToEither "Invalid Address" (addrToText net s.address)+ lift . update $ \a -> do+ set+ a+ [ DBAddressBalanceConfirmed =. val s.confirmed,+ DBAddressBalanceUnconfirmed =. val s.unconfirmed,+ DBAddressBalanceCoins =. val s.utxo,+ DBAddressBalanceTxs =. val s.txs,+ DBAddressBalanceReceived =. val s.received+ ]+ where_ $ a ^. DBAddressAddress ==. val addrT++insertAddress ::+ (MonadUnliftIO m) =>+ Network ->+ DBAccountId ->+ SoftPath ->+ Address ->+ AddrFree ->+ ExceptT String (DB m) DBAddress+insertAddress net (DBAccountKey wallet accDeriv) deriv addr free = do+ time <- liftIO getCurrentTime+ addrT <- liftEither $ maybeToEither "Invalid Address" (addrToText net addr)+ let label = if isIntPath deriv then "Internal Address" else ""+ derivS = cs $ pathToStr deriv+ dbAddr =+ DBAddress+ (fromIntegral $ pathIndex deriv)+ wallet+ accDeriv+ derivS+ addrT+ label+ 0+ 0+ 0+ 0+ 0+ (isIntPath deriv)+ (isAddrFree free)+ time+ lift $ P.insert_ dbAddr+ return dbAddr++-- This is an internal function+genNextAddress ::+ (MonadUnliftIO m) =>+ Ctx ->+ Config ->+ DBAccountId ->+ AddrType ->+ AddrFree ->+ Bool ->+ ExceptT String (DB m) DBAddress+genNextAddress ctx cfg accId addrType addrFree testGap = do+ acc <- getAccountById accId+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ nextIdx = dBAccountCount addrType acc+ deriv = addrDeriv addrType+ when testGap $ checkGap cfg accId nextIdx addrType+ let (addr, _) = derivePathAddr ctx pub deriv (fromIntegral nextIdx)+ dbAddr <-+ insertAddress net accId (deriv :/ fromIntegral nextIdx) addr addrFree+ lift $ P.update accId [dBAccountField addrType P.=. nextIdx + 1]+ return dbAddr++checkGap ::+ (MonadUnliftIO m) =>+ Config ->+ DBAccountId ->+ Int ->+ AddrType ->+ ExceptT String (DB m) ()+checkGap cfg accId addrIdx addrType = do+ let gap = configGap cfg+ usedIdxM <- lift $ bestAddrWithFunds accId addrType+ let usedIdx = maybe 0 (+ 1) usedIdxM+ when (addrIdx >= usedIdx + fromIntegral gap) $+ throwError $+ "Can not generate addresses beyond the gap of " <> show gap++-- Highest address with a positive transaction count+bestAddrWithFunds ::+ (MonadUnliftIO m) => DBAccountId -> AddrType -> DB m (Maybe Int)+bestAddrWithFunds (DBAccountKey wallet accDeriv) addrType = do+ resM <- (flatMaybe <$>) . selectOne . from $ \a -> do+ where_ $+ a ^. DBAddressAccountWallet ==. val wallet+ &&. a ^. DBAddressAccountDerivation ==. val accDeriv+ &&. a ^. DBAddressInternal ==. val (isInternal addrType)+ &&. a ^. DBAddressBalanceTxs >. val 0+ return $ max_ $ a ^. DBAddressIndex+ return $ fromIntegral <$> resM++-- Generate the discovered external and internal addresses+discoverAccGenAddrs ::+ (MonadUnliftIO m) =>+ Ctx ->+ Config ->+ DBAccountId ->+ AddrType ->+ Int ->+ ExceptT String (DB m) ()+discoverAccGenAddrs ctx cfg accId addrType newAddrCnt = do+ acc <- getAccountById accId+ let oldAddrCnt = dBAccountCount addrType acc+ cnt = max 0 $ newAddrCnt - oldAddrCnt+ -- False: Don't check the gap while discovering+ replicateM_ cnt $ genNextAddress ctx cfg accId addrType AddrBusy False++genExtAddress ::+ (MonadUnliftIO m) =>+ Ctx ->+ Config ->+ DBAccountId ->+ Text ->+ ExceptT String (DB m) DBAddress+genExtAddress ctx cfg accId label = do+ -- True: check the gap+ addr <- genNextAddress ctx cfg accId AddrExternal AddrBusy True+ setAddrLabel accId (dBAddressIndex addr) label++setAddrLabel ::+ (MonadUnliftIO m) =>+ DBAccountId ->+ Int ->+ Text ->+ ExceptT String (DB m) DBAddress+setAddrLabel accId@(DBAccountKey wallet accDeriv) idx label = do+ acc <- getAccountById accId+ let path = cs $ pathToStr $ extDeriv :/ fromIntegral idx+ aKey = DBAddressKey wallet accDeriv path+ unless (fromIntegral idx < dBAccountExternal acc) $+ throwError $+ "Address " <> show idx <> " does not exist"+ lift $ P.updateGet aKey [DBAddressLabel P.=. label]++nextFreeIntAddr ::+ (MonadUnliftIO m) =>+ Ctx ->+ Config ->+ DBAccountId ->+ ExceptT String (DB m) DBAddress+nextFreeIntAddr ctx cfg accId@(DBAccountKey wallet accDeriv) = do+ resM <- lift . selectOne . from $ \a -> do+ where_ $+ a ^. DBAddressAccountWallet ==. val wallet+ &&. a ^. DBAddressAccountDerivation ==. val accDeriv+ &&. a ^. DBAddressInternal ==. val True+ &&. a ^. DBAddressFree ==. val True+ orderBy [asc $ a ^. DBAddressIndex]+ limit 1+ return a+ case resM of+ Just (Entity _ a) -> return a+ -- True: Check the gap+ Nothing -> genNextAddress ctx cfg accId AddrInternal AddrFree True++fromDBAddr :: Network -> DBAddress -> Either String (Address, SoftPath)+fromDBAddr net addrDB = do+ let addrT = dBAddressAddress addrDB+ derivT = dBAddressDerivation addrDB+ deriv <- maybeToEither "fromDBAddress deriv" $ parseSoft $ cs derivT+ addr <- maybeToEither "fromDBAddress addr" $ textToAddr net addrT+ return (addr, deriv)++setAddrsFree :: (MonadUnliftIO m) => AddrFree -> [Text] -> DB m Natural+setAddrsFree free addrs = do+ (fromIntegral <$>) . updateCount $ \a -> do+ set a [DBAddressFree =. val (isAddrFree free)]+ where_ $+ a ^. DBAddressAddress `in_` valList addrs+ &&. (a ^. DBAddressBalanceTxs ==. val 0)++addressPage :: (MonadUnliftIO m) => DBAccountId -> Page -> DB m [DBAddress]+addressPage (DBAccountKey wallet accDeriv) (Page lim off) = do+ as <-+ select $+ from $ \a -> do+ where_ $+ a ^. DBAddressAccountWallet ==. val wallet+ &&. a ^. DBAddressAccountDerivation ==. val accDeriv+ &&. a ^. DBAddressInternal ==. val False+ orderBy [desc (a ^. DBAddressIndex)]+ limit $ fromIntegral lim+ offset $ fromIntegral off+ return a+ return $ entityVal <$> as++allAddressesMap ::+ (MonadUnliftIO m) =>+ Network ->+ DBAccountId ->+ ExceptT String (DB m) (Map Address SoftPath, Map Address AddressBalance)+allAddressesMap net (DBAccountKey wallet accDeriv) = do+ dbRes <-+ lift . select $+ from $ \a -> do+ where_ $+ a ^. DBAddressAccountWallet ==. val wallet+ &&. a ^. DBAddressAccountDerivation ==. val accDeriv+ return a+ res <-+ forM dbRes $ \(Entity _ dbAddr) -> do+ a <- liftEither $ textToAddrE net $ dBAddressAddress dbAddr+ d <-+ liftEither . maybeToEither "parsePath failed" $+ parseSoft . cs $+ dBAddressDerivation dbAddr+ let b =+ AddressBalance+ (dBAddressBalanceConfirmed dbAddr)+ (dBAddressBalanceUnconfirmed dbAddr)+ (dBAddressBalanceCoins dbAddr)+ (dBAddressBalanceTxs dbAddr)+ (dBAddressBalanceReceived dbAddr)+ return ((a, d), (a, b))+ return (Map.fromList $ fst <$> res, Map.fromList $ snd <$> res)++getCoinDeriv ::+ (MonadUnliftIO m) =>+ Network ->+ DBAccountId ->+ Store.Unspent ->+ DB m (Either String SoftPath)+getCoinDeriv net accId unspent =+ runExceptT $ do+ addr <-+ liftEither . maybeToEither "getCoinDeriv: no address" $ unspent.address+ liftEither <=< lift $ getAddrDeriv net accId addr++getAddrDeriv ::+ (MonadUnliftIO m) =>+ Network ->+ DBAccountId ->+ Address ->+ DB m (Either String SoftPath)+getAddrDeriv net (DBAccountKey wallet accDeriv) addr =+ runExceptT $ do+ addrT <-+ liftEither . maybeToEither "getAddrDeriv: no address" $ addrToText net addr+ derivM <-+ lift . selectOne . from $ \a -> do+ where_ $+ a ^. DBAddressAddress ==. val addrT+ &&. a ^. DBAddressAccountWallet ==. val wallet+ &&. a ^. DBAddressAccountDerivation ==. val accDeriv+ return $ a ^. DBAddressDerivation+ liftEither . maybeToEither "getAddrDeriv: no derivation" $+ parseSoft . cs . unValue =<< derivM++{- Transactions -}++getConfirmedTxs ::+ (MonadUnliftIO m) => DBAccountId -> Bool -> ExceptT String (DB m) [TxHash]+getConfirmedTxs (DBAccountKey wallet accDeriv) confirm = do+ ts <-+ lift . select $+ from $ \t -> do+ where_ $+ t ^. DBTxInfoAccountWallet ==. val wallet+ &&. t ^. DBTxInfoAccountDerivation ==. val accDeriv+ &&. t ^. DBTxInfoConfirmed ==. val confirm+ orderBy [asc (t ^. DBTxInfoBlockRef)]+ return $ t ^. DBTxInfoTxid+ forM ts $ \(Value t) ->+ liftEither $ maybeToEither "getConfirmedTxs invalid TxHash" $ hexToTxHash t++-- Insert a new transaction or replace it, if it already exists+-- Returns True if there was a change or an insert+repsertTxInfo ::+ (MonadUnliftIO m) =>+ Network ->+ Ctx ->+ DBAccountId ->+ TxInfo ->+ ExceptT String (DB m) (DBTxInfo, Bool)+repsertTxInfo net ctx accId txInfo = do+ time <- liftIO getCurrentTime+ tid <- liftEither $ maybeToEither "TxId" $ txInfoHash txInfo+ let confirmed' = Store.confirmed $ txInfoBlockRef txInfo+ tidT = txHashToHex tid+ bRef = S.encode $ txInfoBlockRef txInfo+ -- Confirmations will get updated when retrieving them+ blob =+ BS.toStrict $+ marshalJSON+ (net, ctx)+ txInfo+ { txInfoConfirmations = 0,+ txInfoPending = Nothing+ }+ (DBAccountKey wallet accDeriv) = accId+ key = DBTxInfoKey wallet accDeriv tidT+ dbInfo =+ DBTxInfo+ { dBTxInfoAccountWallet = wallet,+ dBTxInfoAccountDerivation = accDeriv,+ dBTxInfoTxid = tidT,+ dBTxInfoBlockRef = bRef,+ dBTxInfoConfirmed = confirmed',+ dBTxInfoBlob = blob,+ dBTxInfoCreated = time+ }+ prevM <- lift $ P.get key+ case prevM of+ Just prev -> do+ let newDBInfo =+ prev+ { dBTxInfoBlockRef = bRef,+ dBTxInfoConfirmed = confirmed',+ dBTxInfoBlob = blob+ }+ lift $ P.replace key newDBInfo+ return (newDBInfo, prev /= newDBInfo)+ Nothing -> do+ lift $ P.insert_ dbInfo+ return (dbInfo, True)++txsPage ::+ (MonadUnliftIO m) =>+ Ctx ->+ DBAccountId ->+ Page ->+ ExceptT String (DB m) [TxInfo]+txsPage ctx accId@(DBAccountKey wallet accDeriv) (Page lim off) = do+ acc <- getAccountById accId+ let net = accountNetwork acc+ dbTxs <-+ lift . select . from $ \t -> do+ where_ $+ t ^. DBTxInfoAccountWallet ==. val wallet+ &&. t ^. DBTxInfoAccountDerivation ==. val accDeriv+ orderBy [asc (t ^. DBTxInfoBlockRef)]+ limit $ fromIntegral lim+ offset $ fromIntegral off+ return $ t ^. DBTxInfoBlob+ res <-+ forM dbTxs $ \(Value dbTx) -> do+ liftMaybe "TxInfo unmarshalJSON Failed" $+ unmarshalJSON (net, ctx) $+ BS.fromStrict dbTx+ resLabels <- lift $ mapM (fillTxInfoLabels net) res+ bestM <- lift $ getBest net+ return $ updateConfirmations (snd <$> bestM) <$> resLabels+ where+ updateConfirmations bestM tif =+ tif {txInfoConfirmations = getConfirmations bestM (txInfoBlockRef tif)}++getConfirmations :: Maybe BlockHeight -> Store.BlockRef -> Natural+getConfirmations _ (Store.MemRef _) = 0+getConfirmations Nothing (Store.BlockRef _ _) = 1+getConfirmations (Just best) (Store.BlockRef height _)+ | best < height = 1+ | otherwise = fromIntegral $ best - height + 1++fillTxInfoLabels :: (MonadUnliftIO m) => Network -> TxInfo -> DB m TxInfo+fillTxInfoLabels net txInfo = do+ o <- mapM fillOutput $ Map.assocs $ txInfoMyOutputs txInfo+ i <- mapM fillInput $ Map.assocs $ txInfoMyInputs txInfo+ return $+ txInfo+ { txInfoMyOutputs = Map.fromList o,+ txInfoMyInputs = Map.fromList i+ }+ where+ fillOutput (a, o) = do+ resM <- runMaybeT $ do+ addrT <- hoistMaybe $ addrToText net a+ (Entity _ dbAddr) <- hoistMaybe =<< lift (P.getBy $ UniqueAddress addrT)+ return (a, o {myOutputsLabel = dBAddressLabel dbAddr})+ return $ fromMaybe (a, o) resM+ fillInput (a, i) = do+ resM <- runMaybeT $ do+ addrT <- hoistMaybe $ addrToText net a+ (Entity _ dbAddr) <- hoistMaybe =<< lift (P.getBy $ UniqueAddress addrT)+ return (a, i {myInputsLabel = dBAddressLabel dbAddr})+ return $ fromMaybe (a, i) resM++{- Coins -}++data JsonCoin = JsonCoin+ { jsonCoinOutpoint :: !OutPoint,+ jsonCoinAddress :: !Address,+ jsonCoinValue :: !Word64,+ jsonCoinBlock :: !Store.BlockRef,+ jsonCoinConfirmations :: !Natural,+ jsonCoinLocked :: !Bool+ }+ deriving (Eq, Show)++instance MarshalJSON Network JsonCoin where+ marshalValue net c =+ object+ [ "outpoint" .= jsonCoinOutpoint c,+ "address" .= marshalValue net (jsonCoinAddress c),+ "value" .= jsonCoinValue c,+ "block" .= jsonCoinBlock c,+ "confirmations" .= jsonCoinConfirmations c,+ "locked" .= jsonCoinLocked c+ ]+ unmarshalValue net =+ withObject "JsonCoin" $ \o ->+ JsonCoin+ <$> o .: "outpoint"+ <*> (unmarshalValue net =<< o .: "address")+ <*> o .: "value"+ <*> o .: "block"+ <*> o .: "confirmations"+ <*> o .: "locked"++toJsonCoin :: Network -> Maybe BlockHeight -> DBCoin -> Either String JsonCoin+toJsonCoin net bestM dbCoin = do+ op <- textToOutpoint $ dBCoinOutpoint dbCoin+ ad <-+ maybeToEither "toJsonCoin: Invalid address" $+ textToAddr net $+ dBCoinAddress dbCoin+ br <- S.decode $ dBCoinBlockRef dbCoin+ let confirmations = getConfirmations bestM br+ return $+ JsonCoin+ { jsonCoinOutpoint = op,+ jsonCoinAddress = ad,+ jsonCoinValue = dBCoinValue dbCoin,+ jsonCoinBlock = br,+ jsonCoinConfirmations = confirmations,+ jsonCoinLocked = dBCoinLocked dbCoin+ }++outpointText :: OutPoint -> Text+outpointText = encodeHex . S.encode++textToOutpoint :: Text -> Either String OutPoint+textToOutpoint t = do+ bs <- maybeToEither "textToOutpoint: invalid input" $ decodeHex t+ S.decode bs++-- Get all coins in an account, spendable or not+coinPage ::+ (MonadUnliftIO m) =>+ Network ->+ DBAccountId ->+ Page ->+ ExceptT String (DB m) [JsonCoin]+coinPage net (DBAccountKey wallet accDeriv) (Page lim off) = do+ coins <-+ lift . select . from $ \c -> do+ where_ $ do+ c ^. DBCoinAccountWallet ==. val wallet+ &&. c ^. DBCoinAccountDerivation ==. val accDeriv+ orderBy [asc (c ^. DBCoinBlockRef), desc (c ^. DBCoinCreated)]+ limit $ fromIntegral lim+ offset $ fromIntegral off+ return c+ bestM <- lift $ getBest net+ mapM (liftEither . toJsonCoin net (snd <$> bestM) . entityVal) coins++-- Spendable coins must be confirmed and not locked+getSpendableCoins ::+ (MonadUnliftIO m) =>+ Network ->+ DBAccountId ->+ Natural ->+ ExceptT String (DB m) [Store.Unspent]+getSpendableCoins net (DBAccountKey wallet accDeriv) minConf = do+ bestM <- lift $ getBest net+ coins <- lift . select . from $ \c -> do+ where_ $+ c ^. DBCoinAccountWallet ==. val wallet+ &&. c ^. DBCoinAccountDerivation ==. val accDeriv+ &&. c ^. DBCoinLocked ==. val False+ return c+ let f c = do+ ref <- liftEither $ S.decode $ dBCoinBlockRef c+ return $ getConfirmations (snd <$> bestM) ref >= minConf+ spendableCoins <- filterM (f . entityVal) coins+ let bss = dBCoinBlob . entityVal <$> spendableCoins+ mapM (liftEither . S.decode) bss++insertCoin ::+ (MonadUnliftIO m) =>+ DBAccountId ->+ Text ->+ Store.Unspent ->+ DB m DBCoin+insertCoin (DBAccountKey wallet accDeriv) addr unspent = do+ time <- liftIO getCurrentTime+ let newCoin =+ DBCoin+ { dBCoinAccountWallet = wallet,+ dBCoinAccountDerivation = accDeriv,+ dBCoinOutpoint = outpointText unspent.outpoint,+ dBCoinAddress = addr,+ dBCoinValue = unspent.value,+ dBCoinBlockRef = S.encode unspent.block,+ dBCoinBlob = S.encode unspent,+ dBCoinConfirmed = Store.confirmed unspent.block,+ dBCoinLocked = False,+ dBCoinCreated = time+ }+ P.insert_ newCoin+ return newCoin++updateCoin :: (MonadUnliftIO m) => Store.Unspent -> DB m ()+updateCoin unspent = do+ let key = DBCoinKey $ outpointText unspent.outpoint+ P.update+ key+ [ DBCoinBlob P.=. S.encode unspent,+ DBCoinConfirmed P.=. Store.confirmed unspent.block,+ DBCoinBlockRef P.=. S.encode unspent.block+ ]++deleteCoin :: (MonadUnliftIO m) => DBCoin -> DB m ()+deleteCoin coin =+ delete . from $ \c ->+ where_ $+ c ^. DBCoinOutpoint ==. val (dBCoinOutpoint coin)++getCoinsByAddr :: (MonadUnliftIO m) => Text -> DB m [DBCoin]+getCoinsByAddr addr = do+ coins <- select . from $ \c -> do+ where_ $ c ^. DBCoinAddress ==. val addr+ return c+ return $ entityVal <$> coins++-- This is the main coin function+-- Either insert, update or delete coins as required. Returns the number of+-- coins that have either been inserted, updated or deleted.+refreshCoins ::+ (MonadUnliftIO m) =>+ Network ->+ DBAccountId ->+ [Address] ->+ [Store.Unspent] ->+ ExceptT String (DB m) (Int, [DBCoin])+refreshCoins net accId addrsToUpdate allUnspent = do+ let storeMap = groupCoins net allUnspent+ addrsToUpdateE <-+ mapM (liftEither . maybeToEither "Addr" . addrToText net) addrsToUpdate+ res <- forM addrsToUpdateE $ \addr -> do+ let storeCoins = fromMaybe [] $ Map.lookup addr storeMap+ localCoins <- lift $ getCoinsByAddr addr+ let storeOps = outpointText . (.outpoint) <$> storeCoins+ localOps = dBCoinOutpoint <$> localCoins+ toDelete = filter ((`notElem` storeOps) . dBCoinOutpoint) localCoins+ toInsert =+ filter ((`notElem` localOps) . outpointText . (.outpoint)) storeCoins+ toUpdate = filter (f localCoins) storeCoins+ lift $ forM_ toDelete deleteCoin+ lift $ forM_ toUpdate updateCoin+ newCoins <- lift $ forM toInsert $ insertCoin accId addr+ return (length toDelete + length toUpdate + length toInsert, newCoins)+ return (sum $ fst <$> res, concatMap snd res)+ where+ f localCoins s =+ let cM = find ((== outpointText s.outpoint) . dBCoinOutpoint) localCoins+ in case cM of+ Just c -> dBCoinBlockRef c /= S.encode s.block+ _ -> False++groupCoins :: Network -> [Store.Unspent] -> Map Text [Store.Unspent]+groupCoins net =+ Map.fromListWith (<>) . mapMaybe f+ where+ f x =+ case x.address of+ Just a -> (,[x]) <$> addrToText net a+ _ -> Nothing++setLockCoin :: (MonadUnliftIO m) => OutPoint -> Bool -> DB m Natural+setLockCoin op locked = do+ cnt <- updateCount $ \c -> do+ set c [DBCoinLocked =. val locked]+ where_ $+ c ^. DBCoinOutpoint ==. val (outpointText op)+ &&. c ^. DBCoinLocked ==. val (not locked)+ return $ fromIntegral cnt++{- Raw Transactions -}++insertRawTx :: (MonadUnliftIO m) => Tx -> DB m ()+insertRawTx tx = do+ let hash = txHashToHex $ txHash tx+ key = DBRawTxKey hash+ P.repsert key $ DBRawTx hash (S.encode tx)++getRawTx :: (MonadUnliftIO m) => TxHash -> ExceptT String (DB m) Tx+getRawTx hash = do+ let key = DBRawTxKey $ txHashToHex hash+ txM <- lift $ P.get key+ case txM of+ Just tx -> liftEither . S.decode $ dBRawTxBlob tx+ Nothing -> throwError "getRawTx: missing transaction"++{- Pending Transactions -}++data TxOnline = TxOnline | TxOffline+ deriving (Eq, Show)++-- Returns (TxSignData, isOnline)+getPendingTx ::+ (MonadUnliftIO m) =>+ TxHash ->+ DB m (Maybe (DBAccountId, TxSignData, TxInfoPending))+getPendingTx nosigHash = do+ let hashT = txHashToHex nosigHash+ key = DBPendingTxKey hashT+ resM <- P.get key+ case resM of+ Just (DBPendingTx wallet accDeriv _ blob online _) -> do+ let accId = DBAccountKey wallet accDeriv+ tsdM = Json.decode $ BS.fromStrict blob+ return $ do+ tsd <- tsdM+ let pending =+ TxInfoPending+ (nosigTxHash $ txSignDataTx tsd)+ (txSignDataSigned tsd)+ online+ return (accId, tsd, pending)+ _ -> return Nothing++pendingTxPage ::+ (MonadUnliftIO m) =>+ Ctx ->+ DBAccountId ->+ Page ->+ ExceptT String (DB m) [TxInfo]+pendingTxPage ctx accId@(DBAccountKey wallet accDeriv) (Page lim off) = do+ acc <- getAccountById accId+ let pub = accountXPubKey ctx acc+ net = accountNetwork acc+ tsds <-+ lift . select . from $ \p -> do+ where_ $ do+ p ^. DBPendingTxAccountWallet ==. val wallet+ &&. p ^. DBPendingTxAccountDerivation ==. val accDeriv+ orderBy [desc (p ^. DBPendingTxCreated)]+ limit $ fromIntegral lim+ offset $ fromIntegral off+ return p+ forM tsds $ \(Entity _ res) -> do+ tsd <- liftEither . Json.eitherDecode . BS.fromStrict $ dBPendingTxBlob res+ txInfo <- liftEither $ parseTxSignData net ctx pub tsd+ let nosigHash = nosigTxHash $ txSignDataTx tsd+ pending =+ TxInfoPending nosigHash (txSignDataSigned tsd) (dBPendingTxOnline res)+ let txInfoP = txInfo {txInfoPending = Just pending}+ lift $ fillTxInfoLabels net txInfoP++-- Returns the TxHash and NoSigHash of pending transactions. They are compared+-- during a sync in order to delete pending transactions that are now online.+pendingTxHashes ::+ (MonadUnliftIO m) =>+ DBAccountId ->+ ExceptT String (DB m) [(TxHash, DBPendingTxId)]+pendingTxHashes (DBAccountKey wallet accDeriv) = do+ blobs <-+ lift . select . from $ \p -> do+ where_ $ do+ p ^. DBPendingTxAccountWallet ==. val wallet+ &&. p ^. DBPendingTxAccountDerivation ==. val accDeriv+ return (p ^. DBPendingTxBlob, p ^. DBPendingTxId)+ res <- forM blobs $ \(Value blob, Value key) -> do+ tsd <- liftEither . Json.eitherDecode $ BS.fromStrict blob+ if txSignDataSigned tsd+ then return [(txHash $ txSignDataTx tsd, key)]+ else return []+ return $ concat res++-- Imports a pending transaction, locks coins and locks internal addresses+importPendingTx ::+ (MonadUnliftIO m) =>+ Network ->+ Ctx ->+ DBAccountId ->+ TxSignData ->+ ExceptT String (DB m) TxHash+importPendingTx net ctx accId tsd@(TxSignData tx _ _ _ signed) = do+ acc <- getAccountById accId+ let pub = accountXPubKey ctx acc+ nosigHash = nosigTxHash $ txSignDataTx tsd+ bs = BS.toStrict $ Json.encode tsd+ prevM <- lift $ getPendingTx nosigHash+ case prevM of+ Just (_, TxSignData prevTx _ _ _ prevSigned, pending) -> do+ when (pendingOnline pending) $+ throwError "The transaction is already online"+ when (prevSigned && not signed) $+ throwError "Can not replace a signed transaction with an unsigned one"+ when (prevTx == tx) $+ throwError "The transaction already exists"+ when (not prevSigned && signed) $ do+ let key = DBPendingTxKey $ txHashToHex nosigHash+ lift $ P.update key [DBPendingTxBlob P.=. bs]+ return nosigHash+ Nothing -> do+ txInfoU <- liftEither $ parseTxSignData net ctx pub tsd+ let (outpoints, outIntAddrs, restAddrs) = parseTxInfoU txInfoU+ -- Verify coins and lock them+ forM_ outpoints $ \outpoint -> do+ coinM <- lift $ P.get $ DBCoinKey $ outpointText outpoint+ case coinM of+ Just coin -> do+ when (dBCoinLocked coin) $+ throwError "A coin referenced by the transaction is locked"+ lift $ setLockCoin outpoint True+ _ -> throwError "A coin referenced by the transaction does not exist"+ -- Verify addresses and set internal output addresses to busy+ outIntAddrsT <- mapM (liftMaybe "Addrs" . addrToText net . fst) outIntAddrs+ restAddrsT <- mapM (liftMaybe "Addrs" . addrToText net) restAddrs+ outIntAddrsE <- lift . select . from $ \a -> do+ where_ $ a ^. DBAddressAddress `in_` valList outIntAddrsT+ return a+ restAddrsE <- lift . select . from $ \a -> do+ where_ $ a ^. DBAddressAddress `in_` valList restAddrsT+ return a+ when+ ( length (outIntAddrsT <> restAddrsT)+ /= length (outIntAddrsE <> restAddrsE)+ )+ $ throwError "Some referenced addresses do not exist"+ unless (all (dBAddressFree . entityVal) outIntAddrsE) $+ throwError "Some of the internal output addresses are not free"+ -- Set the output internal addresses to not free+ _ <- lift $ setAddrsFree AddrBusy outIntAddrsT+ -- Insert the pending transaction+ time <- liftIO getCurrentTime+ let ptx =+ DBPendingTx+ (dBAccountWallet acc)+ (dBAccountDerivation acc)+ (txHashToHex nosigHash)+ bs+ False+ time+ lift $ P.insert_ ptx+ return nosigHash++-- (Outpoints, external addrs, internal addrs)+parseTxInfoU :: TxInfo -> ([OutPoint], [(Address, KeyIndex)], [Address])+parseTxInfoU TxInfo {..} =+ (outpoints, nub $ f <$> outIntAddrs, nub $ fst <$> restAddrs)+ where+ outpoints = (.outpoint) <$> concatMap myInputsSigInput (Map.elems txInfoMyInputs)+ outAddrs = second myOutputsPath <$> Map.assocs txInfoMyOutputs+ inAddrs = second myInputsPath <$> Map.assocs txInfoMyInputs+ (outIntAddrs, outExtAddrs) = partition (isIntPath . snd) outAddrs+ restAddrs = outExtAddrs <> inAddrs+ f (a, p) =+ case pathToList p of+ (_ : i : _) -> (a, i)+ _ -> error "parseTxInfoU"++-- Delete a pending transaction, unlocks coins and frees internal addresses+deletePendingTx ::+ (MonadUnliftIO m) =>+ Ctx ->+ TxHash ->+ ExceptT String (DB m) (Natural, Natural)+deletePendingTx ctx nosigHash = do+ let key = DBPendingTxKey $ txHashToHex nosigHash+ tsdM <- lift $ getPendingTx nosigHash+ case tsdM of+ Just (_, _, TxInfoPending _ _ True) -> do+ throwError+ "This pending transaction has been sent to the network.\+ \ Run syncacc to refresh your database."+ -- We only free coins and addresses if the transaction is offline+ Just (accId, tsd, _) -> do+ acc <- getAccountById accId+ let net = accountNetwork acc+ pub = accountXPubKey ctx acc+ txInfoU <- liftEither $ parseTxSignData net ctx pub tsd+ let (outpoints, outIntAddrs, _) = parseTxInfoU txInfoU+ outIntAddrsT <-+ mapM (liftMaybe "Address" . addrToText net . fst) outIntAddrs+ freedCoins <- forM outpoints $ \op -> lift $ setLockCoin op False+ freedAddresses <- lift $ setAddrsFree AddrFree outIntAddrsT+ lift $ P.delete key+ return (sum freedCoins, fromIntegral freedAddresses)+ _ -> throwError "The pending transaction does not exist"++-- When the pending transaction is online, we just delete it+deletePendingTxOnline :: (MonadUnliftIO m) => DBPendingTxId -> DB m ()+deletePendingTxOnline = P.delete++setPendingTxOnline :: (MonadUnliftIO m) => TxHash -> DB m Natural+setPendingTxOnline nosigH = do+ let nosigHT = txHashToHex nosigH+ cnt <- updateCount $ \p -> do+ set p [DBPendingTxOnline =. val True]+ where_ $ p ^. DBPendingTxNosigHash ==. val nosigHT+ return $ fromIntegral cnt++{- Helpers -}++extDeriv :: SoftPath+extDeriv = Deriv :/ 0++intDeriv :: SoftPath+intDeriv = Deriv :/ 1++isExtPath :: SoftPath -> Bool+isExtPath p =+ case pathToList p of+ [0, _] -> True+ _ -> False++isIntPath :: SoftPath -> Bool+isIntPath p =+ case pathToList p of+ [1, _] -> True+ _ -> False++pathIndex :: SoftPath -> KeyIndex+pathIndex p =+ case pathToList p of+ [_, i] -> i+ _ -> error "Invalid pathIndex"++bip44Deriv :: Network -> Natural -> HardPath+bip44Deriv net a = Deriv :| 44 :| net.bip44Coin :| fromIntegral a++xPubIndex :: XPubKey -> Natural+xPubIndex = fromIntegral . xPubChild++addrsDerivPage :: Ctx -> SoftPath -> Page -> XPubKey -> [(Address, SoftPath)]+addrsDerivPage ctx deriv (Page lim off) xpub =+ fmap (\(a, _, i) -> (a, deriv :/ i)) addrs+ where+ addrs =+ take (fromIntegral lim) $+ derivePathAddrs ctx xpub deriv (fromIntegral off)++-- like `maybe` but for a maybe/value sandwich+joinMaybe :: b -> (a -> b) -> Maybe (E.Value (Maybe a)) -> b+joinMaybe d f m =+ case m of+ Just (Value m') -> maybe d f m'+ Nothing -> d++flatMaybe :: Maybe (E.Value (Maybe a)) -> Maybe a+flatMaybe = joinMaybe Nothing Just
@@ -0,0 +1,221 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++module Haskoin.Wallet.Entropy where++import Control.Monad.IO.Class ( MonadIO, liftIO )+import Control.Monad (replicateM, unless, when)+import Control.Monad.Except+import Data.Bits (Bits (setBit, xor))+import qualified Data.ByteString as BS+import Data.List (foldl', nub)+import Data.Text (Text)+import Data.Word (Word8)+import Haskoin.Crypto (Mnemonic, toMnemonic, fromMnemonic)+import Haskoin.Util (bsToInteger)+import Haskoin.Wallet.Util (chunksOf)+import Numeric (showIntAtBase)+import Numeric.Natural (Natural)+import qualified System.Console.Haskeline as Haskeline+import qualified System.Console.Haskeline as Haskline+import qualified System.Directory as D+import System.Entropy (getEntropy)+import System.IO (IOMode (ReadMode), withBinaryFile)+import Text.Read (readMaybe)++-- Produces uniformily distributed dice rolls from a random byte+-- 0, 0x00 -> [6,6,6]+-- 1, 0x01 -> [6,6,1]+-- 215, 0xd7 -> [5,5,5]+-- 216, 0xd8 -> [6,6]+-- 217, 0xd9 -> [6,1]+-- 251, 0xfb -> [5,5]+-- Anything above 0xfb is dropped+word8ToBase6 :: Word8 -> [Natural]+word8ToBase6 w+ | w > 0xfb = [] -- Drop otherwise we don't have uniform distribution+ | w > 0xd7 =+ let res = f <$> encodeBase6 (BS.pack [w - 0xd8])+ in replicate (2 - length res) 6 <> res+ | otherwise =+ let res = f <$> encodeBase6 (BS.pack [w])+ in replicate (3 - length res) 6 <> res+ where+ f :: Char -> Natural+ f = read . (: [])+ b6Data :: String+ b6Data = "612345"+ b6 :: Int -> Char+ b6 = (b6Data !!)+ encodeBase6 :: BS.ByteString -> String+ encodeBase6 bs+ | BS.null bs = mempty+ | otherwise = showIntAtBase 6 b6 (bsToInteger bs) ""++-- Produces Bools using base6ToBool. Once 8 Bools have been+-- accumulated, it is transformed to a Word8 using boolsToWord8.+-- Any unused Bools are returned along with the Word8s.+base6ToWord8 :: [Natural] -> [Bool] -> ([Word8], [Bool])+base6ToWord8 ns obs+ | length bs < 8 = ([], bs)+ | otherwise =+ let (ws, rest) = f bs []+ in (boolsToWord8 <$> ws, rest)+ where+ bs = concatMap base6ToBool ns <> obs+ f :: [Bool] -> [[Bool]] -> ([[Bool]], [Bool])+ f xs acc+ | length xs < 8 = (acc, xs)+ | otherwise =+ let (l, r) = splitAt (length xs - 8) xs+ in f l ([r] <> acc)++-- Produce uniformily distributed bits from dice rolls+-- 6 -> [0,0]+-- 1 -> [0,1]+-- 2 -> [1,0]+-- 3 -> [1,1]+-- 4 -> [0]+-- 5 -> [1]+base6ToBool :: Natural -> [Bool]+base6ToBool =+ \case+ 6 -> [False, False]+ 1 -> [False, True]+ 2 -> [True, False]+ 3 -> [True, True]+ 4 -> [False]+ 5 -> [True]+ _ -> error "Invalid base6ToBool"++-- Big Endian+-- [T,F,F,F,F,F,F,F] -> 128+-- [F,F,F,F,F,F,F,T] -> 1+boolsToWord8 :: [Bool] -> Word8+boolsToWord8 xs+ | length xs /= 8 = error "Invalid boolsToWord8"+ | otherwise =+ foldl' setBit 0 ys+ where+ ys = fmap fst $ filter snd $ zip [0 ..] $ reverse xs++-- Packs bools into a ByteString+packBools :: [Bool] -> BS.ByteString+packBools bs+ | length bs `mod` 8 /= 0 = error "Invalid packBools length"+ | otherwise = BS.pack $ boolsToWord8 <$> chunksOf 8 bs++-- Mix entropy of same length by xoring them+xorBytes :: BS.ByteString -> BS.ByteString -> BS.ByteString+xorBytes ent1 ent2+ | BS.length ent1 == BS.length ent2 =+ BS.pack $ BS.zipWith xor ent1 ent2+ | otherwise = error "Entropy is not of the same length"++-- Split secret s into n parts+-- generate k1 .. k(n-1) randomly+-- compute kn = s xor k1 xor .. xor k(n-1)+-- return k1 .. kn+-- We have the property that s = k1 xor ... xor kn+splitEntropy :: Natural -> BS.ByteString -> IO [BS.ByteString]+splitEntropy n secret+ | n < 2 = error "splitEntropy invalid number"+ | otherwise = do+ ks <- replicateM (fromIntegral n - 1) (snd <$> systemEntropy len)+ return $ splitEntropyWith secret ks+ where+ len = fromIntegral $ BS.length secret++splitEntropyWith :: BS.ByteString -> [BS.ByteString] -> [BS.ByteString]+splitEntropyWith secret ks = foldl' xorBytes secret ks : ks++mergeMnemonicParts :: [Text] -> Either String Mnemonic+mergeMnemonicParts mnems+ | length mnems < 2 = Left "Only one mnemonic provided"+ | length (nub mnems) /= length mnems = Left "Two mnemonics are identical"+ | otherwise = do+ bs <- mapM fromMnemonic mnems+ let ent = foldl' xorBytes (head bs) (tail bs)+ toMnemonic ent++askInputDice :: IO Natural+askInputDice = do+ inputM <-+ Haskeline.runInputT Haskline.defaultSettings $+ Haskeline.getInputLine "Dice roll: "+ case readMaybe =<< inputM of+ Just n ->+ if n `elem` [1 .. 6]+ then return n+ else badInput+ _ -> badInput+ where+ badInput = do+ putStrLn "Invalid dice roll. Must be in range [1..6]"+ askInputDice++-- TODO: Ask the dice rolls in sequences of 5 or so+getDiceEntropy :: Natural -> IO BS.ByteString+getDiceEntropy ent = do+ putStrLn "Enter your dice rolls (from 1 to 6)"+ f [] []+ where+ f :: [Word8] -> [Bool] -> IO BS.ByteString+ f res acc+ | length res == fromIntegral ent =+ return $ BS.pack res+ | otherwise = do+ d <- askInputDice+ let (ws, bs) = base6ToWord8 [d] acc+ unless (null ws) $ do+ putStrLn $+ show (length res + length ws)+ <> "/"+ <> show ent+ <> " bytes collected"+ f (ws <> res) bs++-- Generate a mnemonic with optional dice entropy and key splitting+genMnemonic ::+ (MonadIO m) =>+ Natural ->+ Bool ->+ Natural ->+ ExceptT String m (Text, Mnemonic, [Mnemonic])+genMnemonic reqEnt reqDice splitIn+ | splitIn == 0 = throwError "Invalid split option"+ | reqEnt `elem` [16, 20 .. 32] = do+ (origEnt, sysEnt) <- liftIO $ systemEntropy reqEnt+ ent <-+ if reqDice+ then do+ diceEnt <- liftIO $ getDiceEntropy reqEnt+ return $ sysEnt `xorBytes` diceEnt+ else return sysEnt+ when (BS.length ent /= fromIntegral reqEnt) $+ throwError "Something went wrong with the entropy size"+ mnem <- liftEither $ toMnemonic ent+ if splitIn > 1+ then do+ ks <- liftIO $ splitEntropy splitIn ent+ unless (ent == foldl' xorBytes (head ks) (tail ks)) $+ throwError "Something went wrong while splitting the keys"+ splitMnems <- liftEither $ mapM toMnemonic ks+ unsplitMnem <- liftEither $ mergeMnemonicParts splitMnems+ unless (mnem == unsplitMnem) $+ throwError "Something went wrong while reconstructing the mnemonic"+ return (origEnt, mnem, splitMnems)+ else return (origEnt, mnem, [])+ | otherwise = throwError "The entropy value can only be in [16,20..32]"++systemEntropy :: Natural -> IO (Text, BS.ByteString)+systemEntropy bytes = do+ exists <- D.doesFileExist "/dev/random"+ if exists+ then ("/dev/random",) <$> devRandom bytes+ else ("System.Entropy.getEntropy",) <$> getEntropy (fromIntegral bytes)++devRandom :: Natural -> IO BS.ByteString+devRandom bytes =+ withBinaryFile "/dev/random" ReadMode (`BS.hGet` fromIntegral bytes)
@@ -0,0 +1,131 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}++module Haskoin.Wallet.FileIO where++import Control.Applicative ((<|>))+import Control.Monad+import Control.Monad.Except+import Control.Monad.Reader (MonadIO (..))+import Data.Aeson+import Data.Aeson.Types (parseEither)+import qualified Data.ByteString.Char8 as C8+import Data.Maybe (mapMaybe)+import qualified Data.Serialize as S+import Data.String.Conversions (cs)+import Data.Text (Text)+import qualified Data.Text as Text+import Haskoin+import Haskoin.Wallet.Util+import qualified System.Directory as D+import qualified System.IO as IO++data PubKeyDoc = PubKeyDoc+ { documentPubKey :: !XPubKey,+ documentNetwork :: !Network,+ documentName :: !Text,+ documentWallet :: !Fingerprint+ }+ deriving (Eq, Show)++instance MarshalJSON Ctx PubKeyDoc where+ unmarshalValue ctx =+ withObject "pubkeydocument" $ \o -> do+ net <- maybe mzero return . netByName =<< o .: "network"+ PubKeyDoc+ <$> (unmarshalValue (net, ctx) =<< o .: "xpubkey")+ <*> return net+ <*> (o .: "name")+ <*> (o .: "wallet")++ marshalValue ctx (PubKeyDoc k net name wallet) =+ object+ [ "xpubkey" .= marshalValue (net, ctx) k,+ "network" .= net.name,+ "name" .= name,+ "wallet" .= wallet+ ]++data TxSignData = TxSignData+ { txSignDataTx :: !Tx,+ txSignDataInputs :: ![Tx],+ txSignDataInputPaths :: ![SoftPath],+ txSignDataOutputPaths :: ![SoftPath],+ txSignDataSigned :: !Bool+ }+ deriving (Eq, Show)++instance FromJSON TxSignData where+ parseJSON =+ withObject "txsigndata" $ \o -> do+ let f = eitherToMaybe . S.decode <=< decodeHex+ t <- maybe mzero return . f =<< o .: "tx"+ i <- maybe mzero return . mapM f =<< o .: "txinputs"+ TxSignData t i+ <$> o .: "inputpaths"+ <*> o .: "outputpaths"+ <*> o .: "signed"++instance ToJSON TxSignData where+ toJSON (TxSignData t i oi op s) =+ object+ [ "tx" .= encodeHex (S.encode t),+ "txinputs" .= (encodeHex . S.encode <$> i),+ "inputpaths" .= oi,+ "outputpaths" .= op,+ "signed" .= s+ ]++instance MarshalJSON Ctx TxSignData where+ marshalValue _ = toJSON+ unmarshalValue _ = parseJSON++checkPathFree :: (MonadIO m) => FilePath -> ExceptT String m ()+checkPathFree path = do+ exist <- liftIO $ D.doesPathExist path+ when exist $ throwError $ "Path '" <> path <> "' already exists"++-- JSON IO Helpers--++writeJsonFile :: FilePath -> Value -> IO ()+writeJsonFile filePath doc = C8.writeFile filePath $ encodeJsonPrettyLn doc++readJsonFile :: (FromJSON a) => FilePath -> IO (Either String a)+readJsonFile = eitherDecodeFileStrict'++writeMarshalFile :: (MarshalJSON s a) => s -> FilePath -> a -> IO ()+writeMarshalFile s filePath a = writeJsonFile filePath $ marshalValue s a++readMarshalFile :: (MarshalJSON s a) => s -> FilePath -> IO (Either String a)+readMarshalFile s filePath = do+ vE <- readJsonFile filePath+ return $ parseEither (unmarshalValue s) =<< vE++-- Parse wallet dump files for sweeping --++readFileWords :: FilePath -> IO [[Text]]+readFileWords fp = do+ strContents <- IO.readFile fp+ return $ removeComments $ Text.words <$> Text.lines (cs strContents)++parseSecKeysFile :: Network -> [[Text]] -> [SecKey]+parseSecKeysFile net =+ withParser $ \w -> (.key) <$> (fromWif net w <|> fromMiniKey (cs w))++withParser :: (Text -> Maybe a) -> [[Text]] -> [a]+withParser parser =+ mapMaybe go+ where+ go [] = Nothing+ go (w : ws) = parser w <|> go ws++removeComments :: [[Text]] -> [[Text]]+removeComments =+ mapMaybe go+ where+ go [] = Nothing+ go ws@(w : _)+ | "#" `Text.isPrefixOf` w = Nothing+ | otherwise = Just ws
@@ -0,0 +1,25 @@+module Haskoin.Wallet.Main where++import qualified Data.ByteString.Char8 as C8+import Haskoin (Ctx, MarshalJSON (..), withContext)+import Haskoin.Wallet.Commands (Response, commandResponse)+import Haskoin.Wallet.Config (initConfig)+import Haskoin.Wallet.Database (runDB)+import Haskoin.Wallet.Migration (migrateDB)+import Haskoin.Wallet.Parser (parserMain)+import Haskoin.Wallet.PrettyPrinter (prettyPrinter)+import Haskoin.Wallet.Util (encodeJsonPretty)++clientMain :: IO ()+clientMain =+ withContext $ \ctx -> do+ cfg <- initConfig+ runDB cfg $ migrateDB ctx cfg+ (cmd, unit, json) <- parserMain+ res <- commandResponse ctx cfg unit cmd+ if json+ then jsonPrinter ctx res+ else prettyPrinter unit res++jsonPrinter :: Ctx -> Response -> IO ()+jsonPrinter ctx = C8.putStrLn . encodeJsonPretty . marshalValue ctx
@@ -0,0 +1,67 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}++module Haskoin.Wallet.Migration where++import Conduit (MonadUnliftIO)+import Control.Monad+import Control.Monad.Except+import Control.Monad.IO.Class (MonadIO (liftIO))+import Control.Monad.Reader (MonadTrans (lift))+import Data.Text (Text)+import Database.Esqueleto.Legacy as E+import Haskoin+import Haskoin.Wallet.Config+import Haskoin.Wallet.Database+import qualified Haskoin.Wallet.Migration.V0_9_0 as V0_9_0+import Haskoin.Wallet.Migration.SemVersion++getDatabaseTables :: (MonadUnliftIO m) => DB m [Text]+getDatabaseTables = do+ s <- rawSql "select name from sqlite_master where type='table'" []+ return $ unSingle <$> s++databaseVersion :: (MonadUnliftIO m) => DB m (Maybe SemVersion)+databaseVersion = do+ tables <- getDatabaseTables+ if null tables+ then return Nothing+ else+ if "d_b_version" `elem` tables -- Version table was created at v0.9.0+ then Just <$> getVersion+ else return $ Just V0_9_0.migrateFrom++migrateDB :: (MonadUnliftIO m) => Ctx -> Config -> DB m ()+migrateDB ctx cfg = do+ resE <- runExceptT $ do+ verM <- lift databaseVersion+ case verM of+ Nothing -> do+ lift $ do+ globalMigration+ setVersion currentVersion+ Just ver -> migrateDB_ ctx ver+ return verM+ case resE of+ Left err -> do+ transactionUndo -- Roll back the migration and exit+ error err+ Right verM -> unless (verM == Just currentVersion) $ migrateDB ctx cfg++migrateDB_ ::+ (MonadUnliftIO m) => Ctx -> SemVersion -> ExceptT String (DB m) ()+migrateDB_ ctx dbVer+ | dbVer == V0_9_0.migrateFrom = do+ liftIO $ printDBMigration V0_9_0.migrateFrom V0_9_0.migrateTo+ V0_9_0.migrateDB ctx+ | dbVer == currentVersion = return ()+ -- In case we have a patch version in the database+ | toMinor dbVer == currentVersion = lift $ setVersion currentVersion+ | otherwise = error $ "Invalid migration version: " <> verString dbVer++printDBMigration :: SemVersion -> SemVersion -> IO ()+printDBMigration fromV toV =+ putStrLn $ "Migrating from " <> verString fromV <> " to " <> verString toV
@@ -0,0 +1,64 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}++module Haskoin.Wallet.Migration.SemVersion where++import Data.List.Split+import Data.String (IsString)+import Numeric.Natural++-- | Version of Haskoin Wallet package.+currentVersionStr :: (IsString a) => a+#ifdef CURRENT_PACKAGE_VERSION+currentVersionStr = CURRENT_PACKAGE_VERSION+#else+currentVersionStr = error "No version string"+#endif++data SemVersion+ = VerMajor !Natural+ | VerMinor !Natural !Natural+ | VerPatch !Natural Natural Natural+ deriving (Show)++-- 0 == 0.9 == 0.9.0+instance Eq SemVersion where+ (VerMajor j1) == (VerMajor j2) = j1 == j2+ (VerMajor j1) == (VerMinor j2 _) = j1 == j2+ (VerMajor j1) == (VerPatch j2 _ _) = j1 == j2+ (VerMinor j1 m1) == (VerMinor j2 m2) = j1 == j2 && m1 == m2+ (VerMinor j1 m1) == (VerPatch j2 m2 _) = j1 == j2 && m1 == m2+ (VerPatch j1 m1 p1) == (VerPatch j2 m2 p2) = j1 == j2 && m1 == m2 && p1 == p2+ a == b = b == a++toMajor :: SemVersion -> SemVersion+toMajor (VerMajor a) = VerMajor a+toMajor (VerMinor a _) = VerMajor a+toMajor (VerPatch a _ _) = VerMajor a++toMinor :: SemVersion -> SemVersion+toMinor (VerMajor _) = error "VerMajor in toMinor"+toMinor (VerMinor a b) = VerMinor a b+toMinor (VerPatch a b _) = VerMinor a b++parseSemVersion :: String -> SemVersion+parseSemVersion str =+ case read <$> splitOn "." str of+ [a] -> VerMajor a+ [a, b] -> VerMinor a b+ [a, b, c] -> VerPatch a b c+ _ -> error $ "Bad version: " <> str++verString :: SemVersion -> String+verString (VerMajor a) = show a+verString (VerMinor a b) = show a <> "." <> show b+verString (VerPatch a b c) = show a <> "." <> show b <> "." <> show c++currentVersion :: SemVersion+currentVersion =+ case parseSemVersion currentVersionStr of+ res@(VerPatch {}) -> res+ _ -> error $ "Bad version: " <> currentVersionStr
@@ -0,0 +1,198 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Haskoin.Wallet.Migration.V0_9_0 where++import Conduit (MonadUnliftIO)+import Control.Monad+import Control.Monad.Except+import Control.Monad.Reader (MonadTrans (lift))+import Data.Aeson+import qualified Data.Aeson as Json+import qualified Data.ByteString as BS+import Data.Map.Strict (Map)+import Data.Maybe (fromMaybe)+import Database.Esqueleto.Legacy as E+import qualified Database.Persist as P+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Wallet.Database+import Haskoin.Wallet.TxInfo+import Numeric.Natural (Natural)+import Haskoin.Wallet.Migration.SemVersion++{- Migration from 0.8 to 0.9 -}++migrateFrom :: SemVersion+migrateFrom = VerMinor 0 8++migrateTo :: SemVersion+migrateTo = VerMinor 0 9++data OldTxInfo = OldTxInfo+ { oldTxInfoHash :: !TxHash,+ oldTxInfoType :: !TxType,+ oldTxInfoAmount :: !Integer,+ oldTxInfoMyOutputs :: !(Map Address OldMyOutputs),+ oldTxInfoOtherOutputs :: !(Map Address Natural),+ oldTxInfoNonStdOutputs :: ![Store.StoreOutput],+ oldTxInfoMyInputs :: !(Map Address OldMyInputs),+ oldTxInfoOtherInputs :: !(Map Address OtherInputs),+ oldTxInfoNonStdInputs :: ![Store.StoreInput],+ oldTxInfoSize :: !Natural,+ oldTxInfoFee :: !Natural,+ oldTxInfoFeeByte :: !Natural,+ oldTxInfoBlockRef :: !Store.BlockRef,+ oldTxInfoConfirmations :: !Natural+ }+ deriving (Eq, Show)++data OldMyOutputs = OldMyOutputs+ { oldMyOutputsValue :: !Natural,+ oldMyOutputsPath :: !SoftPath+ }+ deriving (Eq, Show)++instance Json.ToJSON OldMyOutputs where+ toJSON (OldMyOutputs i p) =+ object+ [ "value" .= i,+ "path" .= p+ ]++instance Json.FromJSON OldMyOutputs where+ parseJSON =+ withObject "MyOutputs" $ \o -> do+ i <- o .: "value"+ p <- o .: "path"+ return $ OldMyOutputs i p++data OldMyInputs = OldMyInputs+ { oldMyInputsValue :: !Natural,+ oldMyInputsPath :: !SoftPath,+ oldMyInputsSigInput :: [SigInput]+ }+ deriving (Eq, Show)++instance MarshalJSON Ctx OldMyInputs where+ marshalValue ctx (OldMyInputs i p s) =+ object $+ [ "value" .= i,+ "path" .= p+ ]+ ++ ["siginput" .= (marshalValue ctx <$> s) | not (null s)]++ unmarshalValue ctx =+ withObject "MyInputs" $ \o -> do+ i <- o .: "value"+ p <- o .: "path"+ sM <- o .:? "siginput"+ s <- unmarshalValue ctx `mapM` fromMaybe [] sM+ return $ OldMyInputs i p s++instance MarshalJSON (Network, Ctx) OldTxInfo where+ marshalValue (net, ctx) tx =+ object+ [ "txid" .= oldTxInfoHash tx,+ "type" .= oldTxInfoType tx,+ "amount" .= oldTxInfoAmount tx,+ "myoutputs" .= mapAddrText net (oldTxInfoMyOutputs tx),+ "otheroutputs" .= mapAddrText net (oldTxInfoOtherOutputs tx),+ "nonstdoutputs" .= (marshalValue net <$> oldTxInfoNonStdOutputs tx),+ "myinputs" .= marshalMap net ctx (oldTxInfoMyInputs tx),+ "otherinputs" .= marshalMap net ctx (oldTxInfoOtherInputs tx),+ "nonstdinputs" .= (marshalValue net <$> oldTxInfoNonStdInputs tx),+ "size" .= oldTxInfoSize tx,+ "fee" .= oldTxInfoFee tx,+ "feebyte" .= oldTxInfoFeeByte tx,+ "block" .= oldTxInfoBlockRef tx,+ "confirmations" .= oldTxInfoConfirmations tx+ ]+ unmarshalValue (net, ctx) =+ Json.withObject "TxInfo" $ \o ->+ OldTxInfo+ <$> o .: "txid"+ <*> o .: "type"+ <*> o .: "amount"+ <*> (mapTextAddr net <$> o .: "myoutputs")+ <*> (mapTextAddr net <$> o .: "otheroutputs")+ <*> (mapM (unmarshalValue net) =<< o .: "nonstdoutputs")+ <*> (unmarshalMap net ctx =<< o .: "myinputs")+ <*> (unmarshalMap net ctx =<< o .: "otherinputs")+ <*> (mapM (unmarshalValue net) =<< o .: "nonstdinputs")+ <*> o .: "size"+ <*> o .: "fee"+ <*> o .: "feebyte"+ <*> o .: "block"+ <*> o .: "confirmations"++migrateMyOutputs :: OldMyOutputs -> MyOutputs+migrateMyOutputs OldMyOutputs {..} =+ MyOutputs+ { myOutputsValue = oldMyOutputsValue,+ myOutputsPath = oldMyOutputsPath,+ myOutputsLabel = ""+ }++migrateMyInputs :: OldMyInputs -> MyInputs+migrateMyInputs OldMyInputs {..} =+ MyInputs+ { myInputsValue = oldMyInputsValue,+ myInputsPath = oldMyInputsPath,+ myInputsLabel = "",+ myInputsSigInput = oldMyInputsSigInput+ }++migrateTxInfo :: OldTxInfo -> TxInfo+migrateTxInfo OldTxInfo {..} =+ TxInfo+ { txInfoHash = Just oldTxInfoHash,+ txInfoType = oldTxInfoType,+ txInfoAmount = oldTxInfoAmount,+ txInfoMyOutputs = migrateMyOutputs <$> oldTxInfoMyOutputs,+ txInfoOtherOutputs = oldTxInfoOtherOutputs,+ txInfoNonStdOutputs = oldTxInfoNonStdOutputs,+ txInfoMyInputs = migrateMyInputs <$> oldTxInfoMyInputs,+ txInfoOtherInputs = oldTxInfoOtherInputs,+ txInfoNonStdInputs = oldTxInfoNonStdInputs,+ txInfoSize = oldTxInfoSize,+ txInfoFee = oldTxInfoFee,+ txInfoFeeByte = oldTxInfoFeeByte,+ txInfoBlockRef = oldTxInfoBlockRef,+ txInfoConfirmations = oldTxInfoConfirmations,+ txInfoPending = Nothing+ }++migrateDB :: (MonadUnliftIO m) => Ctx -> ExceptT String (DB m) ()+migrateDB ctx = do+ accs <- lift . select $ from return+ forM_ accs $ \(Entity _ acc) -> do+ let net = accountNetwork acc+ res <- lift . select . from $ \t -> do+ where_ $+ t ^. DBTxInfoAccountWallet ==. val (dBAccountWallet acc)+ &&. t ^. DBTxInfoAccountDerivation ==. val (dBAccountDerivation acc)+ return $ t ^. DBTxInfoId+ forM_ res $ \(Value tKey) -> do+ oldDBTx <- liftMaybe "OldTxInfo" =<< lift (P.get tKey)+ oldTxInfo <-+ liftMaybe "OldTxInfo" $+ unmarshalJSON (net, ctx) $+ BS.fromStrict $+ dBTxInfoBlob oldDBTx+ let newTxInfo = migrateTxInfo oldTxInfo+ newBlob = BS.toStrict $ marshalJSON (net, ctx) newTxInfo+ lift $ P.update tKey [DBTxInfoBlob P.=. newBlob]+ -- This will create the missing version table+ lift $ do+ globalMigration+ setVersion migrateTo
@@ -0,0 +1,1083 @@+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module Haskoin.Wallet.Parser where++import Data.List (intercalate)+import Data.String.Conversions (cs)+import Data.Text (Text)+import Haskoin+import Haskoin.Wallet.Amounts (AmountUnit (..), readNatural)+import Haskoin.Wallet.Util (Page (Page))+import Numeric.Natural (Natural)+import Options.Applicative+import Options.Applicative.Help.Pretty+import Text.RawString.QQ (r)++{- Command Parsers -}++data Command+ = CommandMnemonic+ { commandEntropy :: !Natural,+ commandUseDice :: !Bool,+ commandSplitIn :: !Natural+ }+ | CommandCreateAcc+ { commandName :: !Text,+ commandNetwork :: !Network,+ commandDerivationMaybe :: !(Maybe Natural),+ commandSplitIn :: !Natural+ }+ | CommandTestAcc+ { commandMaybeAcc :: !(Maybe Text),+ commandSplitIn :: !Natural+ }+ | CommandImportAcc+ { commandFilePath :: !FilePath+ }+ | CommandExportAcc+ { commandMaybeAcc :: !(Maybe Text),+ commandFilePath :: !FilePath+ }+ | CommandRenameAcc+ { commandOldName :: !Text,+ commandNewName :: !Text+ }+ | CommandAccounts+ { commandMaybeAcc :: !(Maybe Text)+ }+ | CommandDeleteAcc+ { commandName :: !Text,+ commandNetwork :: !Network,+ commandDerivation :: !HardPath+ }+ | CommandReceive+ { commandMaybeAcc :: !(Maybe Text),+ commandMaybeLabel :: !(Maybe Text)+ }+ | CommandAddrs+ { commandMaybeAcc :: !(Maybe Text),+ commandPage :: !Page+ }+ | CommandLabel+ { commandMaybeAcc :: !(Maybe Text),+ commandAddrIndex :: !Natural,+ commandLabel :: !Text+ }+ | CommandTxs+ { commandMaybeAcc :: !(Maybe Text),+ commandPage :: !Page+ }+ | CommandPrepareTx+ { commandRecipients :: ![(Text, Text)],+ commandMaybeAcc :: !(Maybe Text),+ commandFeeByte :: !Natural,+ commandDust :: !Natural,+ commandRcptPay :: !Bool,+ commandMinConf :: !Natural,+ commandOutputFileMaybe :: !(Maybe FilePath)+ }+ | CommandPendingTxs+ { commandMaybeAcc :: !(Maybe Text),+ commandPage :: !Page+ }+ | CommandReviewTx+ { commandMaybeAcc :: !(Maybe Text),+ commandFilePath :: !FilePath+ }+ | CommandImportTx+ { commandMaybeAcc :: !(Maybe Text),+ commandFilePath :: !FilePath+ }+ | CommandExportTx+ { commandNoSigHash :: !TxHash,+ commandFilePath :: !FilePath+ }+ | CommandDeleteTx+ {commandNoSigHash :: !TxHash}+ | CommandSignTx+ { commandMaybeAcc :: !(Maybe Text),+ commandNoSigHashMaybe :: !(Maybe TxHash),+ commandInputFileMaybe :: !(Maybe FilePath),+ commandOutputFileMaybe :: !(Maybe FilePath),+ commandSplitIn :: !Natural+ }+ | CommandCoins+ { commandMaybeAcc :: !(Maybe Text),+ commandPage :: !Page+ }+ | CommandSendTx+ {commandNoSigHash :: !TxHash}+ | CommandSyncAcc+ { commandMaybeAcc :: !(Maybe Text),+ commandFull :: !Bool+ }+ | CommandDiscoverAcc+ { commandMaybeAcc :: !(Maybe Text)+ }+ | CommandBackup+ {commandFilePath :: !FilePath}+ | CommandRestore+ {commandFilePath :: !FilePath}+ | CommandVersion+ | CommandPrepareSweep+ { commandMaybeAcc :: !(Maybe Text),+ commandSecKeyPath :: !FilePath,+ commandSweepTo :: ![Text],+ commandOutputFileMaybe :: !(Maybe FilePath),+ commandFeeByte :: !Natural,+ commandDust :: !Natural+ }+ | CommandSignSweep+ { commandMaybeAcc :: !(Maybe Text),+ commandNoSigHashMaybe :: !(Maybe TxHash),+ commandInputFileMaybe :: !(Maybe FilePath),+ commandOutputFileMaybe :: !(Maybe FilePath),+ commandSecKeyPath :: !FilePath+ }+ | CommandRollDice+ { commandCount :: !Natural+ }+ deriving (Eq, Show)++parserMain :: IO (Command, AmountUnit, Bool)+parserMain =+ customExecParser+ (prefs $ showHelpOnEmpty <> helpIndent 25)+ programParser++programParser :: ParserInfo (Command, AmountUnit, Bool)+programParser = do+ let cmd = (,,) <$> (commandParser <**> helper) <*> unitOption <*> jsonOption+ info cmd $+ fullDesc+ <> progDesc+ [r|+hw is a BIP-44 command-line wallet for bitcoin and bitcoin-cash. It allows+sensitive commands (!) to be run on a separate offline computer. For more+information on a command, type "hw COMMAND --help".+|]++jsonOption :: Parser Bool+jsonOption =+ switch $+ short 'j'+ <> long "json"+ <> help+ [r|+Display the result as JSON. Specify this option at the end of your command.+|]++unitOption :: Parser AmountUnit+unitOption = satoshiOption <|> bitOption++satoshiOption :: Parser AmountUnit+satoshiOption =+ flag UnitBitcoin UnitSatoshi $+ short 's'+ <> long "satoshi"+ <> help+ [r|+Use satoshis for parsing and displaying amounts (default: bitcoin). Specify this+option at the end of your command.+|]++bitOption :: Parser AmountUnit+bitOption =+ flag UnitBitcoin UnitBit $+ short 'b'+ <> long "bit"+ <> help+ [r|+Use bits for parsing and displaying amounts (default: bitcoin). Specify this+option at the end of your command.+|]++commandParser :: Parser Command+commandParser =+ asum+ [ hsubparser $+ mconcat+ [ commandGroup "Mnemonic and account management",+ command "mnemonic" mnemonicParser,+ command "createacc" createAccParser,+ command "testacc" testAccParser,+ command "renameacc" renameAccParser,+ command "accounts" accountsParser,+ command "syncacc" syncAccParser,+ command "deleteacc" deleteAccParser,+ metavar "COMMAND",+ style (const "COMMAND --help")+ ],+ hsubparser $+ mconcat+ [ commandGroup "Address management",+ command "receive" receiveParser,+ command "addrs" addrsParser,+ command "label" labelParser,+ hidden+ ],+ hsubparser $+ mconcat+ [ commandGroup "Transaction management",+ command "txs" txsParser,+ command "preparetx" prepareTxParser,+ command "pendingtxs" pendingTxsParser,+ command "signtx" signTxParser,+ command "sendtx" sendTxParser,+ command "deletetx" deleteTxParser,+ command "coins" coinsParser,+ hidden+ ],+ hsubparser $+ mconcat+ [ commandGroup "Import/export commands",+ command "exportacc" exportAccParser,+ command "importacc" importAccParser,+ command "reviewtx" reviewTxParser,+ command "exporttx" exportTxParser,+ command "importtx" importTxParser,+ hidden+ ],+ hsubparser $+ mconcat+ [ commandGroup "Backup/restore commands",+ command "backup" backupParser,+ command "restore" restoreParser,+ command "discoveracc" discoverAccParser,+ hidden+ ],+ hsubparser $+ mconcat+ [ commandGroup "Utilities",+ command "version" versionParser,+ command "preparesweep" prepareSweepParser,+ command "signsweep" signSweepParser,+ command "rolldice" rollDiceParser,+ hidden+ ]+ ]++offline :: Doc -> Maybe Doc+offline s = Just $ annotate (color Red) "! " <> s++{- Mnemonic Parser -}++mnemonicParser :: ParserInfo Command+mnemonicParser = do+ let cmd =+ CommandMnemonic+ <$> entropyOption+ <*> diceOption+ <*> splitInOption+ info cmd $+ progDescDoc (offline "Generate a mnemonic")+ <> footer+ [r|+Generate a mnemonic using the systems entropy pool. By default it should be+/dev/random on linux machines. If you use the --dice option, the additional dice+entropy will be mixed with the system entropy. You should ideally run this+command on an offline (air-gapped) computer. The mnemonic will NOT be stored on+disk. It will only be printed to the screen and you should write it down and+keep it in a secure location. Make multiple copies of your mnemonic. Do not+store your mnemonic on a digital medium. If you lose your mnemonic, you lose+your funds. If you choose to use the --split option, the mnemonic will be split+into different pieces such that ALL the pieces will be required for signing.+|]++entropyOption :: Parser Natural+entropyOption =+ option (maybeReader f) $+ short 'e'+ <> long "entropy"+ <> metavar "BYTES"+ <> value 16+ <> showDefault+ <> completeWith valid+ <> help+ [r|+Amount of entropy to use in bytes. Valid values are 16, 20, 24, 28 or 32.+|]+ where+ valid = ["16", "20", "24", "28", "32"]+ f s+ | s `elem` valid = fromIntegral <$> readNatural (cs s)+ | otherwise = Nothing++diceOption :: Parser Bool+diceOption =+ switch $+ short 'd'+ <> long "dice"+ <> help+ [r|+Provide additional entropy using 6-sided dice. The entropy will be mixed with+the system entropy.+|]++splitInOption :: Parser Natural+splitInOption =+ option (eitherReader $ f . cs) $+ short 's'+ <> long "split"+ <> metavar "INT"+ <> value 1+ <> help+ [r|+The mnemonic is split in different pieces and reconstructed using bitwise xor.+All the pieces are required for signing. The ordering of the pieces is not+important.+|]+ where+ f s =+ case readNatural s of+ Just n ->+ if n >= 2 && n <= 12+ then Right n+ else Left "The --split value has to be between 2 and 12"+ Nothing -> Left "Could not parse the --split option"++{- CreateAcc Parser -}++createAccParser :: ParserInfo Command+createAccParser = do+ let cmd =+ CommandCreateAcc+ <$> textArg "Name of the new account"+ <*> networkOption+ <*> optional derivationOption+ <*> splitInOption+ info cmd $+ progDescDoc (offline "Create a new account")+ <> footer+ [r|+An account corresponds to the BIP-32 extended key derivation+m/44'/coin'/account'. For example, the bitcoin account 0 would be m/44'/0'/0'.+An account is tied to a private key and thus to a mnemonic. The derived account+m/44'/0'/0' would be different given two different mnemonics. To help manage+your accounts in hw, they are identified by a name. The command `createacc` will+ask for a mnemonic and save the derived public key M/44'/coin'/account' on disk.+The private keys are never stored anywhere while using hw. If you are using an+offline computer, you can then export your account and import it on an online+computer.+|]++networkOption :: Parser Network+networkOption =+ option (eitherReader (f . netByName)) $+ short 'n'+ <> long "network"+ <> metavar "TEXT"+ <> value btc+ <> showDefaultWith (.name)+ <> completeWith ((.name) <$> allNets)+ <> help ("Specify one of the following networks to use: " <> nets)+ where+ nets = intercalate ", " ((.name) <$> allNets)+ f :: Maybe Network -> Either String Network+ f Nothing =+ Left $ "Invalid network name. Select one of the following: " <> nets+ f (Just res) = Right res++derivationOption :: Parser Natural+derivationOption =+ option (maybeReader $ readNatural . cs) $+ short 'd'+ <> long "derivation"+ <> metavar "INT"+ <> help+ [r|+Specify a different account derivation to use (the last part of+m/44'/coin'/account'). By default, account derivations are chosen sequentially+starting from 0.+|]++{- DeleteAcc Parser -}++deleteAccParser :: ParserInfo Command+deleteAccParser = do+ let cmd =+ CommandDeleteAcc+ <$> textArg "Name of the account to delete"+ <*> networkArg+ <*> fullDerivationOption+ info cmd $+ progDesc ("Delete an account")+ <> footer+ [r|+Delete an account from the wallet. You must specify the name, the network and+the full derivation path as confirmation to delete an account.+|]++fullDerivationOption :: Parser HardPath+fullDerivationOption =+ argument (maybeReader parseHard) $+ metavar "DERIVPATH"+ <> help+ [r|+Specify the full derivation path of the account to delete. For example:+/44'/0'/3'.+|]++networkArg :: Parser Network+networkArg =+ argument (eitherReader (f . netByName)) $+ metavar "NETWORK"+ <> completeWith ((.name) <$> allNets)+ <> help ("Specify the network of the account to delete: " <> nets)+ where+ nets = intercalate ", " ((.name) <$> allNets)+ f :: Maybe Network -> Either String Network+ f Nothing =+ Left $ "Invalid network name. Select one of the following: " <> nets+ f (Just res) = Right res++{- TestAcc Parser -}++testAccParser :: ParserInfo Command+testAccParser = do+ let cmd =+ CommandTestAcc+ <$> accountOption+ <*> splitInOption+ info cmd $+ progDescDoc (offline "Check the validity of your mnemonic/passphrase")+ <> footer+ [r|+`testacc` will prompt for your mnemonic/passphrase and check that the derived+public key M/44'/coin'/account' matches with the account stored on disk.+|]++accountOption :: Parser (Maybe Text)+accountOption =+ optional . strOption $+ short 'a'+ <> long "account"+ <> metavar "TEXT"+ <> completer (mkCompleter accountCompleter)+ <> help+ [r|+Specify the account name if the wallet has more than one account.+|]++{- ImportAcc Parser -}++importAccParser :: ParserInfo Command+importAccParser = do+ let cmd =+ CommandImportAcc+ <$> fileArgument "Path to the account file"+ info cmd $+ progDesc "Import an account file"+ <> footer importExportAccFooter++importExportAccFooter :: String+importExportAccFooter =+ [r|+When working in an online/offline environment, you can `importacc` a public key+file (on an online computer) that was exported with `exportacc` (on an offline+computer). This will allow the online computer to monitor transactions without+requiring access to the private keys. The file contains the public key+derivation M/44'/coin'/account' along with the account name, the network and a+wallet identifier. The wallet identifier is a fingerprint of the first public+account derivation M/44'/coin'/0'.+|]++{- ExportAcc Parser -}++exportAccParser :: ParserInfo Command+exportAccParser = do+ let cmd =+ CommandExportAcc+ <$> accountOption+ <*> fileArgument "File where the account data will be saved"+ info cmd $+ progDesc "Export account data to a file"+ <> footer importExportAccFooter++{- RenameAcc Parser -}++renameAccParser :: ParserInfo Command+renameAccParser = do+ let cmd =+ CommandRenameAcc+ <$> accountArg "Old account name"+ <*> textArg "New account name"+ info cmd $ progDesc "Rename an account"++accountArg :: String -> Parser Text+accountArg desc =+ argument str $+ metavar "TEXT"+ <> completer (mkCompleter accountCompleter)+ <> help desc++accountCompleter :: String -> IO [String]+accountCompleter _ = return []++{- TODO: Fix this+accountCompleter :: String -> IO [String]+accountCompleter pref = do+ names <- runDB getAccountNames+ return $ sort $ nub $ filter (pref `isPrefixOf`) (cs <$> names)+-}++{- Accounts Parser -}++accountsParser :: ParserInfo Command+accountsParser = do+ let cmd = CommandAccounts <$> accountOption+ info cmd $ progDesc "Display account information"++{- Receive Parser -}++receiveParser :: ParserInfo Command+receiveParser = do+ let cmd =+ CommandReceive <$> accountOption <*> labelOption+ info cmd $+ progDesc "Get a new address for receiving a payment"+ <> footer+ [r|+There are two types of addresses in a BIP-44 wallet: internal and external addresses.+Internal addresses are not exposed to the user and are managed internally by the+wallet (mostly for producing change outputs in transactions). The `receive` command+produces external addresses that are meant for receiving payments. Internal addresses+are derived under the BIP-32 path M/44'/coin'/account'/1/address. External addresses+use the path M/44'/coin'/account'/0/address.+|]++labelOption :: Parser (Maybe Text)+labelOption =+ optional . strOption $+ short 'l'+ <> long "label"+ <> metavar "TEXT"+ <> help "Specify a label for the address"++{- Addrs Parser -}++addrsParser :: ParserInfo Command+addrsParser = do+ let cmd =+ CommandAddrs+ <$> accountOption+ <*> (Page <$> limitOption <*> offsetOption)+ info cmd $+ progDesc "List the receiving addresses of an account"++offsetOption :: Parser Natural+offsetOption =+ option (maybeReader $ readNatural . cs) $+ short 'o'+ <> long "offset"+ <> metavar "INT"+ <> value 0+ <> showDefault+ <> help "Offset the result set"++limitOption :: Parser Natural+limitOption =+ option (maybeReader $ readNatural . cs) $+ short 'l'+ <> long "limit"+ <> metavar "INT"+ <> value 5+ <> showDefault+ <> help+ [r|+Limit the result set. If the result set is very large, you can specify the+--limit and --offset options to view the required data. For example, to skip the+first 20 values and then display the following 10, use --limit=10 and+--offset=20.+|]++{- Label Parser-}++labelParser :: ParserInfo Command+labelParser = do+ let cmd =+ CommandLabel+ <$> accountOption+ <*> addrIndexArg+ <*> textArg "The new label for the address"+ info cmd $ progDesc "Set the label of an address"++addrIndexArg :: Parser Natural+addrIndexArg =+ argument (maybeReader $ readNatural . cs) $+ metavar "INT"+ <> help+ [r|+The index of the external address to update. The first address has an index of+0.+|]++{- Txs Parser -}++txsParser :: ParserInfo Command+txsParser = do+ let cmd =+ CommandTxs+ <$> accountOption+ <*> (Page <$> limitOption <*> offsetOption)+ info cmd $ progDesc "Display the transactions of an account"++{- PrepareTx Parser -}++prepareTxParser :: ParserInfo Command+prepareTxParser = do+ let cmd =+ CommandPrepareTx+ <$> some recipientArg+ <*> accountOption+ <*> feeOption+ <*> dustOption+ <*> rcptPayOption+ <*> minConfOption+ <*> outputFileMaybeOption+ info cmd $+ progDesc "Prepare an unsigned transaction for making a payment"+ <> footer+ [r|+In hw, sending a transaction happens in separate steps. First, an unsigned+transaction is prepared using `preparetx`. No mnemonic or private keys are+required for this step and `preparetx` can be run on an online computer. The new+transaction will be stored in the local wallet database as a "pending"+transaction but it can also be exported as a file using the --output option. The+pending transactions can be inspected either with `reviewtx` or `pendingtxs`.+The new transaction is not signed. The next step will be to sign it. Any coins+spent by it will be locked to prevent spending them twice. If you change your+mind about sending this transaction, you can call `deletetx` to remove it and+free up the locked coins. All the pending transactions are referenced by their+noSigHash (a hash of the transaction without its signatures).+|]++recipientArg :: Parser (Text, Text)+recipientArg = (,) <$> addressArg <*> amountArg++addressArg :: Parser Text+addressArg =+ strArgument $+ metavar "ADDRESS"+ <> help+ [r|+Recipient address. You can provide multiple "ADDRESS AMOUNT" pairs. +|]++amountArg :: Parser Text+amountArg =+ strArgument $+ metavar "AMOUNT"+ <> help+ [r|+Recipient amount. By default, amounts are parsed as bitcoins. You can also use the+--satoshi or --bit option to specify amounts in satoshis or bits. For example,+"0.00001" bitcoins is equivalent to "10.00" bits or "1000" satoshi. Bitcoins can+have up to 8 decimal places, bits up to 2 and satoshi are whole numbers.+|]++feeOption :: Parser Natural+feeOption =+ option (maybeReader $ readNatural . cs) $+ short 'f'+ <> long "fee"+ <> metavar "INT"+ <> value 200+ <> showDefault+ <> help "Fee to pay in satoshi/bytes"++dustOption :: Parser Natural+dustOption =+ option (maybeReader $ readNatural . cs) $+ short 'd'+ <> long "dust"+ <> metavar "INT"+ <> value 5430+ <> showDefault+ <> help "Amount (in satoshi) below which an output is considered dust"++rcptPayOption :: Parser Bool+rcptPayOption =+ switch $+ short 'r'+ <> long "recipientpay"+ <> showDefault+ <> help "The transaction fee will be deducted from the recipient amounts"++minConfOption :: Parser Natural+minConfOption =+ option (maybeReader $ readNatural . cs) $+ short 'c'+ <> long "minconf"+ <> metavar "INT"+ <> value 1+ <> showDefault+ <> help "Minimum number of confirmations required to spend a coin"++outputFileMaybeOption :: Parser (Maybe FilePath)+outputFileMaybeOption =+ optional . strOption $+ short 'o'+ <> long "output"+ <> metavar "FILENAME"+ <> action "file"+ <> help "Write the result to this file"++{- PendingTxs Parser -}++pendingTxsParser :: ParserInfo Command+pendingTxsParser = do+ let cmd =+ CommandPendingTxs+ <$> accountOption+ <*> (Page <$> limitOption <*> offsetOption)+ info cmd $+ progDesc "Display the pending transactions of an account"+ <> footer+ [r|+Pending transactions have been created with the `preparetx` command. They are+either unsigned and waiting to be signed, or signed and waiting to be sent to+the network.+|]++{- ReviewTx Parser -}++reviewTxParser :: ParserInfo Command+reviewTxParser = do+ let cmd =+ CommandReviewTx+ <$> accountOption+ <*> fileArgument "Path to the transaction file to review"+ info cmd $+ progDesc "Review a transaction file"+ <> footer+ [r|+Review a pending transaction file that has been created with the --output option+of the `preparetx` command or the `exporttx` command. You can review a+transaction file this way before signing it on an offline computer for example.+|]++{- ExportTx Parser -}++exportTxParser :: ParserInfo Command+exportTxParser = do+ let cmd =+ CommandExportTx+ <$> nosigHashArg+ <*> fileArgument "File where the transaction data will be saved"+ info cmd $+ progDesc "Export pending transaction data to a file"+ <> footer importExportTxFooter++importExportTxFooter :: String+importExportTxFooter =+ [r|+A transaction that has been prepared with `preparetx` can be exported to a file+so that it can be signed on an offline computer. The transaction is identified+by its nosigHash (a hash of the transaction without its signatures) as this+identifier doesn't change when the transaction is unsigned or signed. Once+signed, you can `importtx` the transaction back into the computer that+prepared the transaction. +|]++nosigHashArg :: Parser TxHash+nosigHashArg =+ argument (maybeReader $ hexToTxHash . cs) $+ metavar "NOSIGHASH"+ <> help "The nosigHash of the transaction"++{- ImportTx Parser -}++importTxParser :: ParserInfo Command+importTxParser = do+ let cmd =+ CommandImportTx+ <$> accountOption+ <*> fileArgument "Path to the transaction file to import"+ info cmd $+ progDesc "Import a pending transaction"+ <> footer importExportTxFooter++{- DeleteTx Parser -}++deleteTxParser :: ParserInfo Command+deleteTxParser = do+ let cmd = CommandDeleteTx <$> nosigHashArg+ info cmd $+ progDesc "Delete a pending transaction"+ <> footer+ [r|+A transaction that has been prepared with `preparetx` will lock the coins that+it has spent to prevent double-spending them. A pending transaction can be+deleted with `deletetx` to permanently remove it from the database. This will+also free any coins that have been locked by it. This will only be possible as+long as the transaction hasn't been signed and sent to the network.+|]++{- SignTx Parser -}++signTxParser :: ParserInfo Command+signTxParser = do+ let cmd =+ CommandSignTx+ <$> accountOption+ <*> optional nosigHashArg+ <*> inputFileMaybeOption+ <*> outputFileMaybeOption+ <*> splitInOption+ info cmd $+ progDescDoc (offline "Sign a transaction")+ <> footer+ [r|+The next step after preparing a transaction is to sign it. This step can happen+on an offline computer if you export the unsigned transaction to a file. If the+transaction is stored in the wallet, it can be signed by using its nosigHash.+This will sign and replace the transaction in-place. Otherwise, you must specify+the --input file of the unsigned transaction and the --output file where the+signed transaction will be saved. The transaction still hasn't been uploaded to+the network. You may inspect it using either the `reviewtx` or the `pendingtxs`+command. The transaction can then be sent to the network with `sendtx` or+deleted with `deletetx`.+|]++inputFileMaybeOption :: Parser (Maybe FilePath)+inputFileMaybeOption =+ optional . strOption $+ short 'i'+ <> long "input"+ <> metavar "FILENAME"+ <> action "file"+ <> help "Read the input from this file"++{- Coins Parser -}++coinsParser :: ParserInfo Command+coinsParser = do+ let cmd =+ CommandCoins+ <$> accountOption+ <*> (Page <$> limitOption <*> offsetOption)+ info cmd $+ progDesc "List the account coins (unspent outputs)"+ <> footer+ [r|+These are all the coins in the account. Coins can be locked when preparing new+transactions. When sending those transactions to the network, the locked coins+will be spent and removed. Calling `deletetx` will remove a pending transaction+and free up any locked coins.+|]++{- SendTx Parser -}++sendTxParser :: ParserInfo Command+sendTxParser = do+ let cmd = CommandSendTx <$> nosigHashArg+ info cmd $+ progDesc "Send (upload) a signed transaction to the network"+ <> footer+ [r|+Once a transaction is signed, it can be sent to the network. This step is+irreversible and the transaction will become effective. Make sure everything is+correct with your transaction before sending it. You must `importtx` the+transaction before sending it if you have it in a file.+|]++{- SyncAcc Parser -}++syncAccParser :: ParserInfo Command+syncAccParser = do+ let cmd = CommandSyncAcc <$> accountOption <*> fullOption+ info cmd $+ progDesc "Download account data from the network"+ <> footer+ [r|+The `syncacc` command will download the latest address balances, transactions+and coins from the network. It will also update the account balances.+|]++fullOption :: Parser Bool+fullOption =+ switch $+ long "full"+ <> showDefault+ <> help+ [r|+Force the syncacc command to re-download all the account data instead of only+the deltas (the data that has changed). This should normally not be required+unless some information is wrong, for example, when a previous syncacc was+called on a partially discovered account (the account was missing some+addresses).+|]++{- DiscoverAcc Parser -}++discoverAccParser :: ParserInfo Command+discoverAccParser = do+ let cmd = CommandDiscoverAcc <$> accountOption+ info cmd $+ progDesc "Scan the blockchain to generate missing addresses"+ <> footer+ [r|+`discoveracc` is typically called when restoring an account in a new wallet.+Initially, there will be no addresses stored in the account. `discoveracc` will+scan the blockchain and generate all the addresses (both external and internal)+that have received coins at some point. The search is stopped when a gap+(default = 20) of empty addresses is found. A full sync (syncacc --full) is+run automatically at the end of the `discoveracc` command. When restoring an+old wallet, it is important to discover it first before generating addresses+and receiving payments. Otherwise some addresses might be reused.+|]++{- Backup & Restore Parsers -}++backupParser :: ParserInfo Command+backupParser = do+ let cmd =+ CommandBackup+ <$> fileArgument "File where the backup will be saved"+ info cmd $+ progDesc "Create a wallet backup file"+ <> footer+ [r|+This command will backup your wallet accounts, your address labels and the+internal free addresses to an external file. This is enough to restore your+entire wallet state with some network help. It will, however, not backup any+pending transactions you might have so you should save those separately.+|]++restoreParser :: ParserInfo Command+restoreParser = do+ let cmd =+ CommandRestore+ <$> fileArgument "Path to the backup file"+ info cmd $+ progDesc "Restore a wallet backup file"+ <> footer+ [r|+Restore a backup file containing your accounts, address labels and internal+free addresses. This command will connect to the network to download everything+else that is not included in the backup file like your transactions and coins.+Pending transactions are not included in the backup file.+|]++{- Version Parser -}++versionParser :: ParserInfo Command+versionParser = do+ let cmd = pure CommandVersion+ info cmd $ progDesc "Display the version of hw"++{- PrepareSweep Parser -}++prepareSweepParser :: ParserInfo Command+prepareSweepParser = do+ let cmd =+ CommandPrepareSweep+ <$> accountOption+ <*> prvKeyFileOption+ <*> some sweepToOption -- some: optional+ <*> outputFileMaybeOption+ <*> feeOption+ <*> dustOption+ info cmd $+ progDesc "Prepare a transaction to sweep funds"+ <> footer+ [r|+`preparesweep` will prepare a transaction that sweeps the funds of the given+private keys. The private keys are passed in a file and can be in WIF or minikey+format. The typical use case for this command is to migrate an old wallet to a+new mnemonic. You can send the funds to 1 or multiple addresses. You can use the+shorthand format -t ADDR1 -t ADDR2 to specify the --sweepto addresses.+|]++sweepFromOption :: Parser Text+sweepFromOption =+ strOption $+ short 'w'+ <> long "sweepfrom"+ <> metavar "ADDRESS"+ <> help "Addresses to sweep from"++addressFileOption :: Parser (Maybe FilePath)+addressFileOption =+ optional . strOption $+ long "addrfile"+ <> metavar "FILENAME"+ <> action "file"+ <> help "File containing addresses to sweep from"++sweepToOption :: Parser Text+sweepToOption =+ strOption $+ short 't'+ <> long "sweepto"+ <> metavar "ADDRESS"+ <> help "Addresses to sweep to"++{- SignSweep Parser -}++signSweepParser :: ParserInfo Command+signSweepParser = do+ let cmd =+ CommandSignSweep+ <$> accountOption+ <*> optional nosigHashArg+ <*> inputFileMaybeOption+ <*> outputFileMaybeOption+ <*> prvKeyFileOption+ info cmd $+ progDesc "Sign a sweep transaction"+ <> footer+ [r|+Sign a transaction that was prepared with the `preparesweep` command. As the+coins of this transaction are not from this wallet, the private keys for signing+must be passed in a separate --prvkeys file. The sweep transaction can be+referenced by its nosigHash if it exists in the wallet or you can pass it as a+file --input.+|]++prvKeyFileOption :: Parser FilePath+prvKeyFileOption =+ strOption $+ short 'k'+ <> long "prvkeys"+ <> metavar "FILENAME"+ <> action "file"+ <> help "File containing the private keys in WIF or MiniKey format"++{- RollDice Parser -}++rollDiceParser :: ParserInfo Command+rollDiceParser = do+ let cmd = CommandRollDice <$> diceCountArg+ info cmd $+ progDesc "Roll 6-sided dice using the systems internal entropy"+ <> footer+ [r|+By default, /dev/random is used on linux machines. The mapping from a byte to+dice rolls ensures that all dice rolls have the same probability of occuring.+Bytes 0x00 to 0xfb are used and anything above is dropped.+|]++diceCountArg :: Parser Natural+diceCountArg =+ argument (maybeReader $ readNatural . cs) $+ metavar "INT"+ <> help "Number of 6-sided dice to roll"++{- Argument parser helpers -}++textArg :: String -> Parser Text+textArg desc =+ argument str $+ metavar "TEXT"+ <> help desc++fileArgument :: String -> Parser FilePath+fileArgument desc =+ strArgument $+ metavar "FILENAME"+ <> action "file"+ <> help desc
@@ -0,0 +1,547 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections #-}++module Haskoin.Wallet.PrettyPrinter where++import Control.Monad+import Data.List (intercalate, intersperse)+import qualified Data.Map.Strict as Map+import Data.Maybe (fromMaybe)+import Data.String.Conversions (cs)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Time.Format+import Data.Word+import Haskoin+import Haskoin.Wallet.Amounts+import Haskoin.Wallet.Backup+import Haskoin.Wallet.Commands+import Haskoin.Wallet.Database+import Haskoin.Wallet.TxInfo+import Haskoin.Wallet.Util+import Numeric.Natural (Natural)+import System.Console.ANSI+import System.Exit+import System.IO++data Printer+ = PConcat !Printer !Printer+ | PNewline !Printer+ | PNest !Natural !Printer+ | PText [SGR] !String+ | PEmpty+ | PNonEmpty++instance Semigroup Printer where+ a <> PEmpty = a+ PEmpty <> b = b+ a <> b = PConcat a b++instance Monoid Printer where+ mempty = PEmpty++isEmptyPrinter :: Printer -> Bool+isEmptyPrinter prt =+ case prt of+ PEmpty -> True+ (PText _ s) -> null s+ (PNest _ n) -> isEmptyPrinter n+ (PNewline n) -> isEmptyPrinter n+ _ -> False++text :: String -> Printer+text = PText []++(<+>) :: Printer -> Printer -> Printer+p1 <+> p2+ | isEmptyPrinter p1 = p2+ | isEmptyPrinter p2 = p1+ | otherwise = p1 <> text " " <> p2++vcat :: [Printer] -> Printer+vcat = go . filter (not . isEmptyPrinter)+ where+ go [] = PEmpty+ go [x] = x+ go (x : xs) = x <> PNewline (go xs)++hsep :: [Printer] -> Printer+hsep = go . filter (not . isEmptyPrinter)+ where+ go [] = PEmpty+ go [x] = x+ go (x : xs) = x <+> go xs++nest :: Natural -> Printer -> Printer+nest = PNest++block :: Natural -> String -> String+block n str =+ case n `safeSubtract` fromIntegral (length str) of+ Just missing -> str <> replicate (fromIntegral missing) ' '+ _ -> str++rblock :: Natural -> String -> String+rblock n str =+ case n `safeSubtract` fromIntegral (length str) of+ Just missing -> replicate (fromIntegral missing) ' ' <> str+ _ -> str++renderIO :: Printer -> IO ()+renderIO cp = go 0 0 cp >> putStrLn ""+ where+ go :: Natural -> Natural -> Printer -> IO Natural+ go l n p+ | isEmptyPrinter p = return l+ | otherwise =+ case p of+ PConcat p1 p2 -> do+ l2 <- go l n p1+ go l2 n p2+ PNewline p1 -> do+ putStrLn ""+ putStr $ replicate (fromIntegral n) ' '+ go n n p1+ PNest i p1 -> do+ putStr $ replicate (fromIntegral i) ' '+ go (l + i) (n + i) p1+ PText xs s -> do+ printFormat xs s+ return $ l + fromIntegral (length s)+ PEmpty -> return l+ PNonEmpty -> return l++formatTitle :: String -> Printer+formatTitle =+ PText+ [ SetConsoleIntensity BoldIntensity,+ SetColor Foreground Vivid White+ ]++formatAccount :: String -> Printer+formatAccount =+ PText+ [ SetConsoleIntensity BoldIntensity,+ SetColor Foreground Vivid Cyan+ ]++formatFilePath :: String -> Printer+formatFilePath =+ PText+ [ SetItalicized True,+ SetColor Foreground Vivid White+ ]++formatKey :: String -> Printer+formatKey = PText [SetColor Foreground Dull White]++formatValue :: String -> Printer+formatValue = PText [SetColor Foreground Vivid White]++formatLabel :: String -> Printer+formatLabel =+ PText+ [ SetItalicized True,+ SetColor Foreground Vivid White+ ]++formatDeriv :: String -> Printer+formatDeriv =+ PText+ [ SetItalicized True,+ SetColor Foreground Vivid Yellow+ ]++formatMnemonic :: Bool -> String -> Printer+formatMnemonic split =+ PText+ [ SetConsoleIntensity BoldIntensity,+ SetColor Foreground Vivid (if split then Yellow else Cyan)+ ]++formatAddress :: Bool -> String -> Printer+formatAddress False = PText [SetColor Foreground Dull White]+formatAddress True = PText [SetColor Foreground Vivid White]++formatTxHash :: String -> Printer+formatTxHash = PText [SetColor Foreground Dull White]++formatNoSigTxHash :: String -> Printer+formatNoSigTxHash = PText [SetColor Foreground Vivid Magenta]++formatBlockHash :: String -> Printer+formatBlockHash = PText [SetColor Foreground Dull White]++formatPosAmount :: String -> Printer+formatPosAmount = PText [SetColor Foreground Vivid Green]++formatNegAmount :: String -> Printer+formatNegAmount = PText [SetColor Foreground Vivid Red]++formatZeroAmount :: String -> Printer+formatZeroAmount = PText [SetColor Foreground Vivid White]++formatDice :: String -> Printer+formatDice =+ PText+ [ SetConsoleIntensity BoldIntensity,+ SetColor Foreground Vivid Yellow+ ]++formatTrue :: String -> Printer+formatTrue = PText [SetColor Foreground Vivid Green]++formatFalse :: String -> Printer+formatFalse = PText [SetColor Foreground Vivid Red]++formatError :: String -> Printer+formatError = PText [SetColor Foreground Vivid Red]++printFormat :: [SGR] -> String -> IO ()+printFormat sgr str = do+ support <- hSupportsANSI stdout+ when support $ setSGR sgr+ putStr str+ when support $ setSGR []++{- Pretty Response -}++mnemonicPrinter :: Bool -> [Text] -> Printer+mnemonicPrinter split ws =+ vcat $+ fmap (mconcat . fmap formatWord) $+ chunksOf 4 $+ zip ([1 ..] :: [Natural]) ws+ where+ formatWord (i, w) =+ mconcat+ [ formatKey $ block 4 $ show i <> ".",+ formatMnemonic split $ block 10 $ cs w+ ]++partsPrinter :: [[Text]] -> [Printer]+partsPrinter [] = mempty+partsPrinter xs =+ concatMap formatPart $ zip ([1 ..] :: [Natural]) xs+ where+ formatPart (i, ws) =+ [ formatTitle $ "Private Mnemonic Part #" <> show i,+ nest 2 $ mnemonicPrinter True ws+ ]++keyPrinter :: Natural -> Text -> Printer+keyPrinter n txt = formatKey (block n (cs txt)) <> text ": "++amountPrinter :: AmountUnit -> Word64 -> Printer+amountPrinter unit = integerAmountPrinter unit . fromIntegral++integerAmountPrinter :: AmountUnit -> Integer -> Printer+integerAmountPrinter unit amnt+ | amnt == 0 = integerAmountPrinterWith formatZeroAmount unit amnt+ | amnt > 0 = integerAmountPrinterWith formatPosAmount unit amnt+ | otherwise = integerAmountPrinterWith formatNegAmount unit amnt++integerAmountPrinterWith ::+ (String -> Printer) -> AmountUnit -> Integer -> Printer+integerAmountPrinterWith f unit amnt =+ f (cs $ showIntegerAmount unit amnt) <+> unitPrinter unit amnt++unitPrinter :: AmountUnit -> Integer -> Printer+unitPrinter unit = text . cs . showUnit unit++naturalPrinter :: Natural -> Printer+naturalPrinter nat+ | nat == 0 = formatZeroAmount $ show nat+ | otherwise = formatPosAmount $ show nat++accountPrinter :: AmountUnit -> DBAccount -> Printer+accountPrinter unit acc =+ vcat+ [ keyPrinter 8 "Account" <> formatAccount (cs $ dBAccountName acc),+ mconcat [keyPrinter 8 "Wallet", formatValue $ cs fp],+ mconcat [keyPrinter 8 "Deriv", formatDeriv deriv],+ mconcat [keyPrinter 8 "Network", formatValue net],+ mconcat+ [keyPrinter 8 "External", formatValue $ show $ dBAccountExternal acc],+ mconcat+ [keyPrinter 8 "Internal", formatValue $ show $ dBAccountInternal acc],+ mconcat [keyPrinter 8 "Created", formatValue created],+ formatKey "Balances:",+ nest 2 $+ mconcat+ [ keyPrinter 11 "Confirmed",+ amountPrinter unit $ dBAccountBalanceConfirmed acc+ ],+ nest 2 $+ mconcat+ [ keyPrinter 11 "Unconfirmed",+ amountPrinter unit $ dBAccountBalanceUnconfirmed acc+ ],+ nest 2 $+ mconcat+ [ keyPrinter 11 "Coins",+ naturalPrinter $ fromIntegral $ dBAccountBalanceCoins acc+ ]+ ]+ where+ DBWalletKey fp = dBAccountWallet acc+ deriv = cs $ dBAccountDerivation acc+ net = (accountNetwork acc).name+ utctime = dBAccountCreated acc+ created = formatTime defaultTimeLocale "%d %b %Y" utctime++addressPrinter :: AmountUnit -> Natural -> DBAddress -> Printer+addressPrinter unit pad addr =+ vcat+ [ mconcat+ [ keyPrinter pad $ cs $ show (dBAddressIndex addr),+ formatAddress True $ cs $ dBAddressAddress addr+ ],+ if Text.null $ dBAddressLabel addr+ then PEmpty+ else+ nest 2 $+ mconcat+ [ keyPrinter 8 "Label",+ formatLabel $ cs $ dBAddressLabel addr+ ],+ nest 2 $+ mconcat+ [ keyPrinter 8 "Txs",+ naturalPrinter $ fromIntegral $ dBAddressBalanceTxs addr+ ],+ nest 2 $+ mconcat+ [ keyPrinter 8 "Received",+ amountPrinter unit $ dBAddressBalanceReceived addr+ ]+ ]++txInfoPrinter ::+ Network ->+ AmountUnit ->+ TxInfo ->+ Printer+txInfoPrinter net unit TxInfo {..} =+ vcat+ [ formatTitle (block 9 title) <> text ": " <> total,+ txid,+ pending,+ fee,+ internal,+ debit,+ credit+ ]+ where+ title =+ case txInfoType of+ TxDebit -> "Debit"+ TxInternal -> "Internal"+ TxCredit -> "Credit"+ total = integerAmountPrinter unit txInfoAmount+ fee = keyPrinter 9 "Fee" <> feePrinter unit txInfoFee txInfoFeeByte+ txid =+ case txInfoHash of+ Just tid ->+ keyPrinter 9 "TxHash" <> formatTxHash (cs $ txHashToHex tid)+ _ -> mempty+ pending =+ case txInfoPending of+ Just (TxInfoPending nosigH signed online) ->+ vcat+ [ keyPrinter 9 "NoSigHash"+ <> formatNoSigTxHash (cs $ txHashToHex nosigH),+ keyPrinter 9 "Signed"+ <> text "This pending transaction is"+ <+> if signed+ then formatTrue "signed"+ else formatFalse "not signed",+ if online+ then+ keyPrinter 9 "Online"+ <> text "This transaction is"+ <+> formatTrue "online"+ else mempty+ ]+ _ -> keyPrinter 9 "Confs" <> formatValue (show txInfoConfirmations)+ internal+ | txInfoType /= TxInternal = mempty+ | otherwise =+ vcat $+ [formatKey "From addresses:"]+ <> ( nest 2 . addrPrinter True+ <$> Map.assocs (Map.map g txInfoMyInputs)+ )+ <> [formatKey "To addresses:"]+ <> ( nest 2+ . addrPrinter True+ <$> Map.assocs (Map.map f txInfoMyOutputs)+ )+ debit+ | txInfoType /= TxDebit = mempty+ | otherwise =+ vcat $+ [formatKey "Sending to addresses:"]+ <> ( nest 2 . addrPrinter False+ <$> Map.assocs+ ( Map.map+ (,Nothing,"")+ txInfoOtherOutputs+ )+ )+ credit+ | txInfoType /= TxCredit = mempty+ | otherwise =+ vcat $+ [formatKey "My credited addresses:"]+ <> ( nest 2 . addrPrinter True+ <$> Map.assocs (Map.map f txInfoMyOutputs)+ )+ f (MyOutputs v p l) = (v, Just p, cs l)+ g (MyInputs v p l _) = (v, Just p, cs l)+ addrPrinter isMine (a, (v, pathM, label)) =+ vcat+ [ formatAddress isMine (parseAddr net a)+ <> text ":"+ <+> if isMine+ then integerAmountPrinterWith formatPosAmount unit (fromIntegral v)+ else integerAmountPrinterWith formatZeroAmount unit (fromIntegral v),+ case pathM of+ Just p ->+ nest 2 $+ formatKey "Path:" <+> formatDeriv (pathToStr p)+ _ -> mempty,+ if null label+ then mempty+ else nest 2 $ formatKey "Label:" <+> formatLabel label+ ]++parseAddr :: Network -> Address -> String+parseAddr net = cs . fromMaybe "Invalid Address" . addrToText net++feePrinter :: AmountUnit -> Natural -> Natural -> Printer+feePrinter unit fee feeBytes =+ integerAmountPrinterWith formatZeroAmount unit (fromIntegral fee)+ <+> text "("+ <> formatValue (show feeBytes)+ <+> text "sat/bytes"+ <> text ")"++coinPrinter :: Network -> AmountUnit -> JsonCoin -> Printer+coinPrinter net unit JsonCoin {..} =+ vcat+ [ formatKey (block 7 "TxHash")+ <> text ":"+ <+> formatTxHash (cs $ txHashToHex jsonCoinOutpoint.hash),+ formatKey (block 7 "Index")+ <> text ":"+ <+> formatValue (show jsonCoinOutpoint.index),+ formatKey (block 7 "Value")+ <> text ":"+ <+> amountPrinter unit jsonCoinValue,+ formatKey (block 7 "Address")+ <> text ":"+ <+> formatAddress True (parseAddr net jsonCoinAddress),+ formatKey (block 7 "Confs")+ <> text ":"+ <+> formatValue (show jsonCoinConfirmations),+ formatKey (block 7 "Status")+ <> text ":"+ <+> if jsonCoinLocked+ then formatFalse "Locked"+ else formatTrue "Free"+ ]++prettyPrinter :: AmountUnit -> Response -> IO ()+prettyPrinter unit =+ \case+ ResponseError err -> do+ renderIO . mconcat $ [formatError "Error: ", text $ cs err]+ exitFailure+ ResponseMnemonic orig mnem parts ->+ renderIO . vcat $+ [ formatTitle "System Entropy Source",+ nest 2 $ formatFilePath $ cs orig,+ formatTitle "Private Mnemonic",+ nest 2 $ mnemonicPrinter False mnem+ ]+ <> partsPrinter parts+ ResponseAccount acc -> renderIO $ accountPrinter unit acc+ ResponseAccounts [] -> renderIO $ text "There are no accounts to display"+ ResponseAccounts accs ->+ renderIO . vcat $ intersperse (text " ") $ accountPrinter unit <$> accs+ ResponseAccResult _ res txt -> do+ let p =+ if res+ then formatTrue "Success"+ else formatFalse "Failure"+ renderIO . vcat $ [formatKey "Result:" <+> p, text $ cs txt]+ ResponseFile f -> renderIO $ formatKey "File:" <+> formatFilePath f+ ResponseAddress _ addr -> do+ let l = length $ show $ dBAddressIndex addr+ renderIO $ addressPrinter unit (fromIntegral l) addr+ ResponseAddresses _ [] ->+ renderIO $ text "There are no addresses to display"+ ResponseAddresses _ addrs -> do+ let l = length $ show $ maximum $ dBAddressIndex <$> addrs+ renderIO $ vcat $ addressPrinter unit (fromIntegral l) <$> addrs+ ResponseTxs _ [] ->+ renderIO $ text "There are no transactions to display"+ ResponseTxs acc txs -> do+ let net = accountNetwork acc+ renderIO $ vcat $ intersperse (text " ") $ txInfoPrinter net unit <$> txs+ ResponseTx acc txInfo -> do+ let net = accountNetwork acc+ renderIO $ txInfoPrinter net unit txInfo+ ResponseDeleteTx h c a ->+ renderIO $+ vcat+ [ formatKey "Deleted:" <+> formatNoSigTxHash (cs $ txHashToHex h),+ nest 2 $ formatKey "Freed coins:" <+> formatValue (show c),+ nest 2 $+ formatKey "Freed internal addresses:" <+> formatValue (show a)+ ]+ ResponseCoins _ [] ->+ renderIO $ text "There are no coins to display"+ ResponseCoins acc coins -> do+ let net = accountNetwork acc+ renderIO $ vcat $ intersperse (text " ") $ coinPrinter net unit <$> coins+ ResponseSync xs -> do+ let f (SyncRes acc bh h t c) =+ [ accountPrinter unit acc,+ formatTitle "Sync Results:",+ nest 2 $+ keyPrinter 12 "Best Block"+ <> formatBlockHash (cs $ blockHashToHex bh),+ nest 2 $ keyPrinter 12 "Best Height" <> formatValue (show h),+ nest 2 $ keyPrinter 12 "Tx updates" <> formatValue (show t),+ nest 2 $ keyPrinter 12 "Coin updates" <> formatValue (show c)+ ]+ renderIO $ vcat $ intercalate [text " "] (f <$> xs)+ ResponseRestore as -> do+ let f (acc, t, c) =+ [ accountPrinter unit acc,+ formatTitle "Restore Results:",+ nest 2 $ keyPrinter 12 "Tx updates" <> formatValue (show t),+ nest 2 $ keyPrinter 12 "Coin updates" <> formatValue (show c)+ ]+ renderIO $ vcat $ intercalate [text " "] (f <$> as)+ ResponseVersion v dbv ->+ renderIO $+ vcat+ [ formatKey "Software version:" <+> formatValue (cs v),+ formatKey "Database version:" <+> formatValue (cs dbv)+ ]+ ResponseRollDice ds e ->+ renderIO . vcat $+ [ formatKey "System Entropy Source:" <+> formatFilePath (cs e),+ formatKey "Dice rolls:"+ <+> mconcat (intersperse (text ", ") (formatDice . show <$> ds))+ ]
@@ -0,0 +1,310 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++module Haskoin.Wallet.Signing where++import Conduit (MonadUnliftIO)+import Control.Arrow (second)+import Control.Monad (unless, when, (<=<))+import Control.Monad.Except+import Control.Monad.State+import qualified Data.ByteString as BS+import Data.Default (def)+import Data.Either (rights)+import Data.List (nub)+import qualified Data.Map.Strict as Map+import Data.Text (Text)+import Data.Word (Word64)+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Store.WebClient+import Haskoin.Wallet.Config+import Haskoin.Wallet.Database+import Haskoin.Wallet.FileIO+import Haskoin.Wallet.TxInfo+import Haskoin.Wallet.Util+import Numeric.Natural (Natural)+import System.Random (Random (randomR), StdGen, initStdGen)++{- Building Transactions -}++buildTxSignData ::+ (MonadUnliftIO m) =>+ Network ->+ Ctx ->+ Config ->+ StdGen ->+ DBAccountId ->+ [(Address, Natural)] ->+ Natural ->+ Natural ->+ Bool ->+ Natural ->+ ExceptT String (DB m) TxSignData+buildTxSignData net ctx cfg gen accId rcpts feeByte dust rcptPay minConf+ | null rcpts = throwError "No recipients provided"+ | otherwise = do+ -- Get all spendable coins in the account+ allCoins <- getSpendableCoins net accId minConf+ -- Get a change address+ dbAddr <- nextFreeIntAddr ctx cfg accId+ (change, changeDeriv) <- liftEither $ fromDBAddr net dbAddr+ -- Build a transaction and pick the coins+ (tx, pickedCoins) <-+ liftEither $+ buildWalletTx net ctx gen rcpts change allCoins feeByte dust rcptPay+ -- Get the derivations of our recipients and picked coins+ rcptsDerivsE <- mapM (lift . getAddrDeriv net accId) $ fst <$> rcpts+ let rcptsDerivs = rights rcptsDerivsE -- It's OK to fail here+ inDerivs <- mapM (liftEither <=< lift . getCoinDeriv net accId) pickedCoins+ -- Check if we have a change output+ let noChange = length tx.outputs == length rcpts+ outDerivs =+ if noChange+ then rcptsDerivs+ else changeDeriv : rcptsDerivs+ -- Get the dependent transactions+ let depTxHash = (.hash) . (.outpoint) <$> tx.inputs+ depTxs <- mapM getRawTx depTxHash+ -- Return the result+ return $ TxSignData tx depTxs (nub inDerivs) (nub outDerivs) False++buildWalletTx ::+ Network ->+ Ctx ->+ StdGen -> -- Randomness for coin selection and change address order+ [(Address, Natural)] -> -- recipients+ Address -> -- change+ [Store.Unspent] -> -- Coins to choose from+ Natural -> -- Fee per byte+ Natural -> -- Dust+ Bool -> -- Recipients Pay for Fee+ Either String (Tx, [Store.Unspent])+buildWalletTx net ctx gen rcptsN change coins feeByteN dustN rcptPay =+ flip evalStateT gen $ do+ rdmCoins <- randomShuffle coins+ (pickedCoins, changeAmnt) <-+ lift $ chooseCoins tot feeCoinSel (length rcptsN + 1) False rdmCoins+ let nOuts =+ if changeAmnt <= dust+ then length rcptsN+ else length rcptsN + 1+ totFee =+ guessTxFee (fromIntegral feeByteN) nOuts (length pickedCoins)+ rcptsPayN <-+ if rcptPay+ then lift $ makeRcptsPay (fromIntegral totFee) rcptsN+ else return rcptsN+ let rcpts = second fromIntegral <$> rcptsPayN+ allRcpts+ | changeAmnt <= dust = rcpts+ | otherwise = (change, changeAmnt) : rcpts+ ops = (.outpoint) <$> pickedCoins+ when (any ((<= dust) . snd) allRcpts) $+ lift $+ Left "Recipient output is smaller than the dust value"+ rdmRcpts <- randomShuffle allRcpts+ tx <- lift $ buildAddrTx net ctx ops =<< mapM (addrToText2 net) rdmRcpts+ return (tx, pickedCoins)+ where+ feeCoinSel =+ if rcptPay+ then 0+ else fromIntegral feeByteN :: Word64+ dust = fromIntegral dustN :: Word64+ tot = fromIntegral $ sum $ snd <$> rcptsN :: Word64++makeRcptsPay ::+ Natural -> [(Address, Natural)] -> Either String [(Address, Natural)]+makeRcptsPay fee rcpts =+ mapM f rcpts+ where+ f (a, v) = (a,) <$> maybeToEither err (v `safeSubtract` toPay)+ err = "Recipients can't pay for the fee"+ (q, r) = fee `quotRem` fromIntegral (length rcpts)+ toPay = if r == 0 then q else q + 1++{- Signing Transactions -}++data MnemonicPass = MnemonicPass+ { mnemonicWords :: !Text,+ mnemonicPass :: !Text+ }+ deriving (Eq, Show)++-- Compute an account signing key+signingKey :: Network -> Ctx -> MnemonicPass -> Natural -> Either String XPrvKey+signingKey net ctx mnem acc = do+ seed <- mnemonicToSeed (mnemonicPass mnem) (mnemonicWords mnem)+ return $ derivePath ctx (bip44Deriv net acc) (makeXPrvKey seed)++-- Compute the unique wallet fingerprint given a mnemonic+walletFingerprint :: Network -> Ctx -> MnemonicPass -> Either String Fingerprint+walletFingerprint net ctx mnem = do+ xPrvKey <- signingKey net ctx mnem 0+ return $ xPubFP ctx $ deriveXPubKey ctx xPrvKey++signWalletTx ::+ Network ->+ Ctx ->+ TxSignData ->+ XPrvKey ->+ Either String (TxSignData, TxInfo)+signWalletTx net ctx tsd@TxSignData {txSignDataInputPaths = inPaths} signKey =+ signTxWithKeys net ctx tsd publicKey prvKeys+ where+ publicKey = deriveXPubKey ctx signKey+ prvKeys = (.key) . (\p -> derivePath ctx p signKey) <$> inPaths++signTxWithKeys ::+ Network ->+ Ctx ->+ TxSignData ->+ XPubKey ->+ [SecKey] ->+ Either String (TxSignData, TxInfo)+signTxWithKeys net ctx tsd@(TxSignData tx _ _ _ signed) publicKey secKeys = do+ when signed $ Left "The transaction is already signed"+ when (null secKeys) $ Left "There are no private keys to sign"+ txInfo <- parseTxSignData net ctx publicKey tsd+ -- signing+ let myInputs = txInfoMyInputs txInfo+ othInputs = txInfoOtherInputs txInfo+ mySigInputs = mconcat $ myInputsSigInput <$> Map.elems myInputs+ othSigInputs = mconcat $ otherInputsSigInput <$> Map.elems othInputs+ sigInputs = mySigInputs <> othSigInputs+ signedTx <- signTx net ctx tx sigInputs secKeys+ let txInfoS = signTxInfo signedTx txInfo+ isSigned = verifyTxInfo net ctx signedTx txInfoS+ unless isSigned $ Left "The transaction could not be signed"+ return+ ( tsd {txSignDataTx = signedTx, txSignDataSigned = True},+ txInfoS+ )++signTxInfo :: Tx -> TxInfo -> TxInfo+signTxInfo tx txInfo =+ txInfo+ { txInfoHash = Just $ txHash tx,+ txInfoPending = Just $ TxInfoPending (nosigTxHash tx) True False+ }++verifyTxInfo :: Network -> Ctx -> Tx -> TxInfo -> Bool+verifyTxInfo net ctx tx txInfo =+ let myInputs = txInfoMyInputs txInfo+ othInputs = txInfoOtherInputs txInfo+ mySigInputs = mconcat $ myInputsSigInput <$> Map.elems myInputs+ othSigInputs = mconcat $ otherInputsSigInput <$> Map.elems othInputs+ sigInputs = mySigInputs <> othSigInputs+ f i = (i.script, i.value, i.outpoint)+ vDat = f <$> sigInputs+ in Just (txHash tx) == txInfoHash txInfo+ && noEmptyInputs tx+ && verifyStdTx net ctx tx vDat++noEmptyInputs :: Tx -> Bool+noEmptyInputs = (not . any BS.null) . fmap (.script) . (.inputs)++{- Transaction Sweeping -}++buildSweepSignData ::+ (MonadUnliftIO m) =>+ Network ->+ Ctx ->+ Config ->+ DBAccountId ->+ [SecKey] ->+ [Address] ->+ Natural ->+ Natural ->+ ExceptT String (DB m) TxSignData+buildSweepSignData net ctx cfg accId prvKeys sweepTo feeByte dust+ | null prvKeys = throwError "No private keys to sweep from"+ | null sweepTo = throwError "No addresses to sweep to"+ | otherwise = do+ let host = apiHost net cfg+ -- Generate the addresses to sweep from+ let sweepFrom = nub $ concatMap (genPossibleAddrs net ctx) prvKeys+ -- Get the unspent coins of the sweepFrom addresses+ Store.SerialList coins <-+ liftExcept . apiBatch ctx (configCoinBatch cfg) host $+ GetAddrsUnspent sweepFrom def {limit = Just 0}+ when (null coins) $+ throwError "There are no coins to sweep in those addresses"+ -- Build a set of sweep transactions+ gen <- liftIO initStdGen+ tx <- liftEither $ buildSweepTx net ctx gen coins sweepTo feeByte dust+ -- Get the dependent transactions+ let depTxHash = (.hash) . (.outpoint) <$> (.inputs) tx+ Store.RawResultList depTxs <-+ liftExcept . apiBatch ctx (configTxFullBatch cfg) host $+ GetTxsRaw depTxHash+ -- Check if any of the coins belong to us+ inDerivs <- rights <$> lift (mapM (getAddrDeriv net accId) sweepFrom)+ -- Check if any of the sweepTo addrs belong to us+ outDerivs <- rights <$> lift (mapM (getAddrDeriv net accId) sweepTo)+ return $ TxSignData tx depTxs (nub inDerivs) (nub outDerivs) False++genPossibleAddrs :: Network -> Ctx -> SecKey -> [Address]+genPossibleAddrs net ctx k+ | net `elem` [btc, btcTest, btcRegTest] =+ [ pubKeyAddr ctx pc,+ pubKeyAddr ctx pu,+ pubKeyWitnessAddr ctx pc,+ pubKeyWitnessAddr ctx pu,+ pubKeyCompatWitnessAddr ctx pc,+ pubKeyCompatWitnessAddr ctx pu+ ]+ | otherwise =+ [ pubKeyAddr ctx pc,+ pubKeyAddr ctx pu+ ]+ where+ c = wrapSecKey False k :: PrivateKey -- Compressed+ u = wrapSecKey True k :: PrivateKey -- Uncompressed+ pc = derivePublicKey ctx c+ pu = derivePublicKey ctx u++buildSweepTx ::+ Network ->+ Ctx ->+ StdGen ->+ [Store.Unspent] ->+ [Address] ->+ Natural ->+ Natural ->+ Either String Tx+buildSweepTx net ctx gen coins sweepTo feeByte dust =+ flip evalStateT gen $ do+ rdmOutpoints <- ((.outpoint) <$>) <$> randomShuffle coins+ rdmSweepTo <- randomShuffle sweepTo+ let coinsTot = sum $ (.value) <$> coins+ fee = guessTxFee (fromIntegral feeByte) (length sweepTo) (length coins)+ when (coinsTot < fee) $+ throwError "Could not find a sweep solution: fee is too large"+ let (q, r) = (coinsTot - fee) `quotRem` fromIntegral (length sweepTo)+ amnts = (q + r) : repeat q+ when (q <= fromIntegral dust) $+ throwError "Outputs are smaller than the dust value"+ addrsT <- lift $ mapM (maybeToEither "Addr" . addrToText net) rdmSweepTo+ lift $ buildAddrTx net ctx rdmOutpoints (zip addrsT amnts)++-- Utilities --++randomRange :: (Random a, MonadState StdGen m) => a -> a -> m a+randomRange low hi = do+ gen <- get+ let (res, newGen) = randomR (low, hi) gen+ put newGen+ return res++randomShuffle :: (MonadState StdGen m) => [a] -> m [a]+randomShuffle [] = return []+randomShuffle [x] = return [x]+randomShuffle xs = do+ i <- randomRange 0 (length xs - 1)+ case splitAt i xs of+ (as, e : bs) -> (e :) <$> randomShuffle (as <> bs)+ _ -> error "randomShuffle"
@@ -0,0 +1,403 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++module Haskoin.Wallet.TxInfo where++import Control.Arrow ((&&&))+import Control.Monad (unless)+import Data.Aeson (object, withObject, (.:), (.:?), (.=))+import qualified Data.Aeson as Json+import Data.Aeson.Types (Parser)+import qualified Data.ByteString as BS+import Data.Either (partitionEithers)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (fromMaybe, isJust)+import qualified Data.Serialize as S+import Data.Text (Text)+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Wallet.FileIO+import Haskoin.Wallet.Util+import Numeric.Natural (Natural)++data TxType = TxDebit | TxInternal | TxCredit+ deriving (Show, Eq)++instance Json.ToJSON TxType where+ toJSON =+ Json.String . \case+ TxDebit -> "debit"+ TxInternal -> "internal"+ TxCredit -> "credit"++instance Json.FromJSON TxType where+ parseJSON =+ Json.withText "txtype" $ \case+ "debit" -> return TxDebit+ "internal" -> return TxInternal+ "credit" -> return TxCredit+ _ -> fail "Invalid TxType"++data TxInfo = TxInfo+ { txInfoHash :: !(Maybe TxHash),+ txInfoType :: !TxType,+ txInfoAmount :: !Integer,+ txInfoMyOutputs :: !(Map Address MyOutputs),+ txInfoOtherOutputs :: !(Map Address Natural),+ txInfoNonStdOutputs :: ![Store.StoreOutput],+ txInfoMyInputs :: !(Map Address MyInputs),+ txInfoOtherInputs :: !(Map Address OtherInputs),+ txInfoNonStdInputs :: ![Store.StoreInput],+ txInfoSize :: !Natural,+ txInfoFee :: !Natural,+ txInfoFeeByte :: !Natural,+ txInfoBlockRef :: !Store.BlockRef,+ txInfoConfirmations :: !Natural,+ txInfoPending :: !(Maybe TxInfoPending)+ }+ deriving (Eq, Show)++data TxInfoPending = TxInfoPending+ { pendingNosigHash :: !TxHash,+ pendingSigned :: !Bool,+ pendingOnline :: !Bool+ }+ deriving (Eq, Show)++instance Json.ToJSON TxInfoPending where+ toJSON (TxInfoPending h s o) =+ object+ [ "nosighash" .= h,+ "signed" .= s,+ "online" .= o+ ]++instance Json.FromJSON TxInfoPending where+ parseJSON =+ withObject "TxInfoPending" $ \o -> do+ h <- o .: "nosighash"+ s <- o .: "signed"+ onl <- o .: "online"+ return $ TxInfoPending h s onl++data MyOutputs = MyOutputs+ { myOutputsValue :: !Natural,+ myOutputsPath :: !SoftPath,+ myOutputsLabel :: !Text+ }+ deriving (Eq, Show)++instance Json.ToJSON MyOutputs where+ toJSON (MyOutputs i p l) =+ object+ [ "value" .= i,+ "path" .= p,+ "label" .= l+ ]++instance Json.FromJSON MyOutputs where+ parseJSON =+ withObject "MyOutputs" $ \o -> do+ i <- o .: "value"+ p <- o .: "path"+ l <- o .: "label"+ return $ MyOutputs i p l++data MyInputs = MyInputs+ { myInputsValue :: !Natural,+ myInputsPath :: !SoftPath,+ myInputsLabel :: !Text,+ myInputsSigInput :: [SigInput]+ }+ deriving (Eq, Show)++instance MarshalJSON Ctx MyInputs where+ marshalValue ctx (MyInputs i p l s) =+ object $+ [ "value" .= i,+ "path" .= p,+ "label" .= l+ ]+ ++ ["siginput" .= (marshalValue ctx <$> s) | not (null s)]++ unmarshalValue ctx =+ withObject "MyInputs" $ \o -> do+ i <- o .: "value"+ p <- o .: "path"+ l <- o .: "label"+ sM <- o .:? "siginput"+ s <- unmarshalValue ctx `mapM` fromMaybe [] sM+ return $ MyInputs i p l s++data OtherInputs = OtherInputs+ { otherInputsValue :: !Natural,+ otherInputsSigInput :: [SigInput]+ }+ deriving (Eq, Show)++instance MarshalJSON Ctx OtherInputs where+ marshalValue ctx (OtherInputs i s) =+ object $+ ("value" .= i)+ : ["siginput" .= (marshalValue ctx <$> s) | not (null s)]++ unmarshalValue ctx =+ withObject "OtherInputs" $ \o -> do+ i <- o .: "value"+ sM <- o .:? "siginput"+ s <- unmarshalValue ctx `mapM` fromMaybe [] sM+ return $ OtherInputs i s++instance MarshalJSON (Network, Ctx) TxInfo where+ marshalValue (net, ctx) tx =+ object+ [ "txid" .= txInfoHash tx,+ "type" .= txInfoType tx,+ "amount" .= txInfoAmount tx,+ "myoutputs" .= mapAddrText net (txInfoMyOutputs tx),+ "otheroutputs" .= mapAddrText net (txInfoOtherOutputs tx),+ "nonstdoutputs" .= (marshalValue net <$> txInfoNonStdOutputs tx),+ "myinputs" .= marshalMap net ctx (txInfoMyInputs tx),+ "otherinputs" .= marshalMap net ctx (txInfoOtherInputs tx),+ "nonstdinputs" .= (marshalValue net <$> txInfoNonStdInputs tx),+ "size" .= txInfoSize tx,+ "fee" .= txInfoFee tx,+ "feebyte" .= txInfoFeeByte tx,+ "block" .= txInfoBlockRef tx,+ "confirmations" .= txInfoConfirmations tx,+ "pending" .= txInfoPending tx+ ]+ unmarshalValue (net, ctx) =+ Json.withObject "TxInfo" $ \o ->+ TxInfo+ <$> o .: "txid"+ <*> o .: "type"+ <*> o .: "amount"+ <*> (mapTextAddr net <$> o .: "myoutputs")+ <*> (mapTextAddr net <$> o .: "otheroutputs")+ <*> (mapM (unmarshalValue net) =<< o .: "nonstdoutputs")+ <*> (unmarshalMap net ctx =<< o .: "myinputs")+ <*> (unmarshalMap net ctx =<< o .: "otherinputs")+ <*> (mapM (unmarshalValue net) =<< o .: "nonstdinputs")+ <*> o .: "size"+ <*> o .: "fee"+ <*> o .: "feebyte"+ <*> o .: "block"+ <*> o .: "confirmations"+ <*> o .: "pending"++marshalMap ::+ (MarshalJSON Ctx v) =>+ Network ->+ Ctx ->+ Map Address v ->+ Map Text Json.Value+marshalMap net ctx m = mapAddrText net $ Map.map (marshalValue ctx) m++unmarshalMap ::+ (MarshalJSON Ctx v) =>+ Network ->+ Ctx ->+ Map Text Json.Value ->+ Parser (Map Address v)+unmarshalMap net ctx m = mapTextAddr net <$> (unmarshalValue ctx `mapM` m)++mapAddrText :: Network -> Map Address v -> Map Text v+mapAddrText net m =+ either error id $ do+ let f (a, v) = (,v) <$> addrToTextE net a+ Map.fromList <$> mapM f (Map.assocs m)++mapTextAddr :: Network -> Map Text v -> Map Address v+mapTextAddr net m =+ either error id $ do+ let f (a, v) = (,v) <$> textToAddrE net a+ Map.fromList <$> mapM f (Map.assocs m)++storeToTxInfo ::+ Map Address SoftPath -> Natural -> Store.Transaction -> TxInfo+storeToTxInfo walletAddrs currHeight sTx =+ TxInfo+ { txInfoHash = Just sTx.txid,+ txInfoType = txType amount fee,+ txInfoAmount = amount,+ txInfoMyOutputs = Map.map (\(v, p) -> MyOutputs v p "") myOutputsMap,+ txInfoOtherOutputs = othOutputsMap,+ txInfoNonStdOutputs = nonStdOut,+ txInfoMyInputs = Map.map (\(v, p) -> MyInputs v p "" []) myInputsMap,+ txInfoOtherInputs = Map.map (`OtherInputs` []) othInputsMap,+ txInfoNonStdInputs = nonStdIn,+ txInfoSize = size,+ txInfoFee = fee,+ txInfoFeeByte = feeByte,+ txInfoBlockRef = sTx.block,+ txInfoConfirmations = fromMaybe 0 confM,+ txInfoPending = Nothing+ }+ where+ size = fromIntegral sTx.size :: Natural+ fee = fromIntegral sTx.fee :: Natural+ feeByte = fee `div` size+ (outputMap, nonStdOut) = outputAddressMap sTx.outputs+ myOutputsMap = Map.intersectionWith (,) outputMap walletAddrs+ othOutputsMap = Map.difference outputMap walletAddrs+ (inputMap, nonStdIn) = inputAddressMap sTx.inputs+ myInputsMap = Map.intersectionWith (,) inputMap walletAddrs+ othInputsMap = Map.difference inputMap walletAddrs+ myOutputsSum =+ fromIntegral $ sum $ fst <$> Map.elems myOutputsMap :: Integer+ myInputsSum = fromIntegral $ sum $ fst <$> Map.elems myInputsMap :: Integer+ amount = myOutputsSum - myInputsSum+ confM =+ case sTx.block of+ Store.MemRef _ -> Nothing+ Store.BlockRef height _ ->+ (+ 1) <$> currHeight `safeSubtract` fromIntegral height++{- Helpers for building address maps -}++outputAddressMap ::+ [Store.StoreOutput] -> (Map Address Natural, [Store.StoreOutput])+outputAddressMap outs =+ (Map.fromListWith (+) rs, ls)+ where+ (ls, rs) = partitionEithers $ f <$> outs+ f (Store.StoreOutput v _ _ (Just a)) = Right (a, fromIntegral v)+ f s = Left s++inputAddressMap ::+ [Store.StoreInput] -> (Map Address Natural, [Store.StoreInput])+inputAddressMap ins =+ (Map.fromListWith (+) rs, ls)+ where+ (ls, rs) = partitionEithers $ f <$> ins+ f (Store.StoreInput _ _ _ _ v _ (Just a)) = Right (a, fromIntegral v)+ f s = Left s++txOutAddressMap :: Ctx -> [TxOut] -> (Map Address Natural, [TxOut])+txOutAddressMap ctx outs =+ (Map.fromListWith (+) rs, ls)+ where+ (ls, rs) = partitionEithers $ f <$> outs+ f to@(TxOut v s) =+ case scriptToAddressBS ctx s of+ Left _ -> Left to+ Right a -> Right (a, fromIntegral v)++txInAddressMap ::+ Network ->+ Ctx ->+ [(OutPoint, TxOut)] ->+ (Map Address (Natural, [SigInput]), [(OutPoint, TxOut)])+txInAddressMap net ctx ins =+ (Map.fromListWith (\(a, b) (c, d) -> (a + c, b <> d)) rs, mconcat ls)+ where+ sh = maybeSetForkId net sigHashAll+ (ls, rs) = partitionEithers $ f <$> ins+ f (op, to@(TxOut v s)) =+ case scriptToAddressBS ctx s of+ Left _ -> Left [(op, to)]+ Right a ->+ Right+ ( a,+ ( fromIntegral v,+ [SigInput (addressToOutput a) v op sh Nothing]+ )+ )++maybeSetForkId :: Network -> SigHash -> SigHash+maybeSetForkId net+ | isJust net.sigHashForkId = setForkIdFlag+ | otherwise = id++parseTxSignData ::+ Network ->+ Ctx ->+ XPubKey ->+ TxSignData ->+ Either String TxInfo+parseTxSignData net ctx pubkey tsd@(TxSignData tx _ inPaths outPaths signed) = do+ coins <- txSignDataCoins tsd+ -- Fees+ let outSum = fromIntegral $ sum $ (.value) <$> tx.outputs :: Natural+ inSum = fromIntegral $ sum $ (.value) . snd <$> coins :: Natural+ fee <- maybeToEither "Fee is negative" $ inSum `safeSubtract` outSum+ let feeByte = fee `div` fromIntegral size+ -- Outputs+ (outputMap, nonStdOut) = txOutAddressMap ctx tx.outputs+ myOutputsMap = Map.intersectionWith (,) outputMap outPathAddrs+ othOutputsMap = Map.difference outputMap outPathAddrs+ -- Inputs+ (inputMap, nonStdIn) = txInAddressMap net ctx coins+ myInputsMap =+ Map.intersectionWith (\(n, xs) p -> (n, p, xs)) inputMap inPathAddrs+ othInputsMap = Map.difference inputMap inPathAddrs+ -- Amounts+ myOutputsSum =+ fromIntegral $ sum $ fst <$> Map.elems myOutputsMap :: Integer+ myInputsSum =+ fromIntegral $ sum $ fst3 <$> Map.elems myInputsMap :: Integer+ amount = myOutputsSum - myInputsSum+ -- Sanity checks+ unless (null nonStdOut) $+ Left "There are non-standard outputs in the transaction"+ unless (null nonStdIn) $+ Left "There are non-standard inputs in the transaction"+ unless (length coins == length tx.inputs) $+ Left "Referenced input transactions are missing"+ unless (length inPaths == Map.size myInputsMap) $+ Left "Input derivations don't match the transaction inputs"+ unless (length outPaths == Map.size myOutputsMap) $+ Left "Output derivations don't match the transaction outputs"+ return $+ TxInfo+ { txInfoHash = if signed then Just $ txHash tx else Nothing,+ txInfoType = txType amount fee,+ txInfoAmount = amount,+ txInfoMyOutputs =+ Map.map (\(v, p) -> MyOutputs v p "") myOutputsMap,+ txInfoOtherOutputs = othOutputsMap,+ txInfoNonStdOutputs = [],+ txInfoMyInputs =+ Map.map (\(i, p, s) -> MyInputs i p "" s) myInputsMap,+ txInfoOtherInputs = Map.map (uncurry OtherInputs) othInputsMap,+ txInfoNonStdInputs = [],+ txInfoSize = fromIntegral size,+ txInfoFee = fee,+ txInfoFeeByte = feeByte,+ txInfoBlockRef = Store.MemRef 0,+ txInfoConfirmations = 0,+ txInfoPending = Just $ TxInfoPending (nosigTxHash tx) signed False+ }+ where+ inPathAddrs = Map.fromList $ (pathToAddr ctx pubkey &&& id) <$> inPaths+ outPathAddrs = Map.fromList $ (pathToAddr ctx pubkey &&& id) <$> outPaths+ size+ | signed = BS.length $ S.encode tx+ | otherwise = guessTxSize (length tx.inputs) [] (length tx.outputs) 0++txSignDataCoins :: TxSignData -> Either String [(OutPoint, TxOut)]+txSignDataCoins (TxSignData tx depTxs _ _ _) =+ maybeToEither "Referenced input transactions are missing" $ mapM f ops+ where+ ops = (.outpoint) <$> tx.inputs :: [OutPoint]+ txMap =+ Map.fromList $ (txHash &&& (.outputs)) <$> depTxs :: Map TxHash [TxOut]+ f :: OutPoint -> Maybe (OutPoint, TxOut)+ f op@(OutPoint h i) =+ (op,) <$> ((!!? fromIntegral i) =<< Map.lookup h txMap)++txType :: Integer -> Natural -> TxType+txType amount fee+ | amount > 0 = TxCredit+ | abs amount == fromIntegral fee = TxInternal+ | otherwise = TxDebit++pathToAddr :: Ctx -> XPubKey -> SoftPath -> Address+pathToAddr ctx pk path = xPubAddr ctx $ derivePubPath ctx path pk
@@ -0,0 +1,117 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}++module Haskoin.Wallet.Util where++import Control.Arrow (second)+import Control.Monad.Except+import qualified Data.Aeson as JSON+import qualified Data.Aeson.Encode.Pretty as Pretty+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import Data.List (sortBy)+import Data.Maybe (fromMaybe)+import qualified Data.Serialize as S+import Data.Text (Text)+import qualified Data.Text as Text+import Haskoin+import qualified Haskoin.Store.Data as Store+import Numeric.Natural (Natural)++{- Data.Aeson Compatibility -}++encodeJsonPretty :: (JSON.ToJSON a) => a -> BS.ByteString+encodeJsonPretty =+ BL.toStrict+ . Pretty.encodePretty' Pretty.defConfig {Pretty.confIndent = Pretty.Spaces 2}++encodeJsonPrettyLn :: (JSON.ToJSON a) => a -> BS.ByteString+encodeJsonPrettyLn =+ BL.toStrict+ . Pretty.encodePretty'+ Pretty.defConfig+ { Pretty.confIndent = Pretty.Spaces 2,+ Pretty.confTrailingNewline = True+ }++{- Haskoin helper functions -}++data Page = Page+ { pageLimit :: !Natural,+ pageOffset :: !Natural+ }+ deriving (Eq, Show)++toPage :: Page -> [a] -> [a]+toPage (Page limit offset) xs =+ take (fromIntegral limit) $ drop (fromIntegral offset) xs++liftExcept :: (MonadError String m) => ExceptT Store.Except m a -> m a+liftExcept action = do+ e <- runExceptT action+ case e of+ Right a -> return a+ Left err -> throwError $ show (err :: Store.Except)++addrToTextE :: Network -> Address -> Either String Text+addrToTextE net a =+ maybeToEither "Invalid Address in addrToTextE" (addrToText net a)++textToAddrE :: Network -> Text -> Either String Address+textToAddrE net a =+ maybeToEither "Invalid Address in textToAddrE" (textToAddr net a)++addrToText2 :: Network -> (Address, v) -> Either String (Text, v)+addrToText2 net (a, v) = (,v) <$> addrToTextE net a++textToAddr2 :: Network -> (Text, v) -> Either String (Address, v)+textToAddr2 net (a, v) = (,v) <$> textToAddrE net a++addrToText3 :: Network -> (Address, v, w) -> Either String (Text, v, w)+addrToText3 net (a, v, w) = (,v,w) <$> addrToTextE net a++textToAddr3 :: Network -> (Text, v, w) -> Either String (Address, v, w)+textToAddr3 net (a, v, w) = (,v,w) <$> textToAddrE net a++lastList :: Natural -> [a] -> [a]+lastList count xs = drop (max 0 $ length xs - fromIntegral count) xs++xPubChecksum :: Ctx -> XPubKey -> Text+xPubChecksum ctx = encodeHex . S.encode . xPubFP ctx++(</>) :: String -> String -> String+a </> b = a <> "/" <> b++(!!?) :: [a] -> Natural -> Maybe a+xs !!? i+ | fromIntegral i < length xs = Just $ xs !! fromIntegral i+ | otherwise = Nothing++chunksOf :: Natural -> [a] -> [[a]]+chunksOf n xs+ | null xs = []+ | otherwise =+ uncurry (:) $ second (chunksOf n) $ splitAt (fromIntegral n) xs++sortDesc :: (Ord a) => [a] -> [a]+sortDesc = sortBy (flip compare)++padStart :: Int -> Text -> Text -> Text+padStart n c t =+ Text.replicate (fromMaybe 0 $ n `safeSubtract` Text.length t) c <> t++padEnd :: Int -> Text -> Text -> Text+padEnd n c t =+ t <> Text.replicate (fromMaybe 0 $ n `safeSubtract` Text.length t) c++dropPatternEnd :: Text -> Text -> Text+dropPatternEnd p t = fromMaybe t $ Text.stripSuffix p t++chunksOfEnd :: Int -> Text -> [Text]+chunksOfEnd n t = reverse $ Text.reverse <$> Text.chunksOf n (Text.reverse t)++safeSubtract :: (Integral a) => a -> a -> Maybe a+safeSubtract a b+ | b > a = Nothing+ | otherwise = Just $ a - b
@@ -0,0 +1,68 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Haskoin.Wallet.AmountsSpec where++import Haskoin.Wallet.Amounts+import Haskoin.Wallet.TestUtils (genNatural)+import Test.Hspec (Spec, describe, it, shouldBe)+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (Arbitrary (arbitrary), elements, forAll)++spec :: Spec+spec =+ describe "Amount parser" $ do+ prop "readAmount . showAmount identity" $+ forAll genNatural $ \n unit ->+ readAmount unit (showAmount unit n) `shouldBe` Just n+ prop "readIntegerAmount . showIntegerAmount identity" $ \i unit ->+ readIntegerAmount unit (showIntegerAmount unit i) `shouldBe` Just i+ it "Satoshi amounts vectors" $ do+ readAmount UnitSatoshi "0" `shouldBe` Just 0+ readAmount UnitSatoshi "0000" `shouldBe` Just 0+ readAmount UnitSatoshi "0.0" `shouldBe` Nothing+ readAmount UnitSatoshi "8" `shouldBe` Just 8+ readAmount UnitSatoshi "100" `shouldBe` Just 100+ readAmount UnitSatoshi "1234567890" `shouldBe` Just 1234567890+ readAmount UnitSatoshi "1'234'567'890" `shouldBe` Just 1234567890+ readAmount UnitSatoshi "1 234 567 890" `shouldBe` Just 1234567890+ readAmount UnitSatoshi "1_234_567_890" `shouldBe` Just 1234567890+ it "Bit amounts vectors" $ do+ readAmount UnitBit "0" `shouldBe` Just 0+ readAmount UnitBit "0000" `shouldBe` Just 0+ readAmount UnitBit "0.0" `shouldBe` Just 0+ readAmount UnitBit "0.00" `shouldBe` Just 0+ readAmount UnitBit "0.000" `shouldBe` Nothing+ readAmount UnitBit "0.10" `shouldBe` Just 10+ readAmount UnitBit "0.1" `shouldBe` Just 10+ readAmount UnitBit "0.01" `shouldBe` Just 1+ readAmount UnitBit "1" `shouldBe` Just 100+ readAmount UnitBit "100" `shouldBe` Just 10000+ readAmount UnitBit "100.00" `shouldBe` Just 10000+ readAmount UnitBit "100.01" `shouldBe` Just 10001+ readAmount UnitBit "1234567890.9" `shouldBe` Just 123456789090+ readAmount UnitBit "1'234'567'890.90" `shouldBe` Just 123456789090+ readAmount UnitBit "1 234 567 890.90" `shouldBe` Just 123456789090+ readAmount UnitBit "1_234_567_890.90" `shouldBe` Just 123456789090+ it "Bitcoin amounts vectors" $ do+ readAmount UnitBitcoin "0" `shouldBe` Just 0+ readAmount UnitBitcoin "0000" `shouldBe` Just 0+ readAmount UnitBitcoin "0.0" `shouldBe` Just 0+ readAmount UnitBitcoin "0.00000000" `shouldBe` Just 0+ readAmount UnitBitcoin "0.000000000" `shouldBe` Nothing+ readAmount UnitBitcoin "0.1" `shouldBe` Just 10000000+ readAmount UnitBitcoin "0.1000" `shouldBe` Just 10000000+ readAmount UnitBitcoin "0.10000000" `shouldBe` Just 10000000+ readAmount UnitBitcoin "0.100000000" `shouldBe` Nothing+ readAmount UnitBitcoin "1" `shouldBe` Just 100000000+ readAmount UnitBitcoin "100" `shouldBe` Just 10000000000+ readAmount UnitBitcoin "1234567890.9" `shouldBe` Just 123456789090000000+ readAmount UnitBitcoin "1'234'567'890.9009"+ `shouldBe` Just 123456789090090000+ readAmount UnitBitcoin "1 234 567 890.9009"+ `shouldBe` Just 123456789090090000+ readAmount UnitBitcoin "1_234_567_890.9009"+ `shouldBe` Just 123456789090090000++instance Arbitrary AmountUnit where+ arbitrary = elements [UnitBitcoin, UnitBit, UnitSatoshi]
@@ -0,0 +1,811 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# OPTIONS_GHC -Wno-ambiguous-fields -fno-warn-orphans #-}++module Haskoin.Wallet.CommandsSpec where++import Conduit (MonadIO, liftIO)+import Control.Arrow (second)+import Control.Monad+import Control.Monad.Except+import Control.Monad.Trans (lift)+import Data.Default (def)+import Data.Either+import qualified Data.Map as Map+import Data.Maybe+import qualified Data.Serialize as S+import Data.Text (Text)+import Database.Esqueleto.Legacy hiding (isNothing)+import qualified Database.Persist as P+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Util.Arbitrary+import Haskoin.Wallet.Config+import Haskoin.Wallet.Database+import Haskoin.Wallet.FileIO+import Haskoin.Wallet.Signing+import Haskoin.Wallet.SigningSpec+import Haskoin.Wallet.TestUtils+import Haskoin.Wallet.TxInfo+import Haskoin.Wallet.Util+import Test.Hspec++identityTests :: Ctx -> IdentityTests+identityTests ctx =+ def+ { jsonTests =+ [ JsonBox $ arbitraryDBAccount btc ctx,+ JsonBox $ arbitraryDBAddress btc,+ JsonBox arbitraryAddressBalance,+ JsonBox $ arbitraryTxSignData btc ctx,+ JsonBox arbitraryConfig+ ],+ marshalJsonTests =+ [ MarshalJsonBox ((btc,) <$> arbitraryJsonCoin),+ MarshalJsonBox ((ctx,) <$> arbitraryPubKeyDoc ctx),+ MarshalJsonBox (((btc, ctx),) <$> arbitraryTxInfo btc ctx),+ MarshalJsonBox ((ctx,) <$> arbitraryResponse btc ctx),+ MarshalJsonBox ((ctx,) <$> arbitraryAccountBackup ctx)+ ]+ }++spec :: Spec+spec = do+ let cfg = def :: Config+ prepareContext $ \ctx -> do+ testIdentity $ identityTests ctx+ describe "Database" $ do+ bestSpec+ accountSpec ctx+ extAddressSpec ctx cfg+ intAddressSpec ctx cfg+ txsSpec ctx+ coinSpec ctx+ pendingTxsSpec ctx cfg++liftTest :: (MonadIO m) => Expectation -> m ()+liftTest = liftIO++shouldBeLeft :: (Show a) => ExceptT String (DB IO) a -> DB IO ()+shouldBeLeft action = liftTest . (`shouldSatisfy` isLeft) =<< runExceptT action++shouldBeLeft' ::+ (Show a, Eq a) => String -> ExceptT String (DB IO) a -> DB IO ()+shouldBeLeft' err action =+ liftTest . (`shouldBe` Left err) =<< runExceptT action++dbShouldBe :: (Show a, Eq a) => DB IO a -> a -> DB IO ()+dbShouldBe action a = liftTest . (`shouldBe` a) =<< action++dbShouldBeE ::+ (Show a, Eq a) =>+ ExceptT String (DB IO) a ->+ a ->+ ExceptT String (DB IO) ()+dbShouldBeE action a = liftTest . (`shouldBe` a) =<< action++dbShouldSatisfy :: (Show a) => DB IO a -> (a -> Bool) -> DB IO ()+dbShouldSatisfy action f = liftTest . (`shouldSatisfy` f) =<< action++testNewAcc :: Ctx -> Text -> ExceptT String (DB IO) (DBAccountId, DBAccount)+testNewAcc ctx name = do+ let fp = forceRight $ walletFingerprint btc ctx mnemPass+ d <- lift $ nextAccountDeriv fp btc+ let prv = forceRight $ signingKey btc ctx mnemPass d+ pub = deriveXPubKey ctx prv+ insertAccount btc ctx fp name pub++testNewAcc2 :: Ctx -> Text -> ExceptT String (DB IO) (DBAccountId, DBAccount)+testNewAcc2 ctx name = do+ let fp = forceRight $ walletFingerprint btc ctx mnemPass2+ d <- lift $ nextAccountDeriv fp btc+ let prv = forceRight $ signingKey btc ctx mnemPass2 d+ pub = deriveXPubKey ctx prv+ insertAccount btc ctx fp name pub++bestSpec :: Spec+bestSpec =+ it "can insert and get the best block" $+ runDBMemory $ do+ res1 <- getBest btcTest+ liftTest $ res1 `shouldBe` Nothing+ updateBest+ btc+ "00000000000000000004727b3cc0946dc2054f59e362369e0437325c0a992efb"+ 814037+ updateBest+ btcTest+ "000000000000001400d91785c92efc15ccfc1949e8581f06db7914a707c3f81d"+ 2535443+ res2 <- getBest btc+ res3 <- getBest btcTest+ liftTest $ do+ res2+ `shouldBe` Just+ ( "00000000000000000004727b3cc0946dc2054f59e362369e0437325c0a992efb",+ 814037+ )+ res3+ `shouldBe` Just+ ( "000000000000001400d91785c92efc15ccfc1949e8581f06db7914a707c3f81d",+ 2535443+ )++accountSpec :: Ctx -> Spec+accountSpec ctx = do+ it "can find the correct account derivations with smallestUnused" $ do+ smallestUnused [] `shouldBe` 0+ smallestUnused [0] `shouldBe` 1+ smallestUnused [1] `shouldBe` 0+ smallestUnused [2] `shouldBe` 0+ smallestUnused [1,2,3] `shouldBe` 0+ smallestUnused [1,3,4] `shouldBe` 0+ smallestUnused [0,2] `shouldBe` 1+ smallestUnused [0,1,3,4] `shouldBe` 2+ smallestUnused [0,1,3,4,6,7] `shouldBe` 2+ smallestUnused [0,1,2,3,4,5] `shouldBe` 6+ it "can create and rename accounts" $ do+ runDBMemoryE $ do+ -- No accounts+ lift $ shouldBeLeft $ getAccountByName $ Just "acc1"+ lift $ shouldBeLeft $ getAccountByName Nothing+ lift $ nextAccountDeriv walletFP btc `dbShouldBe` 0+ lift $ nextAccountDeriv walletFP btc `dbShouldBe` 0 -- Still 0+ lift $ getAccounts `dbShouldBe` []+ -- Check basic account properties+ (accId, acc) <- testNewAcc ctx "acc1"+ lift $ nextAccountDeriv walletFP btc `dbShouldBe` 1+ lift $ nextAccountDeriv walletFP btc `dbShouldBe` 1 -- Still 1+ liftTest $ do+ dBAccountName acc `shouldBe` "acc1"+ dBAccountWallet acc `shouldBe` DBWalletKey walletFPText+ dBAccountIndex acc `shouldBe` 0+ dBAccountNetwork acc `shouldBe` "btc"+ dBAccountDerivation acc `shouldBe` "/44'/0'/0'"+ dBAccountExternal acc `shouldBe` 0+ dBAccountInternal acc `shouldBe` 0+ dBAccountXPubKey acc `shouldBe` snd keysT+ dBAccountBalanceConfirmed acc `shouldBe` 0+ dBAccountBalanceUnconfirmed acc `shouldBe` 0+ dBAccountBalanceCoins acc `shouldBe` 0+ -- Check account retrieval+ lift $ getAccounts `dbShouldBe` [(accId, acc)]+ getAccountByName (Just "acc1") `dbShouldBeE` (accId, acc)+ getAccountByName Nothing `dbShouldBeE` (accId, acc) -- There is just 1 account+ getAccountById accId `dbShouldBeE` acc+ lift $ getAccounts `dbShouldBe` [(accId, acc)]+ -- Check the creation of a second account+ (accId2, acc2) <- testNewAcc ctx "acc2"+ lift $ nextAccountDeriv walletFP btc `dbShouldBe` 2+ getAccountByName (Just "acc2") `dbShouldBeE` (accId2, acc2)+ lift $ getAccounts `dbShouldBe` [(accId, acc), (accId2, acc2)]+ lift $ shouldBeLeft $ getAccountByName Nothing -- There are > 1 accounts+ liftTest $ do+ dBAccountWallet acc2 `shouldBe` DBWalletKey walletFPText+ dBAccountDerivation acc2 `shouldBe` "/44'/0'/1'"+ dBAccountIndex acc2 `shouldBe` 1+ -- Create an account in a new wallet+ (accId3, acc3) <- testNewAcc2 ctx "acc3"+ liftTest $ do+ dBAccountName acc3 `shouldBe` "acc3"+ dBAccountWallet acc3 `shouldBe` DBWalletKey walletFPText2+ dBAccountIndex acc3 `shouldBe` 0+ dBAccountNetwork acc3 `shouldBe` "btc"+ dBAccountDerivation acc3 `shouldBe` "/44'/0'/0'"+ dBAccountXPubKey acc3 `shouldBe` snd keysT2+ lift $ nextAccountDeriv walletFP2 btc `dbShouldBe` 1+ lift $+ getAccounts `dbShouldBe` [(accId3, acc3), (accId, acc), (accId2, acc2)]+ getAccountByName (Just "acc3") `dbShouldBeE` (accId3, acc3)+ lift $ shouldBeLeft $ getAccountByName Nothing -- There are > 1 accounts+ getAccountById accId3 `dbShouldBeE` acc3+ -- Rename an account+ lift $ shouldBeLeft $ renameAccount "acc2" "acc2"+ lift $ shouldBeLeft $ renameAccount "acc2" "acc3"+ lift $ shouldBeLeft $ renameAccount "doesnotexist" "hello world"+ acc2' <- renameAccount "acc2" "hello world"+ liftIO $ acc2' `shouldBe` acc2 {dBAccountName = "hello world"}+ lift $ getAccountNames `dbShouldBe` ["acc1", "hello world", "acc3"]+ _ <- renameAccount "hello world" "acc2"+ lift $ getAccountNames `dbShouldBe` ["acc1", "acc2", "acc3"]++extAddressSpec :: Ctx -> Config -> Spec+extAddressSpec ctx cfg =+ it "can generate external addresses" $ do+ runDBMemoryE $ do+ (accId, _) <- testNewAcc ctx "test"+ -- No addresses yet+ lift $ bestAddrWithFunds accId AddrInternal `dbShouldBe` Nothing+ lift $ bestAddrWithFunds accId AddrExternal `dbShouldBe` Nothing+ lift $ addressPage accId (Page 5 0) `dbShouldBe` []+ -- Generate external addresses+ ext1 <- genExtAddress ctx cfg accId "Address 1"+ liftTest $ do+ dBAddressIndex ext1 `shouldBe` 0+ dBAddressAccountWallet ext1 `shouldBe` DBWalletKey walletFPText+ dBAddressAccountDerivation ext1 `shouldBe` "/44'/0'/0'"+ dBAddressDerivation ext1 `shouldBe` "/0/0"+ dBAddressAddress ext1 `shouldBe` head extAddrsT+ dBAddressLabel ext1 `shouldBe` "Address 1"+ dBAddressInternal ext1 `shouldBe` False+ dBAddressFree ext1 `shouldBe` False+ lift $ addressPage accId (Page 5 0) `dbShouldBe` [ext1]+ ext2 <- genExtAddress ctx cfg accId "Address 2"+ liftTest $ do+ dBAddressIndex ext2 `shouldBe` 1+ dBAddressAccountWallet ext2 `shouldBe` DBWalletKey walletFPText+ dBAddressAccountDerivation ext2 `shouldBe` "/44'/0'/0'"+ dBAddressDerivation ext2 `shouldBe` "/0/1"+ dBAddressAddress ext2 `shouldBe` extAddrsT !! 1+ dBAddressLabel ext2 `shouldBe` "Address 2"+ dBAddressInternal ext2 `shouldBe` False+ dBAddressFree ext2 `shouldBe` False+ -- Test Paging+ lift $ addressPage accId (Page 5 0) `dbShouldBe` [ext2, ext1]+ lift $ addressPage accId (Page 1 0) `dbShouldBe` [ext2]+ lift $ addressPage accId (Page 1 1) `dbShouldBe` [ext1]+ lift $ addressPage accId (Page 1 2) `dbShouldBe` []+ -- Set address labels+ _ <- setAddrLabel accId 0 "test address"+ lift $+ addressPage accId (Page 5 0)+ `dbShouldBe` [ext2, ext1 {dBAddressLabel = "test address"}]+ -- Test the gap+ replicateM_ 18 $ genExtAddress ctx cfg accId "" -- We have 20 addresses+ lift $ shouldBeLeft $ genExtAddress ctx cfg accId "" -- fail gap+ lift $ addressPage accId (Page 100 0) `dbShouldSatisfy` ((== 20) . length)+ updateAddressBalances btc [Store.Balance (extAddrs !! 2) 0 0 0 1 1]+ updateAddressBalances btc [Store.Balance (extAddrs !! 4) 0 0 0 1 1]+ lift $ bestAddrWithFunds accId AddrExternal `dbShouldBe` Just 4+ replicateM_ 5 $ genExtAddress ctx cfg accId "" -- We have 25 addresses+ lift $ shouldBeLeft $ genExtAddress ctx cfg accId "" -- fail gap+ lift $ addressPage accId (Page 100 0) `dbShouldSatisfy` ((== 25) . length)+ -- Test discoverAccGenAddrs+ updateAddressBalances btc [Store.Balance (extAddrs !! 9) 0 0 0 1 1]+ discoverAccGenAddrs ctx cfg accId AddrExternal 0+ lift $ addressPage accId (Page 100 0) `dbShouldSatisfy` ((== 25) . length)+ discoverAccGenAddrs ctx cfg accId AddrExternal 25+ lift $ addressPage accId (Page 100 0) `dbShouldSatisfy` ((== 25) . length)+ discoverAccGenAddrs ctx cfg accId AddrExternal 26+ lift $ addressPage accId (Page 100 0) `dbShouldSatisfy` ((== 26) . length)+ -- Test getAddrDeriv+ lift $+ getAddrDeriv btc accId (head extAddrs)+ `dbShouldBe` Right (Deriv :/ 0 :/ 0)+ lift $+ getAddrDeriv btc accId (extAddrs !! 7)+ `dbShouldBe` Right (Deriv :/ 0 :/ 7)+ -- Test accound balances+ acc1 <- lift $ updateAccountBalances accId+ liftTest $ do+ dBAccountBalanceConfirmed acc1 `shouldBe` 0+ dBAccountBalanceUnconfirmed acc1 `shouldBe` 0+ dBAccountBalanceCoins acc1 `shouldBe` 0+ updateAddressBalances btc [Store.Balance (extAddrs !! 1) 2 0 1 1 2]+ updateAddressBalances btc [Store.Balance (extAddrs !! 4) 2 8 2 2 10]+ _ <- nextFreeIntAddr ctx cfg accId+ updateAddressBalances btc [Store.Balance (head intAddrs) 3 2 1 1 5]+ acc1' <- lift $ updateAccountBalances accId+ liftTest $ do+ dBAccountBalanceConfirmed acc1' `shouldBe` 7+ dBAccountBalanceUnconfirmed acc1' `shouldBe` 10+ dBAccountBalanceCoins acc1' `shouldBe` 4+ -- Test account indices+ liftTest $ do+ dBAccountInternal acc1' `shouldBe` 1+ dBAccountExternal acc1' `shouldBe` 26++intAddressSpec :: Ctx -> Config -> Spec+intAddressSpec ctx cfg =+ it "can generate internal addresses" $ do+ runDBMemoryE $ do+ (accId, _) <- testNewAcc ctx "test"+ -- Generate internal addresses+ int1 <- nextFreeIntAddr ctx cfg accId+ liftTest $ do+ dBAddressIndex int1 `shouldBe` 0+ dBAddressAccountWallet int1 `shouldBe` DBWalletKey walletFPText+ dBAddressAccountDerivation int1 `shouldBe` "/44'/0'/0'"+ dBAddressDerivation int1 `shouldBe` "/1/0"+ dBAddressAddress int1 `shouldBe` head intAddrsT+ dBAddressLabel int1 `shouldBe` "Internal Address"+ dBAddressInternal int1 `shouldBe` True+ dBAddressFree int1 `shouldBe` True+ -- Test the Free status+ nextFreeIntAddr ctx cfg accId `dbShouldBeE` int1 -- Should return the same address+ lift $ setAddrsFree AddrBusy [head intAddrsT] `dbShouldBe` 1+ lift $ setAddrsFree AddrBusy [intAddrsT !! 1] `dbShouldBe` 0+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 1+ _ <- lift $ setAddrsFree AddrFree [head intAddrsT]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 0+ _ <- lift $ setAddrsFree AddrBusy [intAddrsT !! 1]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 0+ _ <- lift $ setAddrsFree AddrBusy [head intAddrsT]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 2+ _ <- lift $ setAddrsFree AddrBusy [intAddrsT !! 2]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 3+ _ <- lift $ setAddrsFree AddrBusy [intAddrsT !! 3]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 4+ _ <- lift $ setAddrsFree AddrBusy [intAddrsT !! 4]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 5+ _ <- lift $ setAddrsFree AddrFree [intAddrsT !! 2]+ _ <- lift $ setAddrsFree AddrFree [intAddrsT !! 4]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 2+ _ <- lift $ setAddrsFree AddrBusy [intAddrsT !! 2]+ _ <- lift $ setAddrsFree AddrBusy [intAddrsT !! 5]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 4+ _ <- lift $ setAddrsFree AddrBusy [intAddrsT !! 4]+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 6+ -- Test account indices+ acc <- getAccountById accId+ liftTest $ do+ dBAccountInternal acc `shouldBe` 7+ dBAccountExternal acc `shouldBe` 0++emptyTxInfo :: TxInfo+emptyTxInfo =+ TxInfo+ (Just $ txid' 0)+ TxInternal+ 0+ Map.empty+ Map.empty+ []+ Map.empty+ Map.empty+ []+ 0+ 0+ 0+ (Store.MemRef 0)+ 0+ Nothing++txsSpec :: Ctx -> Spec+txsSpec ctx =+ it "can manage transactions" $ do+ runDBMemoryE $ do+ (accId, _) <- testNewAcc ctx "test"+ -- Simple insert and retrieval+ (dbInfo, change) <- repsertTxInfo btc ctx accId emptyTxInfo+ liftTest $ do+ change `shouldBe` True+ dBTxInfoAccountWallet dbInfo `shouldBe` DBWalletKey walletFPText+ dBTxInfoAccountDerivation dbInfo `shouldBe` "/44'/0'/0'"+ dBTxInfoBlockRef dbInfo `shouldBe` S.encode (Store.MemRef 0)+ dBTxInfoConfirmed dbInfo `shouldBe` False+ -- Reinserting should produce no change+ (dbInfo2, change2) <- repsertTxInfo btc ctx accId emptyTxInfo+ liftTest $ do+ change2 `shouldBe` False+ dbInfo2 `shouldBe` dbInfo+ txsPage ctx accId (Page 5 0) `dbShouldBeE` [emptyTxInfo]+ -- Check that confirmations are updated correctly+ lift $ updateBest btc (bid' 0) 0+ getConfirmedTxs accId True `dbShouldBeE` []+ getConfirmedTxs accId False `dbShouldBeE` [txid' 0]+ txsPage ctx accId (Page 5 0) `dbShouldBeE` [emptyTxInfo]+ (dbInfo', change') <-+ repsertTxInfo+ btc+ ctx+ accId+ emptyTxInfo {txInfoBlockRef = Store.BlockRef 0 0}+ liftTest $ do+ change' `shouldBe` True+ dBTxInfoBlockRef dbInfo' `shouldBe` S.encode (Store.BlockRef 0 0)+ dBTxInfoConfirmed dbInfo' `shouldBe` True+ dBTxInfoCreated dbInfo' `shouldBe` dBTxInfoCreated dbInfo+ txsPage ctx accId (Page 5 0)+ `dbShouldBeE` [ emptyTxInfo+ { txInfoBlockRef = Store.BlockRef 0 0,+ txInfoConfirmations = 1+ }+ ]+ getConfirmedTxs accId True `dbShouldBeE` [txid' 0]+ getConfirmedTxs accId False `dbShouldBeE` []+ lift $ updateBest btcTest (bid' 0) 10+ lift $ updateBest btc (bid' 0) 20+ txsPage ctx accId (Page 5 0)+ `dbShouldBeE` [ emptyTxInfo+ { txInfoBlockRef = Store.BlockRef 0 0,+ txInfoConfirmations = 21+ }+ ]+ (dbInfo'', change'') <-+ repsertTxInfo+ btc+ ctx+ accId+ emptyTxInfo {txInfoBlockRef = Store.BlockRef 10 0}+ liftTest $ do+ change'' `shouldBe` True+ dBTxInfoBlockRef dbInfo'' `shouldBe` S.encode (Store.BlockRef 10 0)+ dBTxInfoConfirmed dbInfo'' `shouldBe` True+ txsPage ctx accId (Page 5 0)+ `dbShouldBeE` [ emptyTxInfo+ { txInfoBlockRef = Store.BlockRef 10 0,+ txInfoConfirmations = 11+ }+ ]++coinSpec :: Ctx -> Spec+coinSpec ctx =+ it "can manage coins" $ do+ runDBMemoryE $ do+ (accId, _) <- testNewAcc ctx "test"+ -- Insert a single coin+ let coin1 = coin' ctx (txid' 0, 0) Nothing (head extAddrs) 10+ (c, dbCoin) <- second head <$> refreshCoins btc accId extAddrs [coin1]+ liftTest $ do+ c `shouldBe` 1+ dBCoinAccountWallet dbCoin `shouldBe` DBWalletKey walletFPText+ dBCoinAccountDerivation dbCoin `shouldBe` "/44'/0'/0'"+ dBCoinAddress dbCoin `shouldBe` head extAddrsT+ dBCoinConfirmed dbCoin `shouldBe` False+ dBCoinLocked dbCoin `shouldBe` False+ -- Test JSON coins+ let jsonCoin1 = forceRight $ toJsonCoin btc Nothing dbCoin+ liftTest $+ jsonCoin1+ `shouldBe` JsonCoin+ { jsonCoinOutpoint = OutPoint (txid' 0) 0,+ jsonCoinAddress = head extAddrs,+ jsonCoinValue = 10,+ jsonCoinBlock = Store.MemRef 0,+ jsonCoinConfirmations = 0,+ jsonCoinLocked = False+ }+ coinPage btc accId (Page 5 0) `dbShouldBeE` [jsonCoin1]+ getSpendableCoins btc accId 0 `dbShouldBeE` [coin1]+ getSpendableCoins btc accId 1 `dbShouldBeE` []+ -- Nothing should happen when refreshing the same data+ refreshCoins btc accId extAddrs [coin1] `dbShouldBeE` (0, [])+ -- Confirm and update the coin+ let coin1' = coin1 {Store.block = Store.BlockRef 1 0} :: Store.Unspent+ refreshCoins btc accId extAddrs [coin1'] `dbShouldBeE` (1, [])+ getSpendableCoins btc accId 0 `dbShouldBeE` [coin1']+ getSpendableCoins btc accId 1 `dbShouldBeE` [coin1']+ getSpendableCoins btc accId 2 `dbShouldBeE` []+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin1+ { jsonCoinBlock = Store.BlockRef 1 0,+ jsonCoinConfirmations = 1+ }+ ]+ -- Best = 0+ lift $ updateBest btc (bid' 0) 0+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin1+ { jsonCoinBlock = Store.BlockRef 1 0,+ jsonCoinConfirmations = 1+ }+ ]+ getSpendableCoins btc accId 0 `dbShouldBeE` [coin1']+ getSpendableCoins btc accId 1 `dbShouldBeE` [coin1']+ getSpendableCoins btc accId 2 `dbShouldBeE` []+ -- Best = 1+ lift $ updateBest btc (bid' 0) 1+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin1+ { jsonCoinBlock = Store.BlockRef 1 0,+ jsonCoinConfirmations = 1+ }+ ]+ getSpendableCoins btc accId 0 `dbShouldBeE` [coin1']+ getSpendableCoins btc accId 1 `dbShouldBeE` [coin1']+ getSpendableCoins btc accId 2 `dbShouldBeE` []+ -- Best = 10+ lift $ updateBest btc (bid' 0) 10+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin1+ { jsonCoinBlock = Store.BlockRef 1 0,+ jsonCoinConfirmations = 10+ }+ ]+ getSpendableCoins btc accId 9 `dbShouldBeE` [coin1']+ getSpendableCoins btc accId 10 `dbShouldBeE` [coin1']+ getSpendableCoins btc accId 11 `dbShouldBeE` []+ let coin1'' = coin1 {Store.block = Store.BlockRef 10 0} :: Store.Unspent+ refreshCoins btc accId extAddrs [coin1''] `dbShouldBeE` (1, [])+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin1+ { jsonCoinBlock = Store.BlockRef 10 0,+ jsonCoinConfirmations = 1+ }+ ]+ getSpendableCoins btc accId 0 `dbShouldBeE` [coin1'']+ getSpendableCoins btc accId 1 `dbShouldBeE` [coin1'']+ getSpendableCoins btc accId 2 `dbShouldBeE` []+ -- Lock the coin+ lift $ setLockCoin (OutPoint (txid' 0) 0) True `dbShouldBe` 1+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin1+ { jsonCoinBlock = Store.BlockRef 10 0,+ jsonCoinConfirmations = 1,+ jsonCoinLocked = True+ }+ ]+ getSpendableCoins btc accId 0 `dbShouldBeE` []+ lift $ setLockCoin (OutPoint (txid' 0) 0) True `dbShouldBe` 0+ lift $ setLockCoin (OutPoint (txid' 0) 0) False `dbShouldBe` 1+ lift $ setLockCoin (OutPoint (txid' 0) 0) False `dbShouldBe` 0+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin1+ { jsonCoinBlock = Store.BlockRef 10 0,+ jsonCoinConfirmations = 1,+ jsonCoinLocked = False+ }+ ]+ getSpendableCoins btc accId 0 `dbShouldBeE` [coin1'']+ getSpendableCoins btc accId 1 `dbShouldBeE` [coin1'']+ getSpendableCoins btc accId 2 `dbShouldBeE` []+ -- Delete the coin+ refreshCoins btc accId extAddrs [] `dbShouldBeE` (1, [])+ getSpendableCoins btc accId 0 `dbShouldBeE` []+ coinPage btc accId (Page 5 0) `dbShouldBeE` []++checkFree :: Int -> Bool -> ExceptT String (DB IO) ()+checkFree i free =+ lift $+ ( dBAddressFree . entityVal . fromJust+ <$> P.getBy (UniqueAddress (intAddrsT !! i))+ )+ `dbShouldBe` free++pendingTxsSpec :: Ctx -> Config -> Spec+pendingTxsSpec ctx cfg =+ it "can manage pending transactions" $ do+ runDBMemoryE $ do+ (accId, _) <- testNewAcc ctx "test"+ lift $ updateBest btc (bid' 0) 0+ -- Build some test data coins/txs+ (dBAddressAddress <$> genExtAddress ctx cfg accId "")+ `dbShouldBeE` head extAddrsT+ (dBAddressAddress <$> nextFreeIntAddr ctx cfg accId)+ `dbShouldBeE` head intAddrsT+ checkFree 0 True+ lift $ setAddrsFree AddrBusy [head intAddrsT] `dbShouldBe` 1+ checkFree 0 False+ let fundTx1 = tx' ctx [(txid' 1, 0)] [(addr' 0, 20)]+ fundTx2 = tx' ctx [(txid' 1, 1)] [(iAddr' 0, 10)]+ coin1 = coin' ctx (txHash fundTx1, 0) (Just 0) (addr' 0) 20+ coin2 = coin' ctx (txHash fundTx2, 0) (Just 0) (iAddr' 0) 10+ -- Insert the dependent transactions+ lift $ insertRawTx fundTx1+ lift $ insertRawTx fundTx2+ -- Insert two coins in the database+ (c, dbCoins) <- refreshCoins btc accId (extAddrs <> intAddrs) [coin1, coin2]+ liftTest $ c `shouldBe` 2+ let jsonCoin1 = forceRight $ toJsonCoin btc Nothing (head dbCoins)+ jsonCoin2 = forceRight $ toJsonCoin btc Nothing (dbCoins !! 1)+ tsd <- buildTxSignData btc ctx cfg gen accId [(oAddr' 0, 8)] 0 0 False 1+ liftTest $+ tsd+ `shouldBe` TxSignData+ { txSignDataTx =+ tx'+ ctx+ [(txHash fundTx1, 0)]+ [(iAddr' 1, 12), (oAddr' 0, 8)],+ txSignDataInputs = [fundTx1],+ txSignDataInputPaths = [Deriv :/ 0 :/ 0],+ txSignDataOutputPaths = [Deriv :/ 1 :/ 1],+ txSignDataSigned = False+ }+ -- Import the pending transaction+ checkFree 0 False+ checkFree 1 True+ let h1 = "db1085753d1d9b14005dd2951ff643803d4fa7ffa3bde6841dbb817958cb74e1"+ importPendingTx btc ctx accId tsd `dbShouldBeE` h1+ liftTest $ nosigTxHash (txSignDataTx tsd) `shouldBe` h1+ -- Check locked coins+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = False},+ jsonCoin1 {jsonCoinLocked = True}+ ]+ ((txInfoPending <$>) <$> pendingTxPage ctx accId (Page 5 0))+ `dbShouldBeE` [Just $ TxInfoPending h1 False False]+ -- Check address free status+ checkFree 0 False+ checkFree 1 False+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 2+ lift $+ shouldBeLeft' "chooseCoins: No solution found" $+ buildTxSignData btc ctx cfg gen accId [(oAddr' 1, 12)] 0 0 False 1+ lift $+ shouldBeLeft' "The transaction already exists" $+ importPendingTx btc ctx accId tsd+ -- Create second pending transaction+ tsd2 <- buildTxSignData btc ctx cfg gen accId [(oAddr' 1, 7)] 0 0 False 1+ liftTest $+ tsd2+ `shouldBe` TxSignData+ { txSignDataTx =+ tx'+ ctx+ [(txHash fundTx2, 0)]+ [(iAddr' 2, 3), (oAddr' 1, 7)],+ txSignDataInputs = [fundTx2],+ txSignDataInputPaths = [Deriv :/ 1 :/ 0],+ txSignDataOutputPaths = [Deriv :/ 1 :/ 2],+ txSignDataSigned = False+ }+ -- Import the second transaction+ checkFree 2 True+ let h2 = "bb0e46f95f90bf0d2b0805655a77e632a0bd5088d5722d0055bc2862158ba747"+ importPendingTx btc ctx accId tsd2 `dbShouldBeE` h2+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = True},+ jsonCoin1 {jsonCoinLocked = True}+ ]+ ((txInfoPending <$>) <$> pendingTxPage ctx accId (Page 5 0))+ `dbShouldBeE` [ Just $ TxInfoPending h2 False False,+ Just $ TxInfoPending h1 False False+ ]+ checkFree 0 False+ checkFree 1 False+ checkFree 2 False+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 3+ lift $+ shouldBeLeft' "chooseCoins: No solution found" $+ buildTxSignData btc ctx cfg gen accId [(oAddr' 2, 1)] 0 0 False 1+ -- Delete first transaction+ deletePendingTx ctx h1 `dbShouldBeE` (1, 1)+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = True},+ jsonCoin1 {jsonCoinLocked = False}+ ]+ ((txInfoPending <$>) <$> pendingTxPage ctx accId (Page 5 0))+ `dbShouldBeE` [Just $ TxInfoPending h2 False False]+ checkFree 0 False+ checkFree 1 True+ checkFree 2 False+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 1+ -- Create transaction with no change+ tsd3 <- buildTxSignData btc ctx cfg gen accId [(oAddr' 2, 20)] 0 0 False 1+ liftTest $+ tsd3+ `shouldBe` TxSignData+ { txSignDataTx =+ tx'+ ctx+ [(txHash fundTx1, 0)]+ [(oAddr' 2, 20)],+ txSignDataInputs = [fundTx1],+ txSignDataInputPaths = [Deriv :/ 0 :/ 0],+ txSignDataOutputPaths = [],+ txSignDataSigned = False+ }+ let h3 = "abe82b09dddb8abdb5490c3d404d34f2ff3c8f7b893bbe80f0af4018576e1f9e"+ importPendingTx btc ctx accId tsd3 `dbShouldBeE` h3+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = True},+ jsonCoin1 {jsonCoinLocked = True}+ ]+ ((txInfoPending <$>) <$> pendingTxPage ctx accId (Page 5 0))+ `dbShouldBeE` [ Just $ TxInfoPending h3 False False,+ Just $ TxInfoPending h2 False False+ ]+ checkFree 0 False+ checkFree 1 True+ checkFree 2 False+ (dBAddressIndex <$> nextFreeIntAddr ctx cfg accId) `dbShouldBeE` 1+ -- Import a signed transaction+ let resE = signWalletTx btc ctx tsd2 (fst $ keys ctx)+ liftTest $ resE `shouldSatisfy` isRight+ let tsd2' = fst $ forceRight resE+ liftTest $ txSignDataSigned tsd2' `shouldBe` True+ importPendingTx btc ctx accId tsd2' `dbShouldBeE` h2+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = True},+ jsonCoin1 {jsonCoinLocked = True}+ ]+ ((txInfoPending <$>) <$> pendingTxPage ctx accId (Page 5 0))+ `dbShouldBeE` [ Just $ TxInfoPending h3 False False,+ Just $ TxInfoPending h2 True False+ ]+ checkFree 0 False+ checkFree 1 True+ checkFree 2 False+ lift $ do+ shouldBeLeft' "The transaction already exists" $+ importPendingTx btc ctx accId tsd2'+ shouldBeLeft' "Can not replace a signed transaction with an unsigned one" $+ importPendingTx btc ctx accId tsd2+ -- Set tsd2 to online+ lift $ do+ setPendingTxOnline h2 `dbShouldBe` 1+ shouldBeLeft' "The transaction is already online" $+ importPendingTx btc ctx accId tsd2'+ -- Can not delete an online transaction+ shouldBeLeft $ deletePendingTx ctx h2+ ((txInfoPending <$>) <$> pendingTxPage ctx accId (Page 5 0))+ `dbShouldBeE` [ Just $ TxInfoPending h3 False False,+ Just $ TxInfoPending h2 True True+ ]+ lift $ deletePendingTxOnline $ DBPendingTxKey $ txHashToHex h2+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin2 {jsonCoinLocked = True},+ jsonCoin1 {jsonCoinLocked = True}+ ]+ ((txInfoPending <$>) <$> pendingTxPage ctx accId (Page 5 0))+ `dbShouldBeE` [Just $ TxInfoPending h3 False False]+ checkFree 0 False+ checkFree 1 True+ checkFree 2 False+ -- Check for other errors+ let fundTx3 = tx' ctx [(txid' 2, 0)] [(addr' 8, 30)]+ coin3 = coin' ctx (txHash fundTx3, 0) (Just 0) (addr' 8) 30+ lift $ do+ shouldBeLeft' "A coin referenced by the transaction is locked" $+ importPendingTx btc ctx accId $+ TxSignData+ { txSignDataTx =+ tx'+ ctx+ [(txHash fundTx1, 0)]+ [(oAddr' 3, 20)],+ txSignDataInputs = [fundTx3, fundTx1],+ txSignDataInputPaths = [Deriv :/ 0 :/ 0],+ txSignDataOutputPaths = [],+ txSignDataSigned = False+ }+ shouldBeLeft' "A coin referenced by the transaction does not exist" $+ importPendingTx btc ctx accId $+ TxSignData+ { txSignDataTx =+ tx'+ ctx+ [(txHash fundTx3, 0)]+ [(oAddr' 3, 20)],+ txSignDataInputs = [fundTx3, fundTx1],+ txSignDataInputPaths = [Deriv :/ 0 :/ 8],+ txSignDataOutputPaths = [],+ txSignDataSigned = False+ }+ (_, dbCoins3) <- refreshCoins btc accId (extAddrs <> intAddrs) [coin1, coin3]+ let jsonCoin3 = forceRight $ toJsonCoin btc Nothing (head dbCoins3)+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin3 {jsonCoinLocked = False},+ jsonCoin1 {jsonCoinLocked = True}+ ]+ lift $+ shouldBeLeft' "Some referenced addresses do not exist" $+ importPendingTx btc ctx accId $+ TxSignData+ { txSignDataTx =+ tx'+ ctx+ [(txHash fundTx3, 0)]+ [(iAddr' 1, 5), (oAddr' 3, 25)],+ txSignDataInputs = [fundTx3],+ txSignDataInputPaths = [Deriv :/ 0 :/ 8],+ txSignDataOutputPaths = [Deriv :/ 1 :/ 1],+ txSignDataSigned = False+ }+ -- The transaction is not being rolled back so we roll it back manually+ replicateM_ 10 $ genExtAddress ctx cfg accId ""+ _ <- lift $ setLockCoin (jsonCoinOutpoint jsonCoin3) False+ coinPage btc accId (Page 5 0)+ `dbShouldBeE` [ jsonCoin3 {jsonCoinLocked = False},+ jsonCoin1 {jsonCoinLocked = True}+ ]+ checkFree 0 False+ checkFree 1 True+ checkFree 2 False+ lift $+ shouldBeLeft' "Some of the internal output addresses are not free" $+ importPendingTx btc ctx accId $+ TxSignData+ { txSignDataTx =+ tx'+ ctx+ [(txHash fundTx3, 0)]+ [(iAddr' 2, 1), (oAddr' 3, 25)],+ txSignDataInputs = [fundTx3],+ txSignDataInputPaths = [Deriv :/ 0 :/ 8],+ txSignDataOutputPaths = [Deriv :/ 1 :/ 2],+ txSignDataSigned = False+ }
@@ -0,0 +1,265 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Haskoin.Wallet.EntropySpec where++import Control.Exception (evaluate)+import Control.Monad+import Control.Monad.Trans (liftIO)+import qualified Data.ByteString as BS+import Data.Default (def)+import Data.Maybe+import Data.Text (Text)+import Haskoin+import Haskoin.Util.Arbitrary+import Haskoin.Wallet.Config+import Haskoin.Wallet.Database+import Haskoin.Wallet.Entropy+import Haskoin.Wallet.Signing+import Haskoin.Wallet.TestUtils+import Test.HUnit+import Test.Hspec+import Test.Hspec.QuickCheck+import Test.QuickCheck++spec :: Spec+spec = do+ let cfg = def :: Config+ prepareContext $ \ctx -> do+ diceSpec+ entropySpec+ mnemonicSpec ctx cfg++diceSpec :: Spec+diceSpec =+ describe "Base6 Encoding" $ do+ it "can encode a Word8 to Base6" $ do+ let vs = [6, 1, 2, 3, 4, 5]+ xs = [[a, b, c] | a <- vs, b <- vs, c <- vs]+ ys = [[a, b] | a <- vs, b <- vs]+ as = [0x00 .. 0xd7]+ bs = [0xd8 .. 0xfb]+ cs = [0xfc .. 0xff]+ forM_ (zip as xs) $ \(a, b) -> word8ToBase6 a `shouldBe` b+ forM_ (zip bs ys) $ \(a, b) -> word8ToBase6 a `shouldBe` b+ forM_ cs $ \c -> word8ToBase6 c `shouldBe` []+ -- Checking a few fixed values+ word8ToBase6 0x00 `shouldBe` [6, 6, 6]+ word8ToBase6 0x01 `shouldBe` [6, 6, 1]+ word8ToBase6 0xd7 `shouldBe` [5, 5, 5]+ word8ToBase6 0xd8 `shouldBe` [6, 6]+ word8ToBase6 0xd9 `shouldBe` [6, 1]+ word8ToBase6 0xfb `shouldBe` [5, 5]+ it "can decode base6 to Word8" $ do+ base6ToWord8 [6] [] `shouldBe` ([], [False, False])+ base6ToWord8 [1] [] `shouldBe` ([], [False, True])+ base6ToWord8 [2] [] `shouldBe` ([], [True, False])+ base6ToWord8 [3] [] `shouldBe` ([], [True, True])+ base6ToWord8 [4] [] `shouldBe` ([], [False])+ base6ToWord8 [5] [] `shouldBe` ([], [True])+ base6ToWord8 [5, 6] [] `shouldBe` ([], [True, False, False])+ base6ToWord8 [6, 6, 6, 5] []+ `shouldBe` ([], [False, False, False, False, False, False, True])+ base6ToWord8 [6, 6, 6, 6] [] `shouldBe` ([0x00], [])+ base6ToWord8 [6, 6, 6, 1] [] `shouldBe` ([0x01], [])+ base6ToWord8 [3, 3, 3, 3] [] `shouldBe` ([0xff], [])+ base6ToWord8 [4, 3, 3, 3, 3] [] `shouldBe` ([0xff], [False])+ base6ToWord8 [1, 1, 1, 1, 3, 3, 3, 3] [] `shouldBe` ([0x55, 0xff], [])++entropySpec :: Spec+entropySpec = do+ it "can mix entropy" $ do+ evaluate (xorBytes BS.empty $ BS.pack [0x00]) `shouldThrow` anyException+ evaluate (xorBytes (BS.pack [0x00]) BS.empty) `shouldThrow` anyException+ evaluate (xorBytes (BS.pack [0x00, 0x00]) (BS.pack [0x00]))+ `shouldThrow` anyException+ evaluate (xorBytes (BS.pack [0x00]) (BS.pack [0x00, 0x00]))+ `shouldThrow` anyException+ xorBytes (BS.pack [0x00]) (BS.pack [0x00]) `shouldBe` BS.pack [0x00]+ xorBytes (BS.pack [0x00]) (BS.pack [0xff]) `shouldBe` BS.pack [0xff]+ xorBytes (BS.pack [0xff]) (BS.pack [0x00]) `shouldBe` BS.pack [0xff]+ xorBytes (BS.pack [0xff]) (BS.pack [0xff]) `shouldBe` BS.pack [0x00]+ xorBytes (BS.pack [0xaa]) (BS.pack [0x55]) `shouldBe` BS.pack [0xff]+ xorBytes (BS.pack [0x55, 0xaa]) (BS.pack [0xaa, 0x55])+ `shouldBe` BS.pack [0xff, 0xff]+ xorBytes (BS.pack [0x7a, 0x54]) (BS.pack [0xd3, 0x8e])+ `shouldBe` BS.pack [0xa9, 0xda]+ it "can split entropy" $ do+ splitEntropyWith (BS.pack [0x00]) [] `shouldBe` [BS.pack [0x00]]+ splitEntropyWith (BS.pack [0x00]) [BS.pack [0x00]]+ `shouldBe` [BS.pack [0x00], BS.pack [0x00]]+ splitEntropyWith (BS.pack [0x55]) [BS.pack [0xaa]]+ `shouldBe` [BS.pack [0xff], BS.pack [0xaa]]+ splitEntropyWith (BS.pack [0x55, 0xaa]) [BS.pack [0xaa, 0x55]]+ `shouldBe` [BS.pack [0xff, 0xff], BS.pack [0xaa, 0x55]]+ prop "prop: can split entropy x2" $+ forAll (arbitraryBSn 32) $ \s ->+ forAll (arbitraryBSn 32) $ \k ->+ splitEntropyWith s [k] `shouldBe` [s `xorBytes` k, k]+ prop "prop: can split entropy x3" $+ forAll (arbitraryBSn 32) $ \s ->+ forAll (arbitraryBSn 32) $ \k1 ->+ forAll (arbitraryBSn 32) $ \k2 ->+ splitEntropyWith s [k1, k2]+ `shouldBe` [s `xorBytes` k1 `xorBytes` k2, k1, k2]+ prop "prop: can reconstruct entropy" $+ forAll (arbitraryBSn 32) $ \s ->+ forAll (arbitraryBSn 32) $ \k1 ->+ forAll (arbitraryBSn 32) $ \k2 -> do+ case splitEntropyWith s [k1, k2] of+ [a, b, c] -> (a `xorBytes` b `xorBytes` c) `shouldBe` s+ _ -> expectationFailure "Invalid splitEntropyWith"+ prop "prop: can reconstruct original mnemonic" $+ forAll (arbitraryBSn 32) $ \s ->+ forAll (arbitraryBSn 32) $ \k1 ->+ case splitEntropyWith s [k1] of+ [a, b] ->+ let mnem = toMnemonic s+ splitMnems = mapM toMnemonic [a, b]+ unsplitMnem = mergeMnemonicParts $ forceRight splitMnems+ in unsplitMnem `shouldBe` mnem+ _ -> expectationFailure "Invalid splitEntropyWith"++mnemonicSpec :: Ctx -> Config -> Spec+mnemonicSpec ctx cfg =+ describe "Mnemonic API" $ do+ -- https://github.com/iancoleman/bip39/issues/58+ it "Can derive iancoleman issue 58" $ do+ let m = "fruit wave dwarf banana earth journey tattoo true farm silk olive fence"+ p = "banana"+ mnemPass = MnemonicPass m p+ xpub = forceRight $ deriveXPubKey ctx <$> signingKey btc ctx mnemPass 0+ (addr0, _) = derivePathAddr ctx xpub extDeriv 0+ addrToText btc addr0 `shouldBe` Just "17rxURoF96VhmkcEGCj5LNQkmN9HVhWb7F"+ it "Passes the test vectors 3 (zero padding)" $+ mapM_ (testVector ctx) testVectors3+ it "Passes the test vectors 4 (zero padding)" $+ mapM_ (testVector ctx) testVectors4+ it "Passes the BIP-44 test vectors" $+ mapM_ (testBip44Vector ctx cfg) bip44Vectors++testVector :: Ctx -> (Text, HardPath, Text, Text) -> Assertion+testVector ctx (seedT, deriv, pubT, prvT) = do+ xPrvExport btc xPrv `shouldBe` prvT+ xPubExport btc ctx xPub `shouldBe` pubT+ where+ seed = fromJust $ decodeHex seedT+ xPrv = derivePath ctx deriv (makeXPrvKey seed)+ xPub = deriveXPubKey ctx xPrv++-- bitpay/bitcore-lib#47 and iancoleman/bip39#58+-- https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-3+testVectors3 :: [(Text, HardPath, Text, Text)]+testVectors3 =+ [ ( "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",+ Deriv, -- m/+ "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13",+ "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6"+ ),+ ( "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be",+ Deriv :| 0, -- m/0'+ "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y",+ "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L"+ )+ ]++-- btcsuite/btcutil#172+-- https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-4+testVectors4 :: [(Text, HardPath, Text, Text)]+testVectors4 =+ [ ( "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678",+ Deriv, -- m/+ "xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa",+ "xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv"+ ),+ ( "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678",+ Deriv :| 0, -- m/0'+ "xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m",+ "xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G"+ ),+ ( "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678",+ Deriv :| 0 :| 1, -- m/0'/1'+ "xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt",+ "xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1"+ )+ ]++testBip44Vector :: Ctx -> Config -> (Text, Text, Text, Text) -> Assertion+testBip44Vector ctx cfg (mnem, pass, addr0, addr1) = do+ runDBMemoryE $ do+ (accId, _) <- insertAccount btc ctx walletFP "test" pub+ extAddr <- genExtAddress ctx cfg accId ""+ intAddr <- nextFreeIntAddr ctx cfg accId+ liftIO $ addr0 `shouldBe` dBAddressAddress extAddr+ liftIO $ addr1 `shouldBe` dBAddressAddress intAddr+ where+ mnemPass = MnemonicPass mnem pass+ walletFP = forceRight $ walletFingerprint btc ctx mnemPass+ prv = forceRight $ signingKey btc ctx mnemPass 0+ pub = deriveXPubKey ctx prv++-- (Mnemonic, BIP38 password, external 0/0, internal 1/0)+bip44Vectors :: [(Text, Text, Text, Text)]+bip44Vectors =+ [ ( "modify truck lens identify brief coffee \+ \gather volcano fatal together muscle elephant",+ "",+ "1KiWbwzHhwH2KdLyreGuJq36SP2pPeGeim",+ "1BY9FShyC6rnEUdPAVQTX37TFZqEkR1enQ"+ ),+ ( "modify truck lens identify brief coffee \+ \gather volcano fatal together muscle elephant",+ "password",+ "1Ha18XQ74YfCnuUAjGTF9rnbS7pm1aCzW4",+ "17SounFZ11k8urp9Pee2FNYjzGU5qwLUfT"+ ),+ ( "modify truck lens identify brief coffee \+ \gather volcano fatal together muscle elephant",+ "Hello world",+ "1KpHPSa9eMohQznkddzmHT9AeMAscYe29Y",+ "16WJrKzdik1HgnoiNiLUW2uGDqry2fSGVx"+ ),+ ( "fiscal gadget drastic coconut awful crime \+ \during common salmon manage random cost \+ \evil owner city",+ "",+ "131dALC6WxwTZzTsHUsifAJ8bkQdDEeeqJ",+ "1JHCmBLzDARCdDgn2WwJQr88tGW8V3EyKj"+ ),+ ( "host mind elephant tone sound apple \+ \service tomato subject attend motion stick \+ \fuel fan rail bamboo tree build",+ "",+ "15nbJ6oHvYiapKu1vBYmyQzVSTjiJNcfuV",+ "12rMvadbhRxFaUP4FoRUmABa1JEPLrrqHL"+ ),+ ( "flight skill wisdom mixture patch dirt \+ \trouble behind chair glad detect swarm \+ \swap truly cruise medal walnut glide \+ \wrestle route defy",+ "",+ "12uDDJU7BJUxJomzbFt5BW3GnukGZz6Y5t",+ "1Gv62T4UkLxJ6dnsmNXENMgjBhLzEzJ1Sc"+ ),+ ( "use turtle trap pause spin venue \+ \hazard hope slot april cattle fork \+ \stand finish arrest nasty acoustic clog \+ \entire course universe evil desert produce",+ "",+ "19fJdt5HjJUrmnvKM8fTS43o9aK3NmFZMT",+ "17Kmofo8651DdydBS7zVVy9JLkixkxj6SK"+ ),+ ( "use turtle trap pause spin venue \+ \hazard hope slot april cattle fork \+ \stand finish arrest nasty acoustic clog \+ \entire course universe evil desert produce",+ "nMCm#Qe7u-*LG^99KS%hSNZJh6w&e&w4S&7HA-22^7vDE#F\+ \@=hNZZZ$M=ZSYG42J8P+U+3sf$xS6YpJf7DemaU#fLEVBW8\+ \!FmugPj=K=E*KDmUuQh_2%7K2W8@RJ4?Pua2HFCb8$@ZB^p\+ \s=T=PNFy5APBBdaz_eTq&r&v3hp6P-tTfvNP9J+xfQ-FKZS\+ \7_?g+P*9MTkpjy*K5=8y+y9db#Sv+b*5bfmV8?@YKrf#AEH\+ \7!ARqBXMhS7d%GYAxB%RG",+ "1L1oKPYJ9j3EFX5CcFXz1d3kkAW9Hoh5ic",+ "1J7jpmP7NVoryofiVSCKMMvMitkCP3U6Qj"+ )+ ]
@@ -0,0 +1,471 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NoFieldSelectors #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Haskoin.Wallet.SigningSpec where++import Control.Arrow (second)+import qualified Data.ByteString as BS+import Data.Either (fromRight)+import qualified Data.Map as Map+import Data.Maybe (fromJust, fromMaybe)+import qualified Data.Serialize as S+import Data.Text (Text)+import Data.Word (Word32, Word8)+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Wallet.Database+import Haskoin.Wallet.FileIO+import Haskoin.Wallet.Signing+import Haskoin.Wallet.TestUtils+import Haskoin.Wallet.TxInfo+import Numeric.Natural (Natural)+import System.Random (StdGen, mkStdGen)+import Test.Hspec++spec :: Spec+spec =+ prepareContext $ \ctx -> do+ buildWalletTxSpec ctx+ signWalletTxSpec ctx++buildWalletTxSpec :: Ctx -> Spec+buildWalletTxSpec ctx =+ describe "Transaction builder" $ do+ it "can build a transaction" $ do+ let coins =+ [ coin' ctx (txid' 1, 0) Nothing (addr' 0) 100000000,+ coin' ctx (txid' 1, 1) Nothing (addr' 1) 200000000,+ coin' ctx (txid' 1, 2) Nothing (addr' 1) 300000000,+ coin' ctx (txid' 1, 3) Nothing (addr' 2) 400000000+ ]+ change = iAddr' 0+ rcps = [(oAddr' 0, 200000000), (oAddr' 1, 200000000)]+ resE = buildWalletTx btc ctx gen rcps change coins 314 10000 False+ (fst <$> resE)+ `shouldBe` Right+ ( tx'+ ctx+ [(txid' 1, 2), (txid' 1, 1), (txid' 1, 0)] -- Greedy algorithm+ ((rcps !! 1) : (change, 199825416) : [head rcps])+ )+ (snd <$> resE)+ `shouldBe` Right [coins !! 2, coins !! 1, head coins]+ it "can fail to build a transaction if funds are insufficient" $ do+ let coins =+ [ coin' ctx (txid' 1, 0) Nothing (addr' 0) 100000000,+ coin' ctx (txid' 1, 1) Nothing (addr' 1) 200000000,+ coin' ctx (txid' 1, 2) Nothing (addr' 1) 300000000,+ coin' ctx (txid' 1, 3) Nothing (addr' 2) 400000000+ ]+ change = iAddr' 0+ rcps = [(oAddr' 0, 500000000), (oAddr' 1, 500000000)]+ resE = buildWalletTx btc ctx gen rcps change coins 1 10000 False+ resE `shouldBe` Left "chooseCoins: No solution found"+ it "will drop the change output if it is dust" $ do+ let coins =+ [ coin' ctx (txid' 1, 0) Nothing (addr' 0) 100000000,+ coin' ctx (txid' 1, 1) Nothing (addr' 1) 200000000,+ coin' ctx (txid' 1, 2) Nothing (addr' 1) 300000000,+ coin' ctx (txid' 1, 3) Nothing (addr' 2) 400000000+ ]+ change = iAddr' 0+ rcps = [(oAddr' 0, 500000000), (oAddr' 1, 499990000)]+ resE1 = buildWalletTx btc ctx gen rcps change coins 0 9999 False+ resE2 = buildWalletTx btc ctx gen rcps change coins 0 10000 False+ resE3 = buildWalletTx btc ctx gen rcps change coins 1 9999 False+ (fst <$> resE1)+ `shouldBe` Right+ ( tx'+ ctx+ [(txid' 1, 3), (txid' 1, 2), (txid' 1, 1), (txid' 1, 0)]+ ((rcps !! 1) : (change, 10000) : [head rcps])+ )+ (fst <$> resE2)+ `shouldBe` Right+ ( tx'+ ctx+ [(txid' 1, 3), (txid' 1, 2), (txid' 1, 1), (txid' 1, 0)]+ ((rcps !! 1) : [head rcps])+ )+ (fst <$> resE3)+ `shouldBe` Right+ ( tx'+ ctx+ [(txid' 1, 3), (txid' 1, 2), (txid' 1, 1), (txid' 1, 0)]+ ((rcps !! 1) : [head rcps])+ )+ it "will fail if sending dust" $ do+ let coins =+ [ coin' ctx (txid' 1, 0) Nothing (addr' 0) 100000000,+ coin' ctx (txid' 1, 1) Nothing (addr' 1) 200000000,+ coin' ctx (txid' 1, 2) Nothing (addr' 1) 300000000,+ coin' ctx (txid' 1, 3) Nothing (addr' 2) 400000000+ ]+ change = iAddr' 0+ rcps = [(oAddr' 0, 500000000), (oAddr' 1, 10000)]+ resE = buildWalletTx btc ctx gen rcps change coins 1 10000 False+ resE `shouldBe` Left "Recipient output is smaller than the dust value"+ it "can make the recipient pay for the fees" $ do+ let coins =+ [ coin' ctx (txid' 1, 0) Nothing (addr' 0) 100000000,+ coin' ctx (txid' 1, 1) Nothing (addr' 1) 200000000,+ coin' ctx (txid' 1, 2) Nothing (addr' 1) 300000000,+ coin' ctx (txid' 1, 3) Nothing (addr' 2) 400000000+ ]+ change = iAddr' 0+ rcps = [(oAddr' 0, 200000000), (oAddr' 1, 200000000)]+ resE = buildWalletTx btc ctx gen rcps change coins 314 10000 True+ (fst <$> resE)+ `shouldBe` Right+ ( tx'+ ctx+ [(txid' 1, 2), (txid' 1, 1), (txid' 1, 0)]+ ( (oAddr' 1, 199912708)+ : (change, 200000000)+ : [(oAddr' 0, 199912708)]+ )+ )+ it "fails when recipients cannot pay" $+ do+ let coins =+ [ coin' ctx (txid' 1, 0) Nothing (addr' 0) 100000000,+ coin' ctx (txid' 1, 1) Nothing (addr' 1) 200000000,+ coin' ctx (txid' 1, 2) Nothing (addr' 1) 300000000,+ coin' ctx (txid' 1, 3) Nothing (addr' 2) 400000000+ ]+ change = iAddr' 0+ rcps1 = [(oAddr' 0, 400000000), (oAddr' 1, 87291)] -- fee is 2*87292+ rcps2 = [(oAddr' 0, 400000000), (oAddr' 1, 87292)]+ rcps3 = [(oAddr' 0, 400000000), (oAddr' 1, 97293)]+ resE1 = buildWalletTx btc ctx gen rcps1 change coins 314 10000 True+ resE2 = buildWalletTx btc ctx gen rcps2 change coins 314 10000 True+ resE3 = buildWalletTx btc ctx gen rcps3 change coins 314 10000 True+ resE1 `shouldBe` Left "Recipients can't pay for the fee"+ resE2 `shouldBe` Left "Recipient output is smaller than the dust value"+ (fst <$> resE3)+ `shouldBe` Right+ ( tx'+ ctx+ [(txid' 1, 2), (txid' 1, 1), (txid' 1, 0)]+ ((oAddr' 1, 10001) : (change, 199902707) : [(oAddr' 0, 399912708)])+ )++signWalletTxSpec :: Ctx -> Spec+signWalletTxSpec ctx =+ describe "Transaction signer" $ do+ it "can derive private signing keys" $ do+ let xPrvE = signingKey btc ctx mnemPass 0+ xPubE = deriveXPubKey ctx <$> xPrvE+ xPrvE `shouldBe` Right (fst $ keys ctx)+ xPubE `shouldBe` Right (snd $ keys ctx)+ it "can sign a simple transaction" $ do+ let fundTx = tx' ctx [(txid' 1, 0)] [(addr' 0, 100000000)]+ newTx =+ tx'+ ctx+ [(txHash fundTx, 0)]+ [(oAddr' 0, 50000000), (iAddr' 0, 40000000)]+ dat =+ TxSignData+ newTx+ [fundTx]+ [extDeriv :/ 0]+ [intDeriv :/ 0]+ False+ xPrv = fst $ keys ctx+ let resE = signWalletTx btc ctx dat xPrv+ (resDat, resTxInfo) = fromRight (error "fromRight") resE+ signedTx = txSignDataTx resDat+ txSignDataSigned resDat `shouldBe` True+ resTxInfo+ `shouldBe` TxInfo+ { txInfoHash =+ Just "e66b790c73d4e72fe13a07e247e4439bdea210cac2f947040e901f1e0ce59ac2",+ txInfoType = TxDebit,+ txInfoAmount = -60000000,+ txInfoMyOutputs =+ Map.fromList [(iAddr' 0, MyOutputs 40000000 (intDeriv :/ 0) "")],+ txInfoOtherOutputs = Map.fromList [(oAddr' 0, 50000000)],+ txInfoNonStdOutputs = [],+ txInfoMyInputs =+ Map.fromList+ [ ( addr' 0,+ MyInputs+ 100000000+ (extDeriv :/ 0)+ ""+ [ SigInput+ (PayPKHash (addr' 0).hash160)+ 100000000+ (OutPoint (txHash fundTx) 0)+ sigHashAll+ Nothing+ ]+ )+ ],+ txInfoOtherInputs = Map.empty,+ txInfoNonStdInputs = [],+ txInfoSize = fromIntegral $ BS.length $ S.encode signedTx,+ txInfoFee = 10000000,+ txInfoFeeByte = 44247,+ txInfoBlockRef = Store.MemRef 0,+ txInfoConfirmations = 0,+ txInfoPending =+ Just $+ TxInfoPending+ "9b41b3cb7b10bfd1dc3f7f1885ce3623789b1b946d206fd59fb432ad4fdb6d70"+ True+ False+ }+ it "can set the correct TxInternal transaction types" $ do+ let fundTx =+ tx' ctx [(txid' 1, 0)] [(addr' 0, 100000000), (addr' 1, 200000000)]+ newTx =+ tx'+ ctx+ [(txHash fundTx, 0), (txHash fundTx, 1)]+ [(iAddr' 0, 50000000), (addr' 2, 200000000)]+ dat =+ TxSignData+ newTx+ [fundTx]+ [extDeriv :/ 0, extDeriv :/ 1]+ [intDeriv :/ 0, extDeriv :/ 2]+ False+ xPrv = fst $ keys ctx+ let resE = signWalletTx btc ctx dat xPrv+ (txInfoType . snd <$> resE) `shouldBe` Right TxInternal+ it "fails when an input is not signed" $ do+ let fundTx =+ tx' ctx [(txid' 1, 0)] [(addr' 0, 100000000), (addr' 1, 100000000)]+ newTx =+ tx'+ ctx+ [(txHash fundTx, 0), (txHash fundTx, 1)]+ [(oAddr' 0, 50000000), (iAddr' 0, 40000000)]+ dat =+ TxSignData+ newTx+ [fundTx]+ [extDeriv :/ 0] -- We omit derivation 1+ [intDeriv :/ 0]+ False+ xPrv = fst $ keys ctx+ let resE = signWalletTx btc ctx dat xPrv+ resE `shouldBe` Left "The transaction could not be signed"+ it "fails when referenced input transactions are missing" $ do+ let fundTx = tx' ctx [(txid' 1, 0)] [(addr' 0, 100000000)]+ newTx =+ tx'+ ctx+ [(txHash fundTx, 0), (txHash fundTx, 1)]+ [(oAddr' 0, 50000000), (iAddr' 0, 40000000)]+ dat =+ TxSignData+ newTx+ [fundTx]+ [extDeriv :/ 0, extDeriv :/ 1] -- 1 is missing in fundTx+ [intDeriv :/ 0]+ False+ xPrv = fst $ keys ctx+ let resE = signWalletTx btc ctx dat xPrv+ resE `shouldBe` Left "Referenced input transactions are missing"+ it "fails when private key derivations don't match the Tx inputs" $+ do+ let fundTx =+ tx' ctx [(txid' 1, 0)] [(addr' 0, 100000000), (addr' 1, 100000000)]+ newTx =+ tx'+ ctx+ [(txHash fundTx, 0), (txHash fundTx, 1)]+ [(oAddr' 0, 50000000), (iAddr' 0, 40000000)]+ dat =+ TxSignData+ newTx+ [fundTx]+ [extDeriv :/ 1, extDeriv :/ 2] -- 1 and 2 instead of 0 and 1+ [intDeriv :/ 0]+ False+ xPrv = fst $ keys ctx+ let resE = signWalletTx btc ctx dat xPrv+ resE `shouldBe` Left "Input derivations don't match the transaction inputs"+ it "fails when output derivations don't match the Tx outputs" $+ do+ let fundTx = tx' ctx [(txid' 1, 0)] [(addr' 0, 100000000)]+ newTx =+ tx'+ ctx+ [(txHash fundTx, 0)]+ [(oAddr' 0, 50000000), (iAddr' 0, 40000000)]+ dat =+ TxSignData+ newTx+ [fundTx]+ [extDeriv :/ 0]+ [intDeriv :/ 1] -- 1 instead of 0+ False+ xPrv = fst $ keys ctx+ let resE = signWalletTx btc ctx dat xPrv+ resE `shouldBe` Left "Output derivations don't match the transaction outputs"++-- Test Helpers --++tx' :: Ctx -> [(TxHash, Word32)] -> [(Address, Natural)] -> Tx+tx' ctx xs ys = Tx 1 txi txo [] 0+ where+ txi =+ fmap+ (\(h, p) -> TxIn (OutPoint h p) BS.empty maxBound)+ xs+ f = marshal ctx . PayPKHash . (.hash160)+ txo = fmap (\(a, v) -> TxOut v $ f a) (Control.Arrow.second fromIntegral <$> ys)++txid' :: Word8 -> TxHash+txid' w =+ fromRight (error "Could not decode txhash") $+ S.decode $+ w `BS.cons` BS.replicate 31 0x00++bid' :: Word8 -> BlockHash+bid' w =+ fromRight (error "Could not decode block hash") $+ S.decode $+ w `BS.cons` BS.replicate 31 0x00++coin' ::+ Ctx ->+ (TxHash, Word32) ->+ Maybe Natural ->+ Address ->+ Natural ->+ Store.Unspent+coin' ctx (h, p) hM a v =+ Store.Unspent+ { Store.block =+ maybe (Store.MemRef 0) ((`Store.BlockRef` 0) . fromIntegral) hM,+ Store.outpoint = OutPoint h p,+ Store.value = fromIntegral v,+ Store.script = marshal ctx $ PayPKHash $ (.hash160) a,+ Store.address = Just a+ }++addr' :: Int -> Address+addr' i = extAddrs !! i++iAddr' :: Int -> Address+iAddr' i = intAddrs !! i++oAddr' :: Int -> Address+oAddr' i = othAddrs !! i++-- Test Constants++-- Use a predictable seed for tests+gen :: StdGen+gen = mkStdGen 0++mnemPass :: MnemonicPass+mnemPass =+ MnemonicPass+ "snow senior nerve virus fabric now \+ \fringe clip marble interest analyst can"+ "correct horse battery staple"++mnemPass2 :: MnemonicPass+mnemPass2 =+ MnemonicPass+ "boring auction demand filter frog accuse \+ \company exchange rely slogan trim typical"+ "correct horse battery staple"++walletFPText :: Text+walletFPText = "892eb8e4"++walletFPText2 :: Text+walletFPText2 = "807a5cfb"++walletFP :: Fingerprint+walletFP = forceRight $ textToFingerprint walletFPText++walletFP2 :: Fingerprint+walletFP2 = forceRight $ textToFingerprint walletFPText2++-- Keys for account 0+keys :: Ctx -> (XPrvKey, XPubKey)+keys ctx =+ ( fromJust $+ xPrvImport+ btc+ (fst keysT),+ fromJust $+ xPubImport+ btc+ ctx+ (snd keysT)+ )++-- Account /44'/0'/0' mnemonic 1+keysT :: (Text, Text)+keysT =+ ( "xprv9yHxeaLAZvxXb9VtJNesqk8avfN8misGAW9DUW9eacZJNqsfZxqKLmK5jfmvFideQqGesviJeagzSQYCuQySjgvt7TdfowKja5aJqbgyuNh",+ "xpub6CHK45s4QJWpodaMQQBtCt5KUhCdBBb7Xj4pGtZG8x6HFeCp7W9ZtZdZaxA34YtFAhuebiKqLqHLYoB8HDadGutW8kEH4HeMdeS1KJz8Uah"+ )++-- Account /44'/0'/0' mnemonic 2+keysT2 :: (Text, Text)+keysT2 =+ ( "xprv9yXnZpEVdonEtT3strknsAgso5qq1cwRooo6susmzVmB5E2vvNw1KKRBgvwvNLxXdBHnkEN5R5uXi2QDs3tpkoBbL61NE6bnSbcrvH6keGa",+ "xpub6CX8yKmPUBLY6w8LztHoEJdcM7gKR5fHB2ihgJHPYqJ9x2N5TvFFs7jfYCX6So9oYyu6eLDTG5dbQWPncv1PYJtXLJ4cwymhoCpeTEmnZFZ"+ )++extAddrs :: [Address]+extAddrs = fromMaybe (error "extAddrs no parse") . textToAddr btc <$> extAddrsT++extAddrsT :: [Text]+extAddrsT =+ [ "1KEn7jEXa7KCLeZy59dka5qRJBLnPMmrLj",+ "1AVj9WSYayTwUd8rS1mTTo4A6CPsS83VTg",+ "1Dg6Kg7kQuyiZz41HRWXKUWKRu6ZyEf1Nr",+ "1yQZuJjA6w7hXpc3C2LRiCv22rKCas7F1",+ "1cWcYiGK7NwjPBJuKRqZxV4aymUnPu1mx",+ "1MZuimSXigp8oqxkVUvZofqHNtVjdcdAqc",+ "1JReTkpFnsrMqhSEJwUNZXPAyeTo2HQfnE",+ "1Hx9xWAHhcjea5uJnyADktCfcLbuBnRnwA",+ "1HXJhfiD7JFCGMFZnhKRsZxoPF7xDTqWXP",+ "1MZpAt1FofY69B6fzooFxZqe6SdrVrC3Yw"+ ]++intAddrs :: [Address]+intAddrs = fromMaybe (error "intAddrs no parse") . textToAddr btc <$> intAddrsT++intAddrsT :: [Text]+intAddrsT =+ [ "17KiDLpE3r92gWR8kFGkYDtgHqEVJrznvn",+ "1NqNFsuS7K3dfF8RnAVr9YYCMvJuF9GCn6",+ "1MZNPWwFwy2CqVgWBq6unPWBWrZTQ7WTnr",+ "19XbPiR98wmoJQZ42K8pVMzdCwSXZBh7iz",+ "1Gkn7EsphiaYuv6XXvG4Kyg3LSfqFMeXHX",+ "14VkCGcLkNqUwRMVjpLEyodAhXvzUWLqPM",+ "1PkyVUxPMGTLzUWNFNraMagACA1x3eD4CF",+ "1M2mmDhWTjEuqPfUdaQH6XPsr5i29gx581",+ "184JdZjasQUmNo2AimkbKAW2sxXMF9BAvK",+ "13b1QVnWFRwCrjvhthj4JabpnJ4nyxbBqm"+ ]++othAddrs :: [Address]+othAddrs =+ fromMaybe (error "othAddrs no parse") . textToAddr btc+ <$> [ "1JCq8Aa9d9rg4T4XV93RV3DMxd5u7GkSSU",+ "1PxH6Yutj49mRAabGvcTxnLkFZCuXDXvRJ",+ "191J7K3FaXXyM7C9ceSMRsJNF6aWCvvf1Q",+ "1FVnYNLRdR5vQkynApupUez6ZfcDqsLHdj",+ "1PmNJHnbk7Kct5FMqbEVRxqqR2mXVQKK5P",+ "18CaQNcVwzUkE9KvwmMd6a5UWNgqJFEAh1",+ "1M2Cv69B7LRud8su2wdd7HV2i6MrXqzdKP",+ "19xYPmoJ2XV1vJnSkzsrXUJXCgKvPE3ri4",+ "1N2JAKWVFAoKFEUci3tY3kvrGFY6poRgvm",+ "15EANoYyJoo1J51ERdQzNwZCyhEtPfcP8g"+ ]
@@ -0,0 +1,315 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Haskoin.Wallet.TestUtils where++import Control.Monad+import Control.Monad.Except (ExceptT)+import Control.Monad.Trans (liftIO)+import Control.Monad.Trans.Except (runExceptT)+import Data.Either+import qualified Data.Map.Strict as Map+import Data.Maybe+import Data.String.Conversions (cs)+import Data.Text (Text)+import Data.Word+import Database.Persist.Sql (runMigrationQuiet)+import Database.Persist.Sqlite (runSqlite)+import Haskoin+import qualified Haskoin.Store.Data as Store+import Haskoin.Util.Arbitrary+import Haskoin.Wallet.Backup+import Haskoin.Wallet.Commands+import Haskoin.Wallet.Config+import Haskoin.Wallet.Database+import Haskoin.Wallet.FileIO+import Haskoin.Wallet.TxInfo+import Numeric.Natural+import Test.HUnit+import Test.Hspec+import Test.QuickCheck++genNatural :: Test.QuickCheck.Gen Natural+genNatural = arbitrarySizedNatural++forceRight :: Either a b -> b+forceRight = fromRight (error "fromRight")++runDBMemory :: DB IO a -> Assertion+runDBMemory action = do+ runSqlite ":memory:" $ do+ _ <- runMigrationQuiet migrateAll+ void action++runDBMemoryE :: (Show a) => ExceptT String (DB IO) a -> Assertion+runDBMemoryE action = do+ runSqlite ":memory:" $ do+ _ <- runMigrationQuiet migrateAll+ resE <- runExceptT action+ liftIO $ resE `shouldSatisfy` isRight++arbitraryText :: Gen Text+arbitraryText = cs <$> (arbitrary :: Gen String)++arbitraryPositive :: (Arbitrary a, Integral a) => Gen a+arbitraryPositive = abs <$> arbitrary++arbitraryNatural :: Gen Natural+arbitraryNatural = fromIntegral <$> (arbitraryPositive :: Gen Word64)++arbitraryBlockRef :: Gen Store.BlockRef+arbitraryBlockRef =+ oneof [a, b]+ where+ a = Store.BlockRef <$> arbitraryPositive <*> arbitraryPositive+ b = Store.MemRef <$> arbitrary++arbitraryDBAccount :: Network -> Ctx -> Gen DBAccount+arbitraryDBAccount net ctx =+ DBAccount+ <$> arbitraryText+ <*> (DBWalletKey . fingerprintToText <$> arbitraryFingerprint)+ <*> arbitraryPositive+ <*> (cs . (.name) <$> arbitraryNetwork)+ <*> (cs . pathToStr <$> arbitraryDerivPath)+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> (xPubExport net ctx <$> arbitraryXPubKey ctx)+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitraryUTCTime++arbitraryDBAddress :: Network -> Gen DBAddress+arbitraryDBAddress net =+ DBAddress+ <$> arbitraryPositive+ <*> (DBWalletKey . fingerprintToText <$> arbitraryFingerprint)+ <*> (cs . pathToStr <$> arbitraryDerivPath)+ <*> (cs . pathToStr <$> arbitraryDerivPath)+ <*> (fromJust . addrToText net <$> arbitraryAddress)+ <*> arbitraryText+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitrary+ <*> arbitrary+ <*> arbitraryUTCTime++arbitraryAddressBalance :: Gen AddressBalance+arbitraryAddressBalance =+ AddressBalance+ <$> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitraryPositive+ <*> arbitraryPositive++arbitraryJsonCoin :: Gen JsonCoin+arbitraryJsonCoin =+ JsonCoin+ <$> arbitraryOutPoint+ <*> arbitraryAddress+ <*> arbitraryPositive+ <*> arbitraryBlockRef+ <*> arbitraryNatural+ <*> arbitrary++arbitraryPubKeyDoc :: Ctx -> Gen PubKeyDoc+arbitraryPubKeyDoc ctx =+ PubKeyDoc+ <$> arbitraryXPubKey ctx+ <*> arbitraryNetwork+ <*> arbitraryText+ <*> arbitraryFingerprint++arbitraryTxSignData :: Network -> Ctx -> Gen TxSignData+arbitraryTxSignData net ctx =+ TxSignData+ <$> arbitraryTx net ctx+ <*> resize 10 (listOf (arbitraryTx net ctx))+ <*> resize 10 (listOf arbitrarySoftPath)+ <*> resize 10 (listOf arbitrarySoftPath)+ <*> arbitrary++arbitraryMyOutputs :: Gen MyOutputs+arbitraryMyOutputs =+ MyOutputs+ <$> arbitraryNatural+ <*> arbitrarySoftPath+ <*> arbitraryText++arbitraryMyInputs :: Network -> Ctx -> Gen MyInputs+arbitraryMyInputs net ctx =+ MyInputs+ <$> arbitraryNatural+ <*> arbitrarySoftPath+ <*> arbitraryText+ <*> resize 10 (listOf $ fst <$> arbitrarySigInput net ctx)++arbitraryOtherInputs :: Network -> Ctx -> Gen OtherInputs+arbitraryOtherInputs net ctx =+ OtherInputs+ <$> arbitraryNatural+ <*> resize 10 (listOf $ fst <$> arbitrarySigInput net ctx)++arbitraryTxType :: Gen TxType+arbitraryTxType = oneof [pure TxDebit, pure TxInternal, pure TxCredit]++arbitraryStoreOutput :: Gen Store.StoreOutput+arbitraryStoreOutput =+ Store.StoreOutput+ <$> arbitrary+ <*> arbitraryBS1+ <*> arbitraryMaybe arbitrarySpender+ <*> arbitraryMaybe arbitraryAddress++arbitrarySpender :: Gen Store.Spender+arbitrarySpender = Store.Spender <$> arbitraryTxHash <*> arbitrary++arbitraryStoreInput :: Gen Store.StoreInput+arbitraryStoreInput =+ oneof+ [ Store.StoreCoinbase+ <$> arbitraryOutPoint+ <*> arbitrary+ <*> arbitraryBS1+ <*> listOf arbitraryBS1,+ Store.StoreInput+ <$> arbitraryOutPoint+ <*> arbitrary+ <*> arbitraryBS1+ <*> arbitraryBS1+ <*> arbitrary+ <*> listOf arbitraryBS1+ <*> arbitraryMaybe arbitraryAddress+ ]++arbitraryTxInfo :: Network -> Ctx -> Gen TxInfo+arbitraryTxInfo net ctx =+ TxInfo+ <$> arbitraryMaybe arbitraryTxHash+ <*> arbitraryTxType+ <*> arbitrary+ <*> ( Map.fromList+ <$> resize+ 10+ (listOf $ (,) <$> arbitraryAddress <*> arbitraryMyOutputs)+ )+ <*> ( Map.fromList+ <$> resize+ 10+ (listOf $ (,) <$> arbitraryAddress <*> arbitraryNatural)+ )+ <*> resize 10 (listOf arbitraryStoreOutput)+ <*> ( Map.fromList+ <$> resize+ 10+ (listOf $ (,) <$> arbitraryAddress <*> arbitraryMyInputs net ctx)+ )+ <*> ( Map.fromList+ <$> resize+ 10+ (listOf $ (,) <$> arbitraryAddress <*> arbitraryOtherInputs net ctx)+ )+ <*> resize 10 (listOf arbitraryStoreInput)+ <*> arbitraryNatural+ <*> arbitraryNatural+ <*> arbitraryNatural+ <*> arbitraryBlockRef+ <*> arbitraryNatural+ <*> arbitraryMaybe arbitraryTxInfoPending++arbitraryTxInfoPending :: Gen TxInfoPending+arbitraryTxInfoPending =+ TxInfoPending+ <$> arbitraryTxHash+ <*> arbitrary+ <*> arbitrary++arbitraryAccountBackup :: Ctx -> Gen AccountBackup+arbitraryAccountBackup ctx =+ AccountBackup+ <$> arbitraryText+ <*> arbitraryFingerprint+ <*> arbitraryXPubKey ctx+ <*> arbitraryNetwork+ <*> arbitraryNatural+ <*> arbitraryNatural+ <*> ( Map.fromList+ <$> resize 20 (listOf $ (,) <$> arbitraryAddress <*> arbitraryText)+ )+ <*> resize 20 (listOf arbitraryAddress)+ <*> arbitraryUTCTime++arbitrarySyncRes :: Network -> Ctx -> Gen SyncRes+arbitrarySyncRes net ctx =+ SyncRes+ <$> arbitraryDBAccount net ctx+ <*> arbitraryBlockHash+ <*> arbitrary+ <*> arbitraryNatural+ <*> arbitraryNatural++arbitraryResponse :: Network -> Ctx -> Gen Response+arbitraryResponse net ctx =+ oneof+ [ ResponseError <$> arbitraryText,+ ResponseMnemonic+ <$> arbitraryText+ <*> resize 12 (listOf arbitraryText)+ <*> resize 12 (listOf $ resize 12 $ listOf arbitraryText),+ ResponseAccount <$> arbitraryDBAccount net ctx,+ ResponseAccResult+ <$> arbitraryDBAccount net ctx+ <*> arbitrary+ <*> arbitraryText,+ ResponseFile <$> (cs <$> arbitraryText),+ ResponseAccounts <$> resize 20 (listOf $ arbitraryDBAccount net ctx),+ ResponseAddress <$> arbitraryDBAccount net ctx <*> arbitraryDBAddress net,+ ResponseAddresses+ <$> arbitraryDBAccount net ctx+ <*> resize 20 (listOf $ arbitraryDBAddress net),+ ResponseTxs+ <$> arbitraryDBAccount net ctx+ <*> resize 20 (listOf $ arbitraryTxInfo net ctx),+ ResponseTx <$> arbitraryDBAccount net ctx <*> arbitraryTxInfo net ctx,+ ResponseDeleteTx+ <$> arbitraryTxHash+ <*> arbitraryNatural+ <*> arbitraryNatural,+ ResponseCoins+ <$> arbitraryDBAccount net ctx+ <*> resize 20 (listOf arbitraryJsonCoin),+ ResponseSync+ <$> (resize 10 $ listOf $ arbitrarySyncRes net ctx),+ ResponseRestore+ <$> resize+ 20+ ( listOf $+ (,,)+ <$> arbitraryDBAccount net ctx+ <*> arbitraryNatural+ <*> arbitraryNatural+ ),+ ResponseVersion+ <$> arbitraryText+ <*> arbitraryText,+ ResponseRollDice <$> resize 20 (listOf arbitraryNatural) <*> arbitraryText+ ]++arbitraryConfig :: Gen Config+arbitraryConfig =+ Config+ <$> arbitraryText+ <*> arbitraryNatural+ <*> arbitraryNatural+ <*> arbitraryNatural+ <*> arbitraryNatural+ <*> arbitraryNatural+ <*> arbitraryNatural+ <*> arbitrary
@@ -0,0 +1,52 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-ambiguous-fields -fno-warn-orphans #-}++module Haskoin.Wallet.VersionSpec where++import Haskoin.Wallet.TestUtils+import Haskoin.Wallet.Migration.SemVersion+import Test.Hspec+import Test.QuickCheck+import Test.Hspec.QuickCheck (prop)++instance Arbitrary SemVersion where+ arbitrary =+ oneof+ [ VerMajor <$> arbitraryNatural+ , VerMinor <$> arbitraryNatural <*> arbitraryNatural+ , VerPatch <$> arbitraryNatural <*> arbitraryNatural <*> arbitraryNatural+ ]++spec :: Spec+spec =+ describe "Semantic Version" $ do+ prop "parseSemVersion . verString identity" $+ forAll (arbitrary :: Gen SemVersion) $ \v ->+ parseSemVersion (verString v) `shouldBe` v+ it "passes equality tests" $ do+ parseSemVersion "0" == parseSemVersion "0" `shouldBe` True+ parseSemVersion "0" == parseSemVersion "0.9" `shouldBe` True+ parseSemVersion "0" == parseSemVersion "0.9.0" `shouldBe` True+ parseSemVersion "0.9" == parseSemVersion "0" `shouldBe` True+ parseSemVersion "0.9.0" == parseSemVersion "0" `shouldBe` True+ parseSemVersion "0.9" == parseSemVersion "0.9" `shouldBe` True+ parseSemVersion "0.9" == parseSemVersion "0.9.1" `shouldBe` True+ parseSemVersion "0.9.1" == parseSemVersion "0.9" `shouldBe` True+ parseSemVersion "0.9.1" == parseSemVersion "0.9.1" `shouldBe` True+ parseSemVersion "0" == parseSemVersion "1" `shouldBe` False+ parseSemVersion "0.9" == parseSemVersion "1" `shouldBe` False+ parseSemVersion "0.9.1" == parseSemVersion "1" `shouldBe` False+ parseSemVersion "1" == parseSemVersion "0.9" `shouldBe` False+ parseSemVersion "1" == parseSemVersion "0.9.1" `shouldBe` False+ parseSemVersion "0.9" == parseSemVersion "0.8" `shouldBe` False+ parseSemVersion "0.9" == parseSemVersion "0.8.1" `shouldBe` False+ parseSemVersion "0.8.1" == parseSemVersion "0.9" `shouldBe` False+ parseSemVersion "0.8.1" == parseSemVersion "0.8.2" `shouldBe` False+ it "passes toMajor and toMinor tests" $ do+ toMajor (parseSemVersion "0") `shouldBe` VerMajor 0+ toMajor (parseSemVersion "0.8") `shouldBe` VerMajor 0+ toMajor (parseSemVersion "0.8.1") `shouldBe` VerMajor 0+ toMinor (parseSemVersion "0.8") `shouldBe` VerMinor 0 8+ toMinor (parseSemVersion "0.8.1") `shouldBe` VerMinor 0 8+
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
@@ -1,16 +0,0 @@-module Main where--import Test.Framework (defaultMain)--import qualified Network.Haskoin.Wallet.Units (tests)-import qualified Network.Haskoin.Wallet.Tests (tests)--import Network.Haskoin.Constants--main :: IO ()-main | networkName == "prodnet" = defaultMain- ( Network.Haskoin.Wallet.Tests.tests- ++ Network.Haskoin.Wallet.Units.tests- )- | otherwise = error "Tests are only available on prodnet"-
@@ -1,37 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}-module Network.Haskoin.Wallet.Arbitrary where--import Test.QuickCheck (Arbitrary, arbitrary, oneof)--import Network.Haskoin.Test-import Network.Haskoin.Wallet--instance Arbitrary AccountType where- arbitrary = oneof- [ return AccountRegular- , do- ArbitraryMSParam m n <- arbitrary- return $ AccountMultisig m n- ]--instance Arbitrary NodeAction where- arbitrary = oneof [ NodeActionRescan <$> arbitrary- , return NodeActionStatus- ]--instance Arbitrary TxAction where- arbitrary = oneof- [ do- as' <- arbitrary- let as = map (\(ArbitraryAddress a, x) -> (a, x)) as'- fee <- arbitrary- rcptFee <- arbitrary- minConf <- arbitrary- sign <- arbitrary- return $ CreateTx as fee rcptFee minConf sign- , do- ArbitraryTx tx <- arbitrary- return (ImportTx tx)- , SignTx <$> (arbitrary >>= \(ArbitraryTxHash h) -> return h)- ]-
@@ -1,24 +0,0 @@-module Network.Haskoin.Wallet.Tests (tests) where--import Test.Framework (Test, testGroup)-import Test.Framework.Providers.QuickCheck2 (testProperty)--import Data.Aeson (FromJSON, ToJSON, encode, decode)-import Data.HashMap.Strict (singleton)--import Network.Haskoin.Wallet.Arbitrary ()-import Network.Haskoin.Wallet--tests :: [Test]-tests =- [ testGroup "Serialize & de-serialize types to JSON"- [ testProperty "AccountType" (metaID :: AccountType -> Bool)- , testProperty "TxAction" (metaID :: TxAction -> Bool)- , testProperty "NodeAction" (metaID :: NodeAction -> Bool)- ]- ]--metaID :: (FromJSON a, ToJSON a, Eq a) => a -> Bool-metaID x = (decode . encode) (singleton ("object" :: String) x) ==- Just (singleton ("object" :: String) x)-
@@ -1,1524 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-module Network.Haskoin.Wallet.Units (tests) where--import Control.Concurrent.STM-import Control.Concurrent.STM.TBMChan-import Control.Exception (Exception, handleJust)-import Control.Monad (guard)-import Control.Monad.Logger (NoLoggingT)-import Control.Monad.Trans (liftIO)-import Control.Monad.Trans.Resource (ResourceT)-import qualified Data.ByteString as BS (ByteString, pack)-import Data.List (sort)-import Data.Maybe (fromJust, isJust)-import Data.String.Conversions (cs)-import Data.Word (Word32, Word64)-import Database.Persist (Entity (..), entityVal,- getBy)-import Database.Persist.Sqlite (SqlPersistT,- runMigrationSilent,- runSqlite)-import Network.Haskoin.Block-import Network.Haskoin.Crypto-import Network.Haskoin.Node.HeaderTree-import Network.Haskoin.Script-import Network.Haskoin.Transaction-import Network.Haskoin.Wallet.Internals-import Test.Framework (Test, testGroup)-import Test.Framework.Providers.HUnit (testCase)-import Test.HUnit (Assertion, assertBool,- assertEqual, assertFailure)--type App = SqlPersistT (NoLoggingT (ResourceT IO))---- TODO: Add tests for accounts with no private key-tests :: [Test]-tests =- [ testGroup "Account tests"- [ testCase "Fail create account with wrong keys" $- assertException- (WalletException "Invalid account keys")- ( newAccount NewAccount- { newAccountName = "fail-this"- , newAccountType = AccountRegular- -- This key does not correspond to the one below- , newAccountMaster = Just- "xprv9s21ZrQH143K33Ezpb81k5upGyhrVcwgqNzHRHnQ2kGBPHkJ3sLPjGwj4LML1kr1bLfguJiY21XrYfVrL1CGurfVoMKSPwRdmzt1LwBtVyR"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- )-- , testCase "Creating two accounts with the same data should fail" $- assertException- (WalletException "Account already exists") $ do- _ <- newAccount NewAccount- { newAccountName = "main"- , newAccountType = AccountRegular- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- newAccount NewAccount- { newAccountName = "main"- , newAccountType = AccountRegular- , newAccountMaster = Nothing- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- , testCase "Invalid multisig parameters (0 of 1)" $- assertException (WalletException "Invalid account type") $- newAccount NewAccount- { newAccountName = "multisig-0-of-1"- , newAccountType = AccountMultisig 0 1- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }-- , testCase "Invalid multisig parameters (2 of 1)" $- assertException (WalletException "Invalid account type") $- newAccount NewAccount- { newAccountName = "multisig-2-of-1"- , newAccountType = AccountMultisig 2 1- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }-- , testCase "Invalid multisig parameters (15 of 16)" $- assertException (WalletException "Invalid account type") $- newAccount NewAccount- { newAccountName = "multisig-15-of-16"- , newAccountType = AccountMultisig 15 16- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }-- , testCase "To many multisig keys (3 keys for 1 of 2)" $- assertException (WalletException "Invalid account keys") $- newAccount NewAccount- { newAccountName = "multisig-1-of-2-with-3"- , newAccountType = AccountMultisig 1 2- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- [ "xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"- , "xpub661MyMwAqRbcFEPH5Aon6F7edspeu1v6a1Nw5qJgk1aX5XYg1ktBL9Azra2CKaAJ2bHXEXkeKHE3eFaCJktFiA5tSMDQDs6bi83maQtdYby"- , "xpub661MyMwAqRbcFtDszBWpawpg4KbNWL9qD4VdRwjd1L5cmcS8nXHWXpg9WL1Xc9Yh7HbQBwWDw37YJfc4AF3YEpvAHEBPBFQPFkUcFHnopw8"- ]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }-- , testCase "Calling addAccountKeys with an empty key list should fail" $- assertException- (WalletException "Invalid account keys") $ do- res <- newAccount NewAccount- { newAccountName = "multisig-1-of-2-plus-empty"- , newAccountType = AccountMultisig 1 2- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- addAccountKeys (fst res) []-- , testCase "Calling addAccountKeys on a non-multisig account should fail" $- assertException- (WalletException "The account is already complete") $ do- res <- newAccount NewAccount- { newAccountName = "regular-plus-more"- , newAccountType = AccountRegular- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- addAccountKeys (fst res)- ["xpub661MyMwAqRbcFEPH5Aon6F7edspeu1v6a1Nw5qJgk1aX5XYg1ktBL9Azra2CKaAJ2bHXEXkeKHE3eFaCJktFiA5tSMDQDs6bi83maQtdYby"]-- , testCase "Adding keys to a complete multisig account should fail" $- assertException- (WalletException "The account is already complete") $ do- res <- newAccount NewAccount- { newAccountName = "regular-plus-more"- , newAccountType = AccountMultisig 1 2- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- [ "xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"- , "xpub661MyMwAqRbcFEPH5Aon6F7edspeu1v6a1Nw5qJgk1aX5XYg1ktBL9Azra2CKaAJ2bHXEXkeKHE3eFaCJktFiA5tSMDQDs6bi83maQtdYby"- ]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- addAccountKeys (fst res)- ["xpub661MyMwAqRbcFtDszBWpawpg4KbNWL9qD4VdRwjd1L5cmcS8nXHWXpg9WL1Xc9Yh7HbQBwWDw37YJfc4AF3YEpvAHEBPBFQPFkUcFHnopw8"]-- , testCase "Getting a non-existing account should fail" $- assertException- (WalletException "Account inexistent does not exist") $- getAccount "inexistent"-- ]- , testGroup "Address tests"- [ testCase "Decreasing the address gap should fail" $- assertException (WalletException "The gap of an account can only be increased") $ do- res <- newAccount NewAccount- { newAccountName = "reduce-gap"- , newAccountType = AccountRegular- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- accE' <- setAccountGap (fst res) 15- setAccountGap accE' 14-- , testCase "Setting a label on a hidden address key should fail" $- assertException (WalletException "Invalid address index 10") $ do- res <- newAccount NewAccount- { newAccountName = "label-hidden"- , newAccountType = AccountRegular- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- setAddrLabel (fst res) 10 AddressExternal "Gym membership"-- , testCase "Setting a label on an invalid address key should fail" $- assertException (WalletException "Invalid address index 20") $ do- res <- newAccount NewAccount- { newAccountName = "label-invalid"- , newAccountType = AccountRegular- , newAccountMaster = Just- "xprv9s21ZrQH143K4a5123LatJaWPdyMvCG4Phpb79kLUXNF3Y9U537QUeKzUjkrdoZVVse747ZnNNUGryPZXEoMFjkuUKyWpEMcg7jbxYECE2b"- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- setAddrLabel (fst res) 20 AddressExternal "Gym membership"-- , testCase "Requesting an address prvkey on a read-only account should fail" $- assertException- (WalletException "Could not get private key") $ do- res <- newAccount NewAccount- { newAccountName = "label-invalid"- , newAccountType = AccountRegular- , newAccountMaster = Nothing- , newAccountDeriv = Nothing- , newAccountKeys =- ["xpub661MyMwAqRbcH49U84sbFSXEwforKeyukvkBuY9x2ruDvLUccaRf2SeUL1f6StQke7sCuvott5CjzFqw6aA49g2NSjaARBkQdHE18zjC5hB"]- , newAccountMnemonic = Nothing- , newAccountReadOnly = False- }- addressPrvKey (fst res) Nothing 2 AddressExternal- ]- , testGroup "Wallet tests"- [ testCase "Verify address derivations" $ runUnit testDerivations- , testCase "Verify balances" $ runUnit testBalances- , testCase "Verify balances in conflict" $ runUnit testConflictBalances- , testCase "Offline transactions" $ runUnit testOffline- , testCase "Kill an offline tx by spending its coins" $ runUnit testKillOffline- , testCase "Offline transaction exceptions" testOfflineExceptions- , testCase "Multisig test 1" $ runUnit testImportMultisig- , testCase "Kill Tx" $ runUnit testKillTx- , testCase "Delete Tx" $ runUnit testDeleteTx- , testCase "Delete Unsigned Tx" $ runUnit testDeleteUnsignedTx- , testCase "Notifications" $ runUnit testNotification- ]- ]--assertException :: (Exception e, Eq e) => e -> App a -> Assertion-assertException ex action =- handleJust matchEx (const $ return ()) $ do- runUnit action- assertFailure $ "Expecting exception: " ++ show ex- where- matchEx = guard . (== ex)--runUnit :: App a -> Assertion-runUnit action = do- _ <- runSqlite ":memory:" $ do- _ <- runMigrationSilent migrateWallet- initWallet 0.0001- action- return ()--ms :: Mnemonic-ms = "mass coast dance birth online various renew alert crunch middle absurd health"--tid1 :: TxHash-tid1 = "0000000000000000000000000000000000000000000000000000000000000001"--z :: Hash256-z = "0000000000000000000000000000000000000000000000000000000000000000"--fakeNode :: NodeBlock -- ^ Parent- -> [TxHash] -- ^ Transactions- -> Word32 -- ^ Chain- -> Word32 -- ^ Nonce- -> NodeBlock-fakeNode parent tids chain nonce =- nodeBlock parent chain header- where- header = createBlockHeader- (blockVersion $ nodeHeader parent)- (nodeHash parent)- (if null tids then z else buildMerkleRoot tids)- (nodeTimestamp parent + 600)- (blockBits $ nodeHeader parent)- nonce---- -- Creates fake testing blocks--- fakeNode :: Word32 -> BlockHash -> NodeBlock--- fakeNode i h = BlockHeaderNode--- { blockHeaderNodeHash = headerHash header--- , blockHeaderNodeHeader = BlockHeader 1 z1 z2 0 0 0--- , blockHeaderNodeHeight = i--- , blockHeaderNodeWork = 0--- , blockHeaderNodeMedianTimes = []--- , blockHeaderNodeMinWork = 0--- }--- where--- z1 = "0000000000000000000000000000000000000000000000000000000000000000"--- z2 = "0000000000000000000000000000000000000000000000000000000000000000"--fakeTx :: [(TxHash, Word32)] -> [(BS.ByteString, Word64)] -> Tx-fakeTx xs ys =- createTx 1 txi txo 0- where- txi = map (\(h,p) -> TxIn (OutPoint h p) (BS.pack [1]) maxBound) xs- f = encodeOutputBS . PayPKHash . fromJust . base58ToAddr- txo = map (\(a,v) -> TxOut v $ f a ) ys--testDerivations :: App ()-testDerivations = do- accE <- fst <$> newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }-- unusedAddresses accE AddressExternal (ListRequest 0 0 False)- >>= liftIO . assertEqual "Generated external addresses do not match"- [ "1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe"- , "1NrcKe9UNtxjjgMSLdhBYaEgrQWQFnvJXX"- , "123XgHRNxkprjec72EpxBFPytPim21u9Kc"- , "16AuD3mAQMzsUHkMGfkQQapG6jhHmV3Rys"- , "1AjF1GhsxyXCN5doPGLjSztDcuLbfYMkqw"- , "1LPUYEjUd1u9dgjM3RqnoYj7Zt4j4dmZYA"- , "1Kzyb5Fpj2VmMoCNWxLNMYeMSu5ocS7u7e"- , "131L3UXV6WakXpyXvmqzqNkHwWJzWExR9i"- , "19FWGTZERHzMePTqKoR8nB9y6w7S5u9yGr"- , "135dwGc8JG2dmhy79onerHKdqoqibHShRJ"- ] . map (addrToBase58 . walletAddrAddress) . fst-- unusedAddresses accE AddressInternal (ListRequest 0 0 False)- >>= liftIO . assertEqual "Generated internal addresses do not match"- [ "1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm"- , "1JVocGcqZvQFbfpeQUY92iL9ARLsAP4DjW"- , "15yaPowqF2g9B3xbdyXaPygV9bfXFjYj2X"- , "187xp9nQTsCa7QJLHYSMyaM1Mf5u2knJYY"- , "1QDQwk4d4zVWH3TnUHMCvDvvrTqFSaA3hr"- , "17ATGqWhkXYLxPdynWrFjnJBvD3EYxqC5A"- , "1EHgPV5DEs3GwUSaRQD7hd2m5MkR4hdByb"- , "1EvUYGoC7p7GC8BtYTz4RX2ERxjUsT3zLz"- , "15857PJZQxUH8Jgomv83mYqJqDsVEUZwz1"- , "18L5fQjr5yXdqqnK98v2UQi1WHAXfYHr7L"- ] . map (addrToBase58 . walletAddrAddress) . fst---assertBalance :: AccountId -> Word32 -> Word64 -> App ()-assertBalance ai conf b = do- bal <- accountBalance ai conf False- liftIO $ assertEqual ("Balance is not " ++ show b) b bal--assertBalanceOffline :: AccountId -> Word32 -> Word64 -> App ()-assertBalanceOffline ai conf b = do- bal <- accountBalance ai conf True- liftIO $ assertEqual ("Balance is not " ++ show b) b bal--assertAddress :: Entity Account- -> Word32 -- Confirmations- -> Word32 -- Address Index- -> AddressType- -> [(KeyIndex, BalanceInfo)]- -> App ()-assertAddress acc conf addr addrtype b = do- b' <- addressBalances acc addr addr addrtype conf False- liftIO $ assertEqual "Address Balance incorrect" b b'--assertAddressOffline :: Entity Account- -> Word32 -- Confirmations- -> Word32 -- Address Index- -> AddressType- -> [(KeyIndex, BalanceInfo)]- -> App ()-assertAddressOffline acc conf addr addrtype b = do- b' <- addressBalances acc addr addr addrtype conf True- liftIO $ assertEqual "Address Balance incorrect" b b'--assertImportTx :: AccountId -> Int -> TxConfidence -> Tx -> App ()-assertImportTx ai as conf tx = do- tx' <- testTx <$> importNetTx tx Nothing- liftIO $ assertEqual "Transaction import failed" ([(ai, conf)], as) tx'--assertImportTxOffline :: AccountId -> Int -> TxConfidence -> Tx -> App ()-assertImportTxOffline ai as conf tx = do- tx' <- testTx <$> importTx tx Nothing ai- liftIO $ assertEqual "Transaction import failed" ([(ai, conf)], as) tx'--assertTxConfidence :: AccountId -> TxHash -> TxConfidence -> App ()-assertTxConfidence ai txh conf = do- txM <- getBy $ UniqueAccTx ai txh- case txM of- Just tx -> do- let conf' = walletTxConfidence $ entityVal tx- liftIO $ assertEqual "Transaction confidence wrong" conf' conf- Nothing -> liftIO $ assertFailure "Transaction not found"--testBalances :: App ()-testBalances = do- accE@(Entity ai _) <- fst <$> newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- let fundingTx = fakeTx- [ (tid1, 0) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000)- , ("1NrcKe9UNtxjjgMSLdhBYaEgrQWQFnvJXX", 20000000)- ]- let tx1 = fakeTx- [ (txHash fundingTx, 0)- , (txHash fundingTx, 1)- ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 30000000) ] -- external- tx2 = fakeTx- [ (txHash fundingTx, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 5000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 5000000) -- change- ]-- assertBalance ai 0 0-- -- Import funding transaction twice. This operation should be idempotent- assertImportTx ai 2 TxPending fundingTx- assertImportTx ai 0 TxPending fundingTx-- spendableCoins ai 0 (const . const []) >>=- liftIO . assertEqual "0-conf spendable coins is not 2" 2 . length- spendableCoins ai 1 (const . const []) >>=- liftIO . assertEqual "1-conf spendable coins is not 0" 0 . length-- assertBalance ai 0 30000000- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal [(0, BalanceInfo 10000000 0 1 0)]- assertAddress accE 0 1 AddressExternal [(1, BalanceInfo 20000000 0 1 0)]- assertAddress accE 1 0 AddressExternal [(0, BalanceInfo 0 0 0 0)]- assertAddress accE 1 1 AddressExternal [(1, BalanceInfo 0 0 0 0)]-- assertImportTx ai 0 TxPending tx1-- assertBalance ai 0 0- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 1 AddressExternal- [(1, BalanceInfo 20000000 20000000 1 1)]- assertAddress accE 1 0 AddressExternal- [(0, BalanceInfo 0 0 0 0)]- assertAddress accE 1 1 AddressExternal- [(1, BalanceInfo 0 0 0 0)]-- -- We re-import tx1. This operation has to be idempotent with respect to- -- balances.- assertImportTx ai 0 TxPending tx1-- assertBalance ai 1 0- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 1 AddressExternal- [(1, BalanceInfo 20000000 20000000 1 1)]- assertAddress accE 1 0 AddressExternal- [(0, BalanceInfo 0 0 0 0)]- assertAddress accE 1 1 AddressExternal- [(1, BalanceInfo 0 0 0 0)]-- -- Importing tx2 twice. This operation has to be idempotent.- assertImportTx ai 1 TxDead tx2- assertImportTx ai 0 TxDead tx2-- assertBalance ai 0 0- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 1 AddressExternal- [(1, BalanceInfo 20000000 20000000 1 1)]-- -- Confirm the funding transaction at height 1- let block1 = fakeNode genesisBlock [txHash fundingTx] 0 1- importMerkles (BestChain [block1]) [[txHash fundingTx]] Nothing-- assertBalance ai 0 0- assertBalance ai 1 0- assertBalance ai 2 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 1 AddressExternal- [(1, BalanceInfo 20000000 20000000 1 1)]- assertAddress accE 1 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 1 1 AddressExternal- [(1, BalanceInfo 20000000 20000000 1 1)]-- -- Confirm tx1 at height 2- let block2 = fakeNode block1 [txHash tx1] 0 2- importMerkles (BestChain [block2]) [[txHash tx1]] Nothing-- assertBalance ai 0 0- assertBalance ai 1 0- assertBalance ai 2 0- assertBalance ai 3 0-- assertAddress accE 2 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 2 1 AddressExternal- [(1, BalanceInfo 20000000 20000000 1 1)]- assertAddress accE 3 0 AddressExternal- [(0, BalanceInfo 0 0 0 0)]- assertAddress accE 3 1 AddressExternal- [(1, BalanceInfo 0 0 0 0)]-- -- Reorg on tx2- let block2' = fakeNode block1 [] 1 22- block3' = fakeNode block2 [txHash tx2] 1 33- importMerkles (ChainReorg block1 [block2] [block2', block3']) [[], [txHash tx2]] Nothing-- getBy (UniqueAccTx ai (txHash tx1))- >>= liftIO- . assertEqual "Confidence is not dead" TxDead- . walletTxConfidence . entityVal . fromJust-- getBy (UniqueAccTx ai (txHash tx2))- >>= liftIO- . assertEqual "Confidence is not building" TxBuilding- . walletTxConfidence . entityVal . fromJust-- assertBalance ai 0 25000000- assertBalance ai 1 25000000- assertBalance ai 2 20000000- assertBalance ai 3 20000000- assertBalance ai 4 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 1 AddressExternal- [(1, BalanceInfo 20000000 0 1 0)]-- assertAddress accE 3 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 3 1 AddressExternal- [(1, BalanceInfo 20000000 0 1 0)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 5000000 0 1 0)]- assertAddress accE 1 0 AddressInternal- [(0, BalanceInfo 5000000 0 1 0)]- assertAddress accE 2 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- -- Reimporting tx2 should be idempotent and return TxBuilding- assertImportTx ai 0 TxBuilding tx2-- accountBalance ai 0 False >>=- liftIO . assertEqual "Balance is not 25000000" 25000000- accountBalance ai 1 False >>=- liftIO . assertEqual "Balance is not 25000000" 25000000- accountBalance ai 2 False >>=- liftIO . assertEqual "Balance is not 20000000" 20000000- accountBalance ai 3 False >>=- liftIO . assertEqual "Balance is not 20000000" 20000000- accountBalance ai 4 False >>=- liftIO . assertEqual "Balance is not 0" 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 1 AddressExternal- [(1, BalanceInfo 20000000 0 1 0)]- assertAddress accE 3 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 3 1 AddressExternal- [(1, BalanceInfo 20000000 0 1 0)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 5000000 0 1 0)]- assertAddress accE 1 0 AddressInternal- [(0, BalanceInfo 5000000 0 1 0)]- assertAddress accE 2 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- -- Reorg back onto tx1- let block3 = fakeNode block2 [] 0 3- block4 = fakeNode block3 [] 0 4- importMerkles (ChainReorg block1 [block2', block3'] [block2, block3, block4])- [[txHash tx1], [], []] Nothing-- assertBalance ai 0 0- assertBalance ai 1 0- assertBalance ai 2 0- assertBalance ai 3 0- assertBalance ai 4 0- assertBalance ai 5 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 1 AddressExternal- [(1, BalanceInfo 20000000 20000000 1 1)]- assertAddress accE 4 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 4 1 AddressExternal- [(1, BalanceInfo 20000000 20000000 1 1)]- assertAddress accE 5 0 AddressExternal- [(0, BalanceInfo 0 0 0 0)]- assertAddress accE 5 1 AddressExternal- [(1, BalanceInfo 0 0 0 0)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]---- tx1, tx2 and tx3 form a chain, and tx4 is in conflict with tx1-testConflictBalances :: App ()-testConflictBalances = do- accE@(Entity ai _) <- fst <$> newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- let tx1 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]- tx2 = fakeTx- [ (txHash tx1, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change- ]- tx3 = fakeTx- [ (txHash tx2, 1) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external- tx4 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 20000000) ]-- -- Import first transaction- assertImportTx ai 1 TxPending tx1-- assertBalance ai 0 10000000- assertBalance ai 0 10000000- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]-- -- Import second transaction- assertImportTx ai 1 TxPending tx2-- assertBalance ai 0 4000000- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 0 1 0)]-- -- Let's confirm these two transactions- let block1 = fakeNode genesisBlock [txHash tx1] 0 1- block2 = fakeNode block1 [txHash tx2] 0 2- importMerkles (BestChain [block1, block2])- [[txHash tx1], [txHash tx2]] Nothing-- assertBalance ai 0 4000000- assertBalance ai 1 4000000- assertBalance ai 2 0- assertBalance ai 3 0-- assertAddress accE 1 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 1 0 AddressInternal- [(0, BalanceInfo 4000000 0 1 0)]- assertAddress accE 2 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 2 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- -- Import third transaction- assertImportTx ai 0 TxPending tx3-- assertBalance ai 0 0- assertBalance ai 1 0- assertBalance ai 2 0- assertBalance ai 3 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 4000000 1 1)]- assertAddress accE 1 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 1 0 AddressInternal- [(0, BalanceInfo 4000000 4000000 1 1)]- assertAddress accE 2 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 2 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- -- Now let's add tx4 which is in conflict with tx1- assertImportTx ai 0 TxDead tx4-- assertBalance ai 0 0- assertBalance ai 1 0- assertBalance ai 2 0- assertBalance ai 3 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 4000000 1 1)]- assertAddress accE 1 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 1 0 AddressInternal- [(0, BalanceInfo 4000000 4000000 1 1)]- assertAddress accE 2 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 2 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- -- Now we trigger a reorg that validates tx4. tx1, tx2 and tx3 should be dead- let block1' = fakeNode genesisBlock [] 1 11- block2' = fakeNode block1' [txHash tx4] 1 22- block3' = fakeNode block2' [] 1 33- importMerkles- (ChainReorg genesisBlock [block1, block2] [block1', block2', block3'])- [[], [txHash tx4], []] Nothing-- assertTxConfidence ai (txHash tx1) TxDead- assertTxConfidence ai (txHash tx2) TxDead- assertTxConfidence ai (txHash tx3) TxDead-- assertBalance ai 0 20000000- assertBalance ai 1 20000000- assertBalance ai 2 20000000- assertBalance ai 3 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 20000000 0 1 0)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]- assertAddress accE 2 0 AddressExternal- [(0, BalanceInfo 20000000 0 1 0)]- assertAddress accE 3 0 AddressExternal- [(0, BalanceInfo 0 0 0 0)]-- -- Reorg back to tx1, tx2 and tx3- let block3 = fakeNode block2 [] 0 3- block4 = fakeNode block3 [] 0 4- importMerkles- (ChainReorg genesisBlock [block1', block2', block3']- [block1, block2, block3, block4])- [[txHash tx1], [txHash tx2], [], []] Nothing-- assertTxConfidence ai (txHash tx1) TxBuilding- assertTxConfidence ai (txHash tx2) TxBuilding-- -- Tx3 remains dead until it is included into a block. Dead transaction are- -- only revived upon confirmations. They are not revived if they are not- -- confirmed even if they have no conflicts anymore.- assertTxConfidence ai (txHash tx3) TxDead- assertTxConfidence ai (txHash tx4) TxDead-- assertBalance ai 0 4000000- assertBalance ai 1 4000000- assertBalance ai 2 4000000- assertBalance ai 3 4000000- assertBalance ai 4 0- assertBalance ai 5 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 0 1 0)]- assertAddress accE 3 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 3 0 AddressInternal- [(0, BalanceInfo 4000000 0 1 0)]- assertAddress accE 4 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddress accE 4 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]- assertAddress accE 5 0 AddressExternal- [(0, BalanceInfo 0 0 0 0)]--testOffline :: App ()-testOffline = do- accE@(Entity ai _) <- fst <$> newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- let tx1 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]- tx2 = fakeTx- [ (txHash tx1, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change- ]- tx3 = fakeTx- [ (txHash tx2, 1) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external- tx4 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 20000000) ]-- -- Import first transaction- assertImportTxOffline ai 1 TxOffline tx1-- assertBalance ai 0 0- assertBalanceOffline ai 0 10000000- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 0 0 0 0)]- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]-- -- Reimporting a transaction should me idempotent- assertImportTxOffline ai 0 TxOffline tx1-- assertBalance ai 0 0- assertBalanceOffline ai 0 10000000- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 0 0 0 0)]- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]-- -- Import tx2- assertImportTxOffline ai 1 TxOffline tx2-- assertBalanceOffline ai 0 4000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 0 1 0)]-- -- Import tx3- assertImportTxOffline ai 0 TxOffline tx3-- assertBalanceOffline ai 0 0-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 4000000 1 1)]-- -- Import tx4- assertImportTxOffline ai 0 TxOffline tx4-- assertTxConfidence ai (txHash tx1) TxDead- assertTxConfidence ai (txHash tx2) TxDead- assertTxConfidence ai (txHash tx3) TxDead-- assertBalanceOffline ai 0 20000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 20000000 0 1 0)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- -- importTx should be idempotent- assertImportTxOffline ai 0 TxOffline tx4-- assertBalanceOffline ai 0 20000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 20000000 0 1 0)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]--testKillOffline :: App ()-testKillOffline = do- accE@(Entity ai _) <- fst <$> newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- let tx1 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]- tx2 = fakeTx- [ (txHash tx1, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change- ]- tx3 = fakeTx- [ (txHash tx2, 1) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external- tx4 = fakeTx- [ (txHash tx1, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 2000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 3000000) -- change- , ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 5000000) -- more change- ]-- -- Import tx1 as a network transaction- assertImportTx ai 1 TxPending tx1-- assertBalance ai 0 10000000- assertBalanceOffline ai 0 10000000- assertBalance ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]-- -- Import tx2 as offline- assertImportTxOffline ai 1 TxOffline tx2-- assertBalance ai 0 10000000- assertBalanceOffline ai 0 4000000- assertBalance ai 1 0- assertBalanceOffline ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 0 1 0)]-- -- Import tx3 as offline- assertImportTxOffline ai 0 TxOffline tx3-- assertBalance ai 0 10000000- assertBalanceOffline ai 0 0- assertBalance ai 1 0- assertBalanceOffline ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 4000000 1 1)]-- -- Import tx4 as a network transaction. It should override tx2 and tx3.- assertImportTx ai 0 TxPending tx4-- assertTxConfidence ai (txHash tx2) TxDead- assertTxConfidence ai (txHash tx3) TxDead-- assertBalance ai 0 8000000- assertBalanceOffline ai 0 8000000- assertBalance ai 1 0- assertBalanceOffline ai 1 0-- assertAddress accE 0 0 AddressExternal- [(0, BalanceInfo 15000000 10000000 2 1)]- assertAddress accE 0 0 AddressInternal- [(0, BalanceInfo 3000000 0 1 0)]- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 15000000 10000000 2 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 3000000 0 1 0)]--testOfflineExceptions :: Assertion-testOfflineExceptions = do- let tx1 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]- tx2 = fakeTx- [ (txHash tx1, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change- ]- tx3 = fakeTx- [ (txHash tx2, 1) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external- tx4 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 20000000) ]-- assertException (WalletException "Could not import offline transaction") $ do- _ <- newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- Entity ai _ <- getAccount "acc1"- assertImportTx ai 1 TxPending tx1- importTx tx4 Nothing ai-- assertException (WalletException "Could not import offline transaction") $ do- _ <- newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- Entity ai _ <- getAccount "acc1"- assertImportTx ai 1 TxPending tx4- assertImportTx ai 0 TxDead tx1- assertImportTx ai 1 TxDead tx2- importTx tx3 Nothing ai-- assertException (WalletException "Could not import offline transaction") $ do- _ <- newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- Entity ai _ <- getAccount "acc1"- assertImportTx ai 1 TxPending tx1- importTx tx1 Nothing ai---- This test create a multisig account with the key of testImportMultisig2-testImportMultisig :: App ()-testImportMultisig = do- accE1@(Entity ai1 _) <- fst <$> newAccount NewAccount- { newAccountName = "ms1"- , newAccountType = AccountMultisig 2 2- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = ["xpub68kRFKHWxUt3oS8X5kVogwH5rvuAd4jrLkxVfHeudFC4MfwQ8oYV59F91uFnsLXANRB1MkN4Wa1PwymE4cRsU8PE755HNCb1EoBbSoAKXpW"]- , newAccountReadOnly = False- }- accE2@(Entity ai2 _) <- fst <$> newAccount NewAccount- { newAccountName = "ms2"- , newAccountType = AccountMultisig 2 2- , newAccountDeriv = Just (Deriv :| 1)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = ["xpub68kRFKHWxUt3mfJjcXdLeuDjCHnByqKSBVfMktJRXM6LSNNDR4ae6Nw1Kh621fzyKiBf6ssyZWPPTDUTQp1BhuZQuoVdtb8j2TRzqDLHmY7"]- , newAccountReadOnly = False- }- let fundingTx = createTx- 1- [ TxIn (OutPoint tid1 0) (BS.pack [1]) maxBound ] -- dummy input- [ TxOut 10000000 $- encodeOutputBS $ PayScriptHash $ fromJust $- base58ToAddr "32RexHZdsMoV8yzL1pQyFhYY6XeUNcWP78"- ]- 0-- importNetTx fundingTx Nothing- >>= liftIO- . assertEqual "Transaction import failed"- ([(ai1, TxPending), (ai2, TxPending)], 2)- . testTx-- -- Create a transaction which has 0 signatures in ms1- tx1 <- fst <$> createWalletTx accE1 Nothing Nothing- [ ( fromJust $ base58ToAddr "3BYWaQHz6AVXx7wXmCka4846tRfa1ccWvh"- , 5000000- )- ] 10000 0 False True-- liftIO $ assertEqual "Confidence is not offline" TxOffline $- walletTxConfidence tx1-- spendableCoins ai1 0 (const . const [])- >>= liftIO- . assertEqual "Wrong txhash in coins" []- . map (walletCoinHash . entityVal . inCoinDataCoin)-- txs Nothing ai1 (ListRequest 0 10 False)- >>= liftIO- . assertEqual "Wrong txhash in tx list"- (sort [txHash fundingTx, walletTxHash tx1])- . sort . map walletTxHash . fst-- assertBalance ai1 0 10000000- assertBalance ai1 1 0- assertBalanceOffline ai1 0 9990000-- -- Import the empty transaction in ms2- tx2 <- head . fst <$> importTx (walletTxTx tx1) Nothing ai2-- -- This second import should be idempotent- _ <- importTx (walletTxTx tx1) Nothing ai2-- liftIO $ assertEqual "Txid do not match"- (walletTxHash tx1) (walletTxHash tx2)-- liftIO $ assertEqual "Confidence is not offline" TxOffline $- walletTxConfidence tx2-- spendableCoins ai2 0 (const . const [])- >>= liftIO- . assertEqual "Wrong txhash in coins" []- . map (walletCoinHash . entityVal . inCoinDataCoin)-- txs Nothing ai2 (ListRequest 0 10 False)- >>= liftIO- . assertEqual "Wrong txhash in tx list"- (sort [txHash fundingTx, walletTxHash tx2])- . sort . map walletTxHash . fst-- assertBalance ai2 0 10000000- assertBalance ai2 1 0- assertBalanceOffline ai2 0 9990000-- -- Sign the transaction in ms2- tx3:_ <- fst <$> signAccountTx accE2 Nothing Nothing (walletTxHash tx2)-- liftIO $ assertEqual "Confidence is not pending" TxPending $- walletTxConfidence tx3-- spendableCoins ai2 0 (const . const [])- >>= liftIO- . assertEqual "Wrong txhash in coins"- [walletTxHash tx3, walletTxHash tx3]- . map (walletCoinHash . entityVal . inCoinDataCoin)-- txs Nothing ai2 (ListRequest 0 10 False)- >>= liftIO- . assertEqual "Wrong txhash in tx list"- (sort [txHash fundingTx, walletTxHash tx3])- . sort . map walletTxHash . fst-- assertBalance ai2 0 9990000- assertBalance ai2 1 0- assertBalanceOffline ai2 0 9990000-- tx4 <- fmap (entityVal . fromJust) $- getBy $ UniqueAccTx ai1 $ walletTxHash tx3-- liftIO $ assertEqual "Confidence is not pending" TxPending $- walletTxConfidence tx4-- spendableCoins ai1 0 (const . const [])- >>= liftIO- . assertEqual "Wrong txhash in coins"- [walletTxHash tx3, walletTxHash tx3]- . map (walletCoinHash . entityVal . inCoinDataCoin)-- txs Nothing ai1 (ListRequest 0 10 False)- >>= liftIO- . assertEqual "Wrong txhash in tx list"- (sort [txHash fundingTx, walletTxHash tx3])- . sort . map walletTxHash . fst-- assertBalance ai1 0 9990000- assertBalance ai1 1 0- assertBalanceOffline ai1 0 9990000-- -- Importing the transaction should have no effect as it was globally- -- imported already in the previous step.- tx5 <- head . fst <$> importTx (walletTxTx tx3) Nothing ai1-- liftIO $ assertEqual "Confidence is not pending" TxPending $- walletTxConfidence tx5-- spendableCoins ai1 0 (const . const [])- >>= liftIO- . assertEqual "Wrong txhash in coins"- [walletTxHash tx5, walletTxHash tx5]- . map (walletCoinHash . entityVal . inCoinDataCoin)-- txs Nothing ai1 (ListRequest 0 10 False)- >>= liftIO- . assertEqual "Wrong txhash in tx list"- (sort [txHash fundingTx, walletTxHash tx5])- . sort . map walletTxHash . fst-- assertBalance ai1 0 9990000- assertBalance ai1 1 0- assertBalanceOffline ai1 0 9990000--testDeleteTx :: App ()-testDeleteTx = do- accE@(Entity ai _) <- fst <$> newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- let tx1 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]- tx2 = fakeTx- [ (txHash tx1, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change- ]- tx3 = fakeTx- [ (txHash tx2, 1) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external-- assertImportTx ai 1 TxPending tx1- assertImportTx ai 1 TxPending tx2- assertImportTx ai 0 TxPending tx3-- assertBalance ai 0 0-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 4000000 1 1)]-- tx2M' <- getTx $ txHash tx2- liftIO $ assertBool "Transaction 2 not found" $ isJust tx2M'- deleteTx $ txHash tx2-- tx1M <- getTx $ txHash tx1- tx2M'' <- getTx $ txHash tx2- tx3M <- getTx $ txHash tx3- liftIO $ assertEqual "Transaction 1 removed" (Just tx1) tx1M- liftIO $ assertEqual "Transaction 2 not removed" Nothing tx2M''- liftIO $ assertEqual "Transaction 3 not removed" Nothing tx3M-- assertBalance ai 0 10000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]--testDeleteUnsignedTx :: App ()-testDeleteUnsignedTx = do- accE1@(Entity ai1 _) <- fst <$> newAccount NewAccount- { newAccountName = "ms1"- , newAccountType = AccountMultisig 2 2- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = ["xpub68kRFKHWxUt3oS8X5kVogwH5rvuAd4jrLkxVfHeudFC4MfwQ8oYV59F91uFnsLXANRB1MkN4Wa1PwymE4cRsU8PE755HNCb1EoBbSoAKXpW"]- , newAccountReadOnly = False- }- Entity ai2 _ <- fst <$> newAccount NewAccount- { newAccountName = "ms2"- , newAccountType = AccountMultisig 2 2- , newAccountDeriv = Just (Deriv :| 1)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = ["xpub68kRFKHWxUt3mfJjcXdLeuDjCHnByqKSBVfMktJRXM6LSNNDR4ae6Nw1Kh621fzyKiBf6ssyZWPPTDUTQp1BhuZQuoVdtb8j2TRzqDLHmY7"]- , newAccountReadOnly = False- }- let fundingTx = createTx- 1- [ TxIn (OutPoint tid1 0) (BS.pack [1]) maxBound ] -- dummy input- [ TxOut 10000000 $- encodeOutputBS $ PayScriptHash $ fromJust $- base58ToAddr "32RexHZdsMoV8yzL1pQyFhYY6XeUNcWP78"- ]- 0-- importNetTx fundingTx Nothing- >>= liftIO- . assertEqual "Confidence is not pending"- ([(ai1, TxPending), (ai2, TxPending)], 2)- . testTx-- -- Create a transaction which has 0 signatures in ms1- tx1 <- fst <$> createWalletTx accE1 Nothing Nothing- [ ( fromJust $ base58ToAddr "3BYWaQHz6AVXx7wXmCka4846tRfa1ccWvh"- , 5000000- )- ] 10000 0 False True-- liftIO $ assertEqual "Confidence is not offline" TxOffline $- walletTxConfidence tx1-- spendableCoins ai1 0 (const . const [])- >>= liftIO- . assertEqual "Wrong txhash in coins" []- . map (walletCoinHash . entityVal . inCoinDataCoin)-- txs Nothing ai1 (ListRequest 0 10 False)- >>= liftIO- . assertEqual "Wrong txhash in tx list"- (sort [txHash fundingTx, walletTxHash tx1])- . sort . map walletTxHash . fst-- assertBalance ai1 0 10000000- assertBalance ai1 1 0- assertBalanceOffline ai1 0 9990000-- tx1EM <- getTx $ walletTxHash tx1- liftIO $ assertBool "Transaction 1 not found" $ isJust tx1EM- deleteTx $ walletTxHash tx1-- tx1M <- getTx $ walletTxHash tx1- liftIO $ assertEqual "Transaction not removed" Nothing tx1M--testNotification :: App ()-testNotification = do- _ <- newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- let tx1 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]- tx2 = fakeTx- [ (txHash tx1, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change- ]- tx3 = fakeTx- [ (txHash tx2, 1) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external-- notifChan <- liftIO $ atomically $ newTBMChan 1000-- _ <- importNetTx tx1 (Just notifChan)- tx1NM <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case tx1NM of- Just (NotifTx JsonTx{..}) ->- assertEqual "Notif hash does not match" (txHash tx1) jsonTxHash- _ -> assertFailure "Transaction notification is not the right type"-- _ <- importNetTx tx2 (Just notifChan)- tx2NM <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case tx2NM of- Just (NotifTx JsonTx{..}) ->- assertEqual "Notif hash does not match" (txHash tx2) jsonTxHash- _ -> assertFailure "Transaction notification is not the right type"-- _ <- importNetTx tx3 Nothing-- let block1 = fakeNode genesisBlock [txHash tx1] 0 1- block2 = fakeNode block1 [txHash tx2] 0 2- best = BestChain [block1, block2]- txs1 = [[txHash tx1], [txHash tx2]]- importMerkles best txs1 (Just notifChan)- b1NM <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case b1NM of- Just (NotifBlock JsonBlock{..}) ->- assertEqual "Block hash does not match"- (nodeHash block1) jsonBlockHash- _ -> assertFailure "Block notification not the right type"- tx1NM' <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case tx1NM' of- Just (NotifTx JsonTx{..}) ->- assertEqual "Transaction list does not match" (txHash tx1)- jsonTxHash- _ -> assertFailure "Transaction notification not the right type"- b2NM <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case b2NM of- Just (NotifBlock JsonBlock{..}) ->- assertEqual "Block hash does not match"- (nodeHash block2) jsonBlockHash- _ -> assertFailure "Block notification not the right type"- tx2NM' <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case tx2NM' of- Just (NotifTx JsonTx{..}) ->- assertEqual "Transaction list does not match" (txHash tx2)- jsonTxHash- _ -> assertFailure "Transaction notification not the right type"--- let block2' = fakeNode block1 [txHash tx2, txHash tx3] 1 22- txs2 = [[txHash tx2, txHash tx3]]- reorg = ChainReorg block1 [block2] [block2']- importMerkles reorg txs2 (Just notifChan)- b3NM <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case b3NM of- Just (NotifBlock JsonBlock{..}) ->- assertEqual "Block hash does not match"- (nodeHash block2') jsonBlockHash- _ -> assertFailure "Block notification not the right type"- tx3NM2 <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case tx3NM2 of- Just (NotifTx JsonTx{..}) ->- assertEqual "Transaction does not match"- (txHash tx2)- jsonTxHash- _ -> assertFailure "Transaction notification not the right type"- tx3NM3 <- liftIO $ atomically $ readTBMChan notifChan- liftIO $ case tx3NM3 of- Just (NotifTx JsonTx{..}) ->- assertEqual "Transaction does not match"- (txHash tx3)- jsonTxHash- _ -> assertFailure "Transaction notification not the right type"---testKillTx :: App ()-testKillTx = do- accE@(Entity ai _) <- fst <$> newAccount NewAccount- { newAccountName = "acc1"- , newAccountType = AccountRegular- , newAccountDeriv = Just (Deriv :| 0)- , newAccountMaster = Nothing- , newAccountMnemonic = Just (cs ms)- , newAccountKeys = []- , newAccountReadOnly = False- }- let tx1 = fakeTx- [ (tid1, 4) ]- [ ("1DLW4wieCwUPMh6ThVwT2bKqSzkjeb8wUe", 10000000) ]- tx2 = fakeTx- [ (txHash tx1, 0) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 6000000) -- external- , ("1PY7pWZ5FddWi747C6k5Y7okNHFUM2BKAm", 4000000) -- change- ]- tx3 = fakeTx- [ (txHash tx2, 1) ]- [ ("1MchgrtQEUgV1f7Nqe1vEzvdmBzJHz8zrY", 4000000) ] -- external-- assertImportTx ai 1 TxPending tx1- assertImportTx ai 1 TxPending tx2- assertImportTx ai 0 TxPending tx3-- assertBalanceOffline ai 0 0-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 4000000 1 1)]-- killTxs Nothing [txHash tx2]-- assertBalanceOffline ai 0 10000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- -- Killing a transaction should be idempotent- killTxs Nothing [txHash tx2]-- assertBalanceOffline ai 0 10000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- killTxs Nothing [txHash tx3]-- assertBalanceOffline ai 0 10000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 0 1 0)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 0 0 0 0)]-- reviveTx Nothing tx2-- assertBalanceOffline ai 0 4000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 0 1 0)]-- -- Reviving a transaction should be idempotent- reviveTx Nothing tx2-- assertBalanceOffline ai 0 4000000-- assertAddressOffline accE 0 0 AddressExternal- [(0, BalanceInfo 10000000 10000000 1 1)]- assertAddressOffline accE 0 0 AddressInternal- [(0, BalanceInfo 4000000 0 1 0)]--testTx :: ([WalletTx], [WalletAddr])- -> ([(AccountId, TxConfidence)], Int)-testTx (txls, addrs) = (map f txls, length addrs)- where- f tx = (walletTxAccount tx, walletTxConfidence tx)-