This repository has been archived by the owner on Aug 18, 2020. It is now read-only.
/
Pure.hs
223 lines (197 loc) · 8.82 KB
/
Pure.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
{-# LANGUAGE TemplateHaskell #-}
-- | Pure functions for operations with transactions
module Pos.Wallet.Tx.Pure
(
-- * Tx creation
makePubKeyTx
, makeMOfNTx
, createTx
, createMOfNTx
-- * History derivation
, getRelatedTxs
, deriveAddrHistory
, deriveAddrHistoryPartial
-- * Additional datatypes
, TxHistoryEntry (..)
, thTxId
, thTx
, thIsOutput
, thDifficulty
, TxError
) where
import Control.Lens (makeLenses, (%=))
import Control.Monad.State (StateT (..), evalStateT)
import Control.Monad.Trans.Maybe (MaybeT (..))
import qualified Data.DList as DL
import Data.List (tail)
import qualified Data.Map as M
import qualified Data.Vector as V
import Universum
import Pos.Binary ()
import Pos.Crypto (PublicKey, SecretKey, WithHash (..), hash,
sign, toPublic, withHash)
import Pos.Data.Attributes (mkAttributes)
import Pos.Script (Script)
import Pos.Script.Examples (multisigRedeemer, multisigValidator)
-- import Pos.Ssc.Class (Ssc)
import Pos.Types (Address, Block, ChainDifficulty, Coin,
MonadUtxoRead (..), Tx (..), TxAux,
TxDistribution (..), TxId, TxIn (..),
TxInWitness (..), TxOut (..), TxOutAux,
TxSigData, TxWitness, Utxo, UtxoStateT (..),
applyTxToUtxo, blockTxas, difficultyL,
filterUtxoByAddr, makePubKeyAddress,
makeScriptAddress, mkCoin, sumCoins,
topsortTxs)
import Pos.Types.Coin (unsafeIntegerToCoin, unsafeSubCoin)
type TxOutIdx = (TxId, Word32)
type TxInputs = [TxOutIdx]
type TxOutputs = [TxOutAux]
type TxError = Text
-----------------------------------------------------------------------------
-- Tx creation
-----------------------------------------------------------------------------
-- | Generic function to create a transaction, given desired inputs, outputs and a
-- way to construct witness from signature data
makeAbstractTx :: (TxSigData -> TxInWitness) -> TxInputs -> TxOutputs -> TxAux
makeAbstractTx mkWit inputs outputs = (Tx {..}, txWitness, txDist)
where txInputs = map makeTxIn inputs
txOutputs = map fst outputs
txAttributes = mkAttributes ()
txOutHash = hash txOutputs
txDist = TxDistribution (map snd outputs)
txDistHash = hash txDist
txWitness = V.fromList $ map (mkWit . makeTxSigData) inputs
makeTxIn (txInHash, txInIndex) = TxIn {..}
makeTxSigData (txInHash, txInIndex) = (txInHash, txInIndex, txOutHash, txDistHash)
-- | Makes a transaction which use P2PKH addresses as a source
makePubKeyTx :: SecretKey -> TxInputs -> TxOutputs -> TxAux
makePubKeyTx sk = makeAbstractTx mkWit
where pk = toPublic sk
mkWit sigData = PkWitness
{ twKey = pk
, twSig = sign sk sigData
}
makeMOfNTx :: Script -> [Maybe SecretKey] -> TxInputs -> TxOutputs -> TxAux
makeMOfNTx validator sks = makeAbstractTx mkWit
where mkWit sigData = ScriptWitness
{ twValidator = validator
, twRedeemer = multisigRedeemer sigData sks
}
type FlatUtxo = [(TxOutIdx, TxOutAux)]
type InputPicker = StateT (Coin, FlatUtxo) (Either TxError)
-- | Given Utxo, desired source address and desired outputs, prepare lists
-- of correct inputs and outputs to form a transaction
prepareInpOuts :: Utxo -> Address -> TxOutputs -> Either TxError (TxInputs, TxOutputs)
prepareInpOuts utxo addr outputs = do
futxo <- evalStateT (pickInputs []) (totalMoney, sortedUnspent)
let inputs = map fst futxo
inputSum = unsafeIntegerToCoin $
sumCoins $ map (txOutValue . fst . snd) futxo
newOuts
| inputSum > totalMoney =
(TxOut addr (inputSum `unsafeSubCoin` totalMoney), [])
: outputs
| otherwise = outputs
pure (inputs, newOuts)
where
totalMoney = unsafeIntegerToCoin $
sumCoins $ map (txOutValue . fst) outputs
allUnspent = M.toList $ filterUtxoByAddr addr utxo
sortedUnspent = sortBy (comparing $ Down . txOutValue . fst . snd) allUnspent
pickInputs :: FlatUtxo -> InputPicker FlatUtxo
pickInputs inps = do
moneyLeft <- use _1
if moneyLeft == mkCoin 0
then return inps
else do
mNextOut <- head <$> use _2
case mNextOut of
Nothing -> fail "Not enough money to send!"
Just inp@(_, (TxOut{..}, _)) -> do
_1 %= unsafeSubCoin (min txOutValue moneyLeft)
_2 %= tail
pickInputs (inp : inps)
-- | Make a multi-transaction using given secret key and info for outputs
createTx :: Utxo -> SecretKey -> TxOutputs -> Either TxError TxAux
createTx utxo sk outputs =
uncurry (makePubKeyTx sk) <$>
prepareInpOuts utxo (makePubKeyAddress $ toPublic sk) outputs
-- | Make a transaction, using M-of-N script as a source
createMOfNTx :: Utxo -> [(PublicKey, Maybe SecretKey)] -> TxOutputs -> Either TxError TxAux
createMOfNTx utxo keys outputs = uncurry (makeMOfNTx validator sks) <$> inpOuts
where pks = map fst keys
sks = map snd keys
m = length $ filter isJust sks
validator = multisigValidator m pks
addr = makeScriptAddress validator
inpOuts = prepareInpOuts utxo addr outputs
----------------------------------------------------------------------
-- Deduction of history
----------------------------------------------------------------------
-- | Check if given 'Address' is one of the receivers of 'Tx'
hasReceiver :: Tx -> Address -> Bool
hasReceiver Tx {..} addr = any ((== addr) . txOutAddress) txOutputs
-- | Given some 'Utxo', check if given 'Address' is one of the senders of 'Tx'
hasSender :: MonadUtxoRead m => Tx -> Address -> m Bool
hasSender Tx {..} addr = anyM hasCorrespondingOutput txInputs
where hasCorrespondingOutput txIn =
fmap toBool $ fmap ((== addr) . txOutAddress . fst) <$> utxoGet txIn
toBool Nothing = False
toBool (Just b) = b
-- | Datatype for returning info about tx history
data TxHistoryEntry = THEntry
{ _thTxId :: !TxId
, _thTx :: !Tx
, _thIsOutput :: !Bool
, _thDifficulty :: !(Maybe ChainDifficulty)
} deriving (Show, Eq, Generic)
makeLenses ''TxHistoryEntry
-- | Type of monad used to deduce history
type TxSelectorT m = UtxoStateT (MaybeT m)
-- | Select transactions related to given address. `Bool` indicates
-- whether the transaction is outgoing (i. e. is sent from given address)
getRelatedTxs
:: Monad m
=> Address
-> [(WithHash Tx, TxWitness, TxDistribution)]
-> TxSelectorT m [TxHistoryEntry]
getRelatedTxs addr txs = fmap DL.toList $
lift (MaybeT $ return $ topsortTxs (view _1) txs) >>=
foldlM step DL.empty
where
step ls (WithHash tx txId, _wit, dist) = do
let isIncoming = tx `hasReceiver` addr
isOutgoing <- tx `hasSender` addr
if isOutgoing || isIncoming
then do
applyTxToUtxo (WithHash tx txId) dist
identity %= filterUtxoByAddr addr
return $ ls <> DL.singleton (THEntry txId tx isOutgoing Nothing)
else return ls
-- | Given a full blockchain, derive address history and Utxo
-- TODO: Such functionality will still be useful for merging
-- blockchains when wallet state is ready, but some metadata for
-- Tx will be required.
deriveAddrHistory
-- :: (Monad m, Ssc ssc) => Address -> [Block ssc] -> TxSelectorT m [TxHistoryEntry]
:: (Monad m) => Address -> [Block ssc] -> TxSelectorT m [TxHistoryEntry]
deriveAddrHistory addr chain = identity %= filterUtxoByAddr addr >>
deriveAddrHistoryPartial [] addr chain
deriveAddrHistoryPartial
-- :: (Monad m ssc)
:: (Monad m)
=> [TxHistoryEntry]
-> Address
-> [Block ssc]
-> TxSelectorT m [TxHistoryEntry]
deriveAddrHistoryPartial hist addr chain =
DL.toList <$> foldrM updateAll (DL.fromList hist) chain
where
updateAll (Left _) hst = pure hst
updateAll (Right blk) hst = do
txs <- getRelatedTxs addr $
map (over _1 withHash) (blk ^. blockTxas)
let difficulty = blk ^. difficultyL
txs' = map (thDifficulty .~ Just difficulty) txs
return $ DL.fromList txs' <> hst