{-| Module : EasyBitcoin Description : Libray to parse and compose bitcoin transactions, keys, addresses and escrows. Copyright : (c) Alejandro Durán Pallarés, 2015 License : BSD3 Maintainer : vwwv@correo.ugr.es Stability : experimental EasyBitcoin is a simple haskell library providing types and class-instances for bitcoin related code; it also include an small set of functions to handle addresses, transactions and escrows. Some small examples can be found <https://github.com/vwwv/easy-bitcoin/tree/master/Examples here> . -} {-# LANGUAGE DataKinds, GADTs #-} module Network.EasyBitcoin( -- * Usage Example: -- $example -- * Addresses: Address() , Addressable(..) , isPay2SH , isPay2PKH , addressFromUncompressed -- * Keys: , Key() , Visibility(..) , derive , derivePublic , deriveHardened , deriveRoot , showAsCompressedSingletonKey , showAsUncompressedSingletonKey , serializeCompressedSingleton , serializeUncompressedSingleton , (===) -- * Transactions: , Outpoint (..) , Txid() , txid , Tx () , transaction , unsignedTransaction , txOutputs , txInputs -- * Escrows and Signatures: , RedeemScript(..) , ScriptSig() , SigHash(..) , TxSignature() , sigHashType , signTxAt , scriptSig , escrowSignatures , escrowSignaturesFor , simpleSignature , checkInput , checkSignatureAt , createSignature , createSignatureAs , BTC() , btc , mBTC , satoshis , asBtc , asMbtc , asSatoshis , showAsBtc , showAsMbtc , showAsSatoshis -- * Network Parameters: , ProdNet , TestNet , BlockNetwork(..) , Params(..) ) where import Network.EasyBitcoin.Address ( Address , Addressable(..) , isPay2SH , isPay2PKH , addressFromUncompressed ) import Network.EasyBitcoin.Script ( RedeemScript(..) ) import Network.EasyBitcoin.NetworkParams( ProdNet , TestNet , BlockNetwork(..) , Params(..) ) import Network.EasyBitcoin.Internal.Transaction ( Outpoint (..) , Txid , txid , Tx() , sigHashType ) import Network.EasyBitcoin.Transaction ( transaction , txOutputs , txInputs , unsignedTransaction , ScriptSig() , TxSignature() , signTxAt , scriptSig , escrowSignatures , escrowSignaturesFor , simpleSignature , checkInput , checkSignatureAt , createSignature , createSignatureAs , SigHash(..) ) import Network.EasyBitcoin.Keys ( Key() , Visibility(..) , derive , derivePublic , deriveHardened , deriveRoot , (===) , showAsCompressedSingletonKey , showAsUncompressedSingletonKey , serializeCompressedSingleton , serializeUncompressedSingleton ) import Network.EasyBitcoin.BitcoinUnits( BTC() , btc , mBTC , satoshis , asBtc , asMbtc , asSatoshis , showAsBtc , showAsMbtc , showAsSatoshis ) {- $example As a toy example, let's imagine the following scenario: * On a blog, there's a donation address @mm8LjcoUYdPNKgWshGs7dueFu33aK56ckb@ (private key @__91vaDsoxZACAZeGM89Y7dBnbTB7wrvtBeEkMTpL2sCgEtHf4RBn__@). * The blog is written by blogger A, who wrote __70%__ of the posts, and blogger B, who wrote the remaining __30%__. * They want split the donation proportionally to the number of posts they have written. * Blogger A has as personal address @__miHWju2dzq9RcUPESzYBgVWa3W3swTXtLo__@ . * Blogger B has as personal address @__mvsXpubWQSw2dK2L85iYFppnNjGm439aWK__@ . As this is an example, we won't use real bitcoin, but testnet bitcoin, also, we'll use Coinbase's public bitcoin client, Toshi, so we don't have to install anything in our computer: @ {-# LANGUAGE DataKinds, OverloadedStrings #-} import Network.EasyBitcoin import Control.Monad(forever) import Network.HTTP.Client(HttpException(StatusCodeException)) import Network.Wreq(get,post,statusCode,responseBody) import Control.Exception(handleJust) import Control.Lens import Data.Aeson.Lens import Data.Aeson import Safe import Control.Applicative import Control.Monad import Control.Concurrent ---------------------------------------------------------------------------------------------- incoming = read "91vaDsoxZACAZeGM89Y7dBnbTB7wrvtBeEkMTpL2sCgEtHf4RBn" :: Key Private TestNet outgoingA = read "miHWju2dzq9RcUPESzYBgVWa3W3swTXtLo" :: Address TestNet outgoingB = read "mvsXpubWQSw2dK2L85iYFppnNjGm439aWK" :: Address TestNet ---------------------------------------------------------------------------------------------- fee = btc 0.0001 -- the miner fee to use. threshold = btc 0.2 -- won't send any transaction till reach this amount. This is important -- to avoid creating "dust" transactions. server = "https://testnet3.toshi.io/" -- The Coinbase Toshi client testnet url. secondsToPool = 20 ---------------------------------------------------------------------------------------------- -- General workflow: -- each 20 seconds: -- - read from Toshi all unspent outpoints. -- - if not enough funds holds on the unspent outpoints: -- continue next iteration. -- - else: -- combining all available outpoints into a transaction to miHWju2dzq9RcUPESzYBgVWa3W3swTXtLo and mvsXpubWQSw2dK2L85iYFppnNjGm439aWK -- send this transaction to Toshi to be broad-casted into the network. main::IO () main = do putStrLn $ "Rebrodcasting from " ++ show (address incoming) ++ " to "++ show outgoingA ++ " and " ++ show outgoingB forever $ do readToshi >>= maybe (return ()) sendToshi . createTransaction threadDelay (secondsToPool*1000000) -- If not enough funds, returns Nothing, otherwise, returns the transaction to be sent. createTransaction :: [(Outpoint, BTC TestNet)] -> Maybe (Tx TestNet) createTransaction inputs = if amount > threshold then Just txToSend else Nothing where amount = sum (fmap snd inputs) - fee amountToA = btc (asBtc amount * 0.7) amountToB = amount - amountToA txToSend = transaction [ (outpoint,incoming) | (outpoint, _ ) <- inputs] (outgoingA,amountToA) [(outgoingB,amountToB)] sendToshi :: Tx TestNet -> IO () sendToshi tx = do putStrLn $ "Sending tx: " ++ show (txid tx) post (server ++ "/api/v0/transactions") (toJSON$show tx) return () -- Querying and parsing the Toshi client about the unspent_outputs holds on the address defined by the private key -- 91vaDsoxZACAZeGM89Y7dBnbTB7wrvtBeEkMTpL2sCgEtHf4RBn (that is mm8LjcoUYdPNKgWshGs7dueFu33aK56ckb). readToshi :: IO [(Outpoint, BTC TestNet)] readToshi = handleJust isNotFound (const$ return []) $ do body <- get $ server ++ "api/v0/addresses/"++ show (address incoming) ++ "/unspent_outputs" return $ body ^.. responseBody . values . to parseOutpoint . _Just where -- Toshi returns 404 if the address has never received any tx isNotFound ex = case ex of StatusCodeException s _ _ | s ^. statusCode == 404 -> Just () _ -> Nothing parseOutpoint val = do vout <- (val ^? key "output_index" ._JSON) txid <- (val ^? key "transaction_hash" ._JSON. to readMay._Just) amount <- (val ^? key "amount" ._JSON. to satoshis) Just (Outpoint txid vout, amount) @ -}