haskoin-wallet-0.0.1: Network/Haskoin/Wallet/Store/DbTx.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}
module Network.Haskoin.Wallet.Store.DbTx
( dbImportTx
, dbRemoveTx
, dbSendTx
, dbSendSolution
, dbSendCoins
, dbSignTx
, yamlTx
, RawTxOutPoints(..)
, RawTxDests(..)
, RawSigInput(..)
, RawPrvKey(..)
) where
import Control.Applicative ((<$>), (<*>))
import Control.Monad (forM, unless, when, mzero)
import Control.Monad.Trans (liftIO)
import Control.Monad.Trans.Either (EitherT, left)
import Data.Time (UTCTime, getCurrentTime)
import Data.Word (Word32, Word64)
import Data.List ((\\), nub)
import Data.Maybe (catMaybes, isNothing, isJust, fromJust)
import Data.Either (rights)
import Data.Yaml
( Value
, object
, (.=), (.:), (.:?)
, parseJSON
, Parser
)
import qualified Data.Aeson as Json
( FromJSON
, withObject
, withArray
)
import qualified Data.Vector as V (toList)
import qualified Data.HashMap.Strict as H (toList)
import qualified Data.Map.Strict as M
( toList
, empty
, lookup
, insert
)
import qualified Data.Text as T (pack, unpack)
import Database.Persist
( PersistStore
, PersistUnique
, PersistQuery
, PersistMonadBackend
, Entity(..)
, entityVal
, entityKey
, get
, getBy
, selectList
, deleteWhere
, updateWhere
, update
, insert_
, insertUnique
, replace
, (=.), (==.), (<-.)
)
import Database.Persist.Sql (SqlBackend)
import Network.Haskoin.Wallet.Keys
import Network.Haskoin.Wallet.Manager
import Network.Haskoin.Wallet.TxBuilder
import Network.Haskoin.Wallet.Store.DbAccount
import Network.Haskoin.Wallet.Store.DbAddress
import Network.Haskoin.Wallet.Store.DbCoin
import Network.Haskoin.Wallet.Store.Util
import Network.Haskoin.Script
import Network.Haskoin.Protocol
import Network.Haskoin.Crypto
import Network.Haskoin.Util
import Network.Haskoin.Util.BuildMonad
yamlTx :: DbTxGeneric b -> Value
yamlTx tx = object $ concat
[ [ "Recipients" .= dbTxRecipients tx
, "Value" .= dbTxValue tx
]
, if dbTxOrphan tx then ["Orphan" .= True] else []
, if dbTxPartial tx then ["Partial" .= True] else []
]
-- |Remove a transaction from the database and any parent transaction
dbRemoveTx :: PersistQuery m => String -> EitherT String m [String]
dbRemoveTx tid = do
-- Find all parents of this transaction
-- Partial transactions should not have any coins. Won't check for it
coins <- selectList [ DbCoinTxid ==. tid ] []
let parents = nub $ catStatus $ map (dbCoinStatus . entityVal) coins
-- Recursively remove parents
pids <- forM parents dbRemoveTx
-- Delete output coins generated from this transaction
deleteWhere [ DbCoinTxid ==. tid ]
-- Delete account transactions
deleteWhere [ DbTxTxid ==. tid ]
-- Delete transaction blob
deleteWhere [ DbTxBlobTxid ==. tid ]
-- Delete orphaned input coins spent by this transaction
deleteWhere [ DbCoinOrphan ==. True
, DbCoinStatus <-. [Spent tid, Reserved tid]
]
-- Unspend input coins that were previously spent by this transaction
updateWhere [ DbCoinStatus <-. [Spent tid, Reserved tid] ]
[ DbCoinStatus =. Unspent ]
return $ tid:(concat pids)
-- |Import a transaction into the database
dbImportTx :: ( PersistQuery m, PersistUnique m
, PersistMonadBackend m ~ SqlBackend
)
=> Tx -> EitherT String m [DbTxGeneric SqlBackend]
dbImportTx tx = do
coinsM <- mapM (getBy . f) $ map prevOutput $ txIn tx
let inCoins = catMaybes coinsM
-- Unspent coins in the wallet related to this transaction
unspent = filter ((== Unspent) . dbCoinStatus . entityVal) inCoins
-- OutPoints from this transaction with no associated coins
unknown = map snd $ filter (isNothing . fst) $ zip coinsM $ txIn tx
-- Fail if an input is spent by a transaction which is not this one
unless (isImportValid tid $ map entityVal inCoins) $ left
"dbImportTx: Double spend detected. Import failed"
let toRemove = txToRemove $ map entityVal inCoins
if length toRemove > 0
then do
-- Partial transactions need to be removed
-- before trying to re-import
_ <- mapM dbRemoveTx toRemove
dbImportTx tx
else do
time <- liftIO getCurrentTime
-- Change status of all the unspent coins
_ <- forM unspent $ \(Entity ci _) ->
update ci [DbCoinStatus =. status]
-- Insert orphaned coins
orphans <- catMaybes <$> mapM (dbImportOrphan status) unknown
-- Import new coins and update existing coins
outCoins <- catMaybes <$>
(mapM (dbImportCoin tid complete) $ zip (txOut tx) [0..])
let accTxs = buildAccTx tx ((map entityVal inCoins) ++ orphans)
outCoins (not complete) time
-- Insert transaction blob. Ignore if already exists
_ <- insertUnique $ DbTxBlob tid (encode' tx) time
-- insert account transactions into database,
-- rewriting if they exist
_ <- forM accTxs $ \accTx -> do
prev <- getBy $ UniqueTx (dbTxTxid accTx) (dbTxAccount accTx)
if isNothing prev
then insert_ accTx
else replace (entityKey $ fromJust prev) accTx
-- Re-import parent transactions (transactions spending this one)
if complete
then do
let ids = nub $ catStatus $ map dbCoinStatus outCoins
blobs <- mapM dbGetTxBlob ids
reImported <- forM blobs $ (dbImportTx =<<) . dec
return $ accTxs ++ (concat reImported)
else return accTxs
where
tid = encodeTxid $ txid tx
f (OutPoint h i) = CoinOutPoint (encodeTxid h) (fromIntegral i)
complete = isTxComplete tx
status = if complete then Spent tid else Reserved tid
dec = liftEither . decodeToEither . dbTxBlobValue . entityVal
-- |A transaction can not be imported if it double spends coins in the wallet.
-- Upstream code needs to remove the conflicting transaction first using
-- dbTxRemove function
isImportValid :: String -> [DbCoinGeneric b] -> Bool
isImportValid tid coins = all (f . dbCoinStatus) coins
where
f (Spent parent) = parent == tid
f _ = True
-- When a transaction spends coins previously spent by a partial transaction,
-- we need to remove the partial transactions from the database and try to
-- re-import the transaction. Coins with Reserved status are spent by a partial
-- transaction.
txToRemove :: [DbCoinGeneric b] -> [String]
txToRemove coins = catMaybes $ map (f . dbCoinStatus) coins
where
f (Reserved parent) = Just parent
f _ = Nothing
-- |Group input and output coins by accounts and create
-- account-level transaction
buildAccTx :: Tx -> [DbCoinGeneric b] -> [DbCoinGeneric b]
-> Bool -> UTCTime -> [DbTxGeneric b]
buildAccTx tx inCoins outCoins partial time = map build $ M.toList oMap
where
iMap = foldr (f (\(i,o) x -> (x:i,o))) M.empty inCoins
oMap = foldr (f (\(i,o) x -> (i,x:o))) iMap outCoins
f g coin accMap = case M.lookup (dbCoinAccount coin) accMap of
Just tuple -> M.insert (dbCoinAccount coin) (g tuple coin) accMap
Nothing -> M.insert (dbCoinAccount coin) (g ([],[]) coin) accMap
allRecip = rights $ map toAddr $ txOut tx
toAddr = (addrToBase58 <$>) . scriptRecipient . scriptOutput
sumVal = sum . (map dbCoinValue)
build (ai,(i,o)) =
DbTx (encodeTxid $ txid tx) recips total ai orphan partial time
where
orphan = or $ map dbCoinOrphan i
total | orphan = 0
| otherwise = sumVal o - sumVal i
addrs = map dbCoinAddress o
recips | null addrs = allRecip
| orphan = allRecip \\ addrs -- assume this is an outgoing tx
| total < 0 = allRecip \\ addrs -- remove the change
| otherwise = addrs
-- |Create an orphaned coin if the input spends from an address in the wallet
-- but the coin doesn't exist in the wallet. This allows out-of-order tx import
dbImportOrphan :: (PersistUnique m, PersistMonadBackend m ~ SqlBackend)
=> CoinStatus -> TxIn
-> EitherT String m (Maybe (DbCoinGeneric SqlBackend))
dbImportOrphan status (TxIn (OutPoint h i) s _)
| isLeft a = return Nothing
| otherwise = getBy (UniqueAddress b58) >>= \addrM -> case addrM of
Nothing -> return Nothing
Just (Entity _ add) -> do
time <- liftIO $ getCurrentTime
rdm <- dbGetRedeem add
let coin = build rdm add time
insert_ coin >> return (Just coin)
where
a = scriptSender s
b58 = addrToBase58 $ fromRight a
build rdm add time =
DbCoin (encodeTxid h) (fromIntegral i) 0 "" rdm b58 status
(dbAddressAccount add) True time
-- |Create a new coin for an output if it sends coins to an
-- address in the wallet. Does not actually write anything to the database
-- if commit is False. This is for correctly reporting on partial transactions
-- without creating coins in the database from a partial transaction
dbImportCoin :: ( PersistQuery m, PersistUnique m
, PersistMonadBackend m ~ SqlBackend
)
=> String -> Bool -> (TxOut,Int)
-> EitherT String m (Maybe (DbCoinGeneric SqlBackend))
dbImportCoin tid commit ((TxOut v s), index)
| isLeft a = return Nothing
| otherwise = getBy (UniqueAddress b58) >>= \addrM -> case addrM of
Nothing -> return Nothing
Just (Entity _ add) -> do
time <- liftIO getCurrentTime
coinM <- getBy $ CoinOutPoint tid index
case coinM of
Just (Entity ci coin) -> do
-- Update existing coin with up to date information
let newCoin = coin{ dbCoinOrphan = False
, dbCoinValue = (fromIntegral v)
, dbCoinScript = scp
, dbCoinCreated = time
}
when commit $ replace ci newCoin
return $ Just newCoin
Nothing -> do
rdm <- dbGetRedeem add
let coin = build rdm add time
when commit $ insert_ coin
dbAdjustGap add -- Adjust address gap
return $ Just coin
where
a = scriptRecipient s
b58 = addrToBase58 $ fromRight a
scp = bsToHex $ encodeScriptOps s
build rdm add time =
DbCoin tid index (fromIntegral v) scp rdm b58 Unspent
(dbAddressAccount add) False time
-- |Builds a redeem script given an address. Only relevant for addresses
-- linked to multisig accounts. Otherwise it returns Nothing
dbGetRedeem :: (PersistStore m, PersistMonadBackend m ~ SqlBackend)
=> DbAddressGeneric SqlBackend -> EitherT String m (Maybe String)
dbGetRedeem add = do
acc <- liftMaybe accErr =<< (get $ dbAddressAccount add)
rdm <- if isMSAcc acc
then Just <$> liftMaybe rdmErr (getRdm acc)
else return Nothing
return $ bsToHex . encodeScriptOps . encodeOutput <$> rdm
where
getRdm acc = do
key <- loadPubAcc =<< (xPubImport $ dbAccountKey acc)
msKeys <- mapM xPubImport $ dbAccountMsKeys acc
addrKeys <- f key msKeys $ fromIntegral $ dbAddressIndex add
let pks = map (xPubKey . getAddrPubKey) addrKeys
sortMulSig . (PayMulSig pks) <$> dbAccountMsRequired acc
where
f = if dbAddressInternal add then intMulSigKey else extMulSigKey
accErr = "dbImportOut: Invalid address account"
rdmErr = "dbGetRedeem: Could not generate redeem script"
-- |Build and sign a transactoin given a list of recipients
dbSendTx :: ( PersistUnique m, PersistQuery m
, PersistMonadBackend m ~ SqlBackend
)
=> AccountName -> [(String,Word64)] -> Word64
-> EitherT String m (Tx, Bool)
dbSendTx name dests fee = do
(coins,recips) <- dbSendSolution name dests fee
dbSendCoins coins recips (SigAll False)
-- |Given a list of recipients and a fee, finds a valid combination of coins
dbSendSolution :: ( PersistUnique m, PersistQuery m
, PersistMonadBackend m ~ SqlBackend
)
=> AccountName -> [(String,Word64)] -> Word64
-> EitherT String m ([Coin],[(String,Word64)])
dbSendSolution name dests fee = do
(Entity ai acc) <- dbGetAcc name
unspent <- liftEither . (mapM toCoin) =<< dbCoins ai
(coins,change) <- liftEither $ if isMSAcc acc
then let msParam = ( fromJust $ dbAccountMsRequired acc
, fromJust $ dbAccountMsTotal acc
)
in chooseMSCoins tot fee msParam unspent
else chooseCoins tot fee unspent
recips <- if change < 5000 then return dests else do
cAddr <- dbGenIntAddrs name 1
return $ dests ++ [(dbAddressBase58 $ head cAddr,change)]
return (coins,recips)
where
tot = sum $ map snd dests
-- | Build and sign a transaction by providing coins and recipients
dbSendCoins :: PersistUnique m
=> [Coin] -> [(String,Word64)] -> SigHash
-> EitherT String m (Tx, Bool)
dbSendCoins coins recipients sh = do
tx <- liftEither $ buildAddrTx (map coinOutPoint coins) recipients
ys <- mapM (dbGetSigData sh) coins
let sigTx = detSignTx tx (map fst ys) (map snd ys)
bsTx <- liftEither $ buildToEither sigTx
return (bsTx, isComplete sigTx)
dbSignTx :: PersistUnique m
=> AccountName -> Tx -> SigHash -> EitherT String m (Tx, Bool)
dbSignTx name tx sh = do
(Entity ai _) <- dbGetAcc name
coins <- catMaybes <$> (mapM (getBy . f) $ map prevOutput $ txIn tx)
-- Filter coins for this account only
let accCoins = filter ((== ai) . dbCoinAccount . entityVal) coins
ys <- forM accCoins g
let sigTx = detSignTx tx (map fst ys) (map snd ys)
bsTx <- liftEither $ buildToEither sigTx
return (bsTx, isComplete sigTx)
where
f (OutPoint h i) = CoinOutPoint (encodeTxid h) (fromIntegral i)
g = ((dbGetSigData sh) =<<) . liftEither . toCoin . entityVal
-- |Given a coin, retrieves the necessary data to sign a transaction
dbGetSigData :: PersistUnique m
=> SigHash -> Coin -> EitherT String m (SigInput,PrvKey)
dbGetSigData sh coin = do
(Entity _ w) <- dbGetWallet "main"
a <- liftEither $ scriptRecipient out
(Entity _ add) <- dbGetAddr $ addrToBase58 a
acc <- liftMaybe accErr =<< get (dbAddressAccount add)
mst <- liftMaybe mstErr $ loadMasterKey =<< xPrvImport (dbWalletMaster w)
aKey <- liftMaybe prvErr $ accPrvKey mst $ fromIntegral $ dbAccountIndex acc
let g = if dbAddressInternal add then intPrvKey else extPrvKey
sigKey <- liftMaybe addErr $ g aKey $ fromIntegral $ dbAddressIndex add
return (sigi, xPrvKey $ getAddrPrvKey sigKey)
where
out = scriptOutput $ coinTxOut coin
rdm = coinRedeem coin
sigi | isJust rdm = SigInputSH out (coinOutPoint coin) (fromJust rdm) sh
| otherwise = SigInput out (coinOutPoint coin) sh
mstErr = "dbGetSigData: Could not load master key"
accErr = "dbGetSigData: Could not load address account"
prvErr = "dbGetSigData: Invalid account derivation index"
addErr = "dbGetSigData: Invalid address derivation index"
data RawTxOutPoints = RawTxOutPoints [OutPoint]
deriving (Eq, Show)
data RawTxDests = RawTxDests [(String,Word64)]
deriving (Eq, Show)
instance Json.FromJSON RawTxOutPoints where
parseJSON = Json.withArray "Expected: Array" $ \arr -> do
RawTxOutPoints <$> (mapM f $ V.toList arr)
where
f = Json.withObject "Expected: Object" $ \obj -> do
tid <- obj .: T.pack "txid" :: Parser String
vout <- obj .: T.pack "vout" :: Parser Word32
let i = maybeToEither ("Failed to decode txid" :: String)
(decodeTxid tid)
o = OutPoint <$> i <*> (return vout)
either (const mzero) return o
instance Json.FromJSON RawTxDests where
parseJSON = Json.withObject "Expected: Object" $ \obj ->
RawTxDests <$> (mapM f $ H.toList obj)
where
f (add,v) = do
amnt <- parseJSON v :: Parser Word64
return (T.unpack add, amnt)
data RawSigInput = RawSigInput [(SigHash -> SigInput)]
data RawPrvKey = RawPrvKey [PrvKey]
deriving (Eq, Show)
instance Json.FromJSON RawSigInput where
parseJSON = Json.withArray "Expected: Array" $ \arr -> do
RawSigInput <$> (mapM f $ V.toList arr)
where
f = Json.withObject "Expected: Object" $ \obj -> do
tid <- obj .: T.pack "txid" :: Parser String
vout <- obj .: T.pack "vout" :: Parser Word32
scp <- obj .: T.pack "scriptPubKey" :: Parser String
rdm <- obj .:? T.pack "scriptRedeem" :: Parser (Maybe String)
let s = decodeScriptOps =<< maybeToEither "Hex parsing failed"
(hexToBS scp)
i = maybeToEither "Failed to decode txid" (decodeTxid tid)
o = OutPoint <$> i <*> (return vout)
r = decodeScriptOps =<< maybeToEither "Hex parsing failed"
(hexToBS $ fromJust rdm)
res | isJust rdm = SigInputSH <$> s <*> o <*> r
| otherwise = SigInput <$> s <*> o
either (const mzero) return res
instance Json.FromJSON RawPrvKey where
parseJSON = Json.withArray "Expected: Array" $ \arr ->
RawPrvKey <$> (mapM f $ V.toList arr)
where
f v = do
str <- parseJSON v :: Parser String
maybe mzero return $ fromWIF str