haskoin-wallet-0.9.4: src/Haskoin/Wallet/TxInfo.hs
{-# 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