bitcoind-regtest-0.1.0.0: src/Bitcoin/Core/Regtest.hs
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
module Bitcoin.Core.Regtest
(
-- * Run an ephemeral regtest node
NodeHandle (..)
, runBitcoind
, withBitcoind
-- * Funding
, oneBitcoin
, createOutput
, generate
, spendPackageOutputs
-- * Internal wallet
--
-- | In the following lists, entries correspond to each other e.g. the p2pkh
-- address for @keys !! 1@ is @addrs !! 1@.
, xprv
, keys
, pubKeys
, addrs
, textAddrs
) where
import Control.Concurrent (threadDelay)
import Control.Exception (bracket)
import Control.Monad (void)
import qualified Data.Serialize as S
import Data.Text (Text)
import Data.Word (Word64)
import Network.Haskoin.Address (Address, addrToString,
addressToOutput)
import Network.Haskoin.Block (blockTxns)
import Network.Haskoin.Constants (btcTest)
import Network.Haskoin.Crypto (SecKey)
import Network.Haskoin.Keys (PubKeyI, XPrvKey (..),
deriveAddrs, derivePubKeyI,
deriveXPubKey, makeXPrvKey,
prvSubKeys, wrapSecKey)
import Network.Haskoin.Script (sigHashAll)
import Network.Haskoin.Transaction (OutPoint (..), SigInput (..),
Tx (..), TxOut (..), buildAddrTx,
signTx, txHash)
import Network.Haskoin.Util (encodeHex, maybeToEither)
import Network.HTTP.Client (Manager)
import Servant.API (BasicAuthData)
import System.IO (Handle, IOMode (..), openFile)
import System.IO.Temp (withSystemTempDirectory)
import System.Process (CreateProcess (..), ProcessHandle,
StdStream (..), createProcess,
proc, terminateProcess,
waitForProcess)
import Bitcoin.Core.RPC (BitcoindClient, BitcoindException,
basicAuthFromCookie)
import qualified Bitcoin.Core.RPC as RPC
-- | Data needed to connect to a @bitcoind-regtest@ ephemeral node
data NodeHandle = NodeHandle
{ nodePort :: Int
, nodeAuth :: BasicAuthData
}
-- | Run an RPC computation with an ephemeral node
runBitcoind :: Manager -> NodeHandle -> BitcoindClient r -> IO (Either BitcoindException r)
runBitcoind mgr (NodeHandle port auth) = RPC.runBitcoind mgr "127.0.0.1" port auth
-- | Provide bracketed access to a fresh ephemeral node
withBitcoind
:: Int
-- ^ node port
-> (NodeHandle -> IO r)
-> IO r
withBitcoind port k = withSystemTempDirectory "bitcoind-rpc-tests" $ \dd ->
bracket (initBitcoind dd port) stopBitcoind . const $ do
auth <- basicAuthFromCookie $ dd <> "/regtest/.cookie"
k $ NodeHandle port auth
initBitcoind :: FilePath -> Int -> IO ProcessHandle
initBitcoind ddir port = do
logH <- openFile "/tmp/bitcoind-rpc.log" WriteMode
(_, _, _, h) <- createProcess $ bitcoind ddir port logH
h <$ threadDelay 1_000_000
stopBitcoind :: ProcessHandle -> IO ()
stopBitcoind h = terminateProcess h >> void (waitForProcess h)
bitcoind :: FilePath -> Int -> Handle -> CreateProcess
bitcoind ddir port output
= (proc "bitcoind" args)
{ std_out = UseHandle output
, std_err = UseHandle output
}
where
args = ["-regtest", "-txindex", "-disablewallet", "-datadir=" <> ddir, "-rpcport=" <> show port]
oneBitcoin :: Word64
oneBitcoin = 100_000_000
-- | Funds an output with the minimum of the given amount and 100 blocks of subsidies
createOutput
:: Address
-- ^ address for the newly created output
-> Word64
-- ^ target amount
-> BitcoindClient (OutPoint, Word64)
createOutput addr vTarget = do
inputs <- generateEnoughBlocks vTarget
-- Make mined outputs spendable
RPC.generateToAddress 100 textAddr2 Nothing
let Right (tx, vFund) = spendPackageOutputs inputs addr vTarget
h <- RPC.sendRawTransaction (encodeHex $ S.encode tx) Nothing
RPC.generateToAddress 6 textAddr2 Nothing
return (OutPoint h 0, vFund)
generateEnoughBlocks :: Word64 -> BitcoindClient [(OutPoint, Word64)]
generateEnoughBlocks vTarget = go ([], 0, 0 :: Int)
where
go (xs, v, n)
| n >= 100 = return xs
| otherwise = do
x@(_, v0) <- generate
let xs' = x : xs
if v + v0 >= vTarget
then return xs'
else go (xs', v + v0, n+1)
-- | A simplified block generator which does not require the tester to manage a
-- wallet. Use 'spendPackageOutputs' to spend.
generate :: BitcoindClient (OutPoint, Word64)
generate
= fmap (processCoinbase . head . blockTxns)
$ RPC.generateToAddress 1 textAddr0 Nothing >>= RPC.getBlock . head
where
processCoinbase tx0 = (OutPoint (txHash tx0) 0 , outValue . head $ txOut tx0)
-- | Spend outputs created by 'generate'
spendPackageOutputs
:: [(OutPoint, Word64)]
-- ^ outputs produced by 'generate'
-> Address
-- ^ recipient address
-> Word64
-- ^ amount to spend
-> Either String (Tx, Word64)
spendPackageOutputs inputs addr vTarget = do
addrText <- maybeToEither "Addr conversion failed" $ addrToString btcTest addr
let outSpec | vTarget + 10_000 < vAvail
= Right ([(addrText, vTarget), (textAddr1, vAvail - vTarget - 10_000)], vTarget)
| vAvail > 10_000
= Right ([(addrText, vAvail - 10_000)], vAvail - 10_000)
| otherwise
= Left "Insufficient funds"
vAvail = sum $ snd <$> inputs
sigIn (op, val) = SigInput (addressToOutput addr0) val op sigHashAll Nothing
(outs, vFund) <- outSpec
txSpec <- buildAddrTx btcTest (fst <$> inputs) outs
tx <- signTx btcTest txSpec (sigIn <$> inputs) [key0]
return (tx, vFund)
-- | Root key for the package wallet
xprv :: XPrvKey
xprv = makeXPrvKey "bitcoind-regtest key seed"
-- | Example secret keys
keys :: [SecKey]
keys = xPrvKey . fst <$> prvSubKeys xprv 0
-- | Example public keys
pubKeys :: [PubKeyI]
pubKeys = derivePubKeyI . wrapSecKey True <$> keys
key0 :: SecKey
key0 : _ = keys
-- | Example p2pkh addresses
addrs :: [Address]
addrs = repack <$> deriveAddrs (deriveXPubKey xprv) 0
where repack (x, _, _) = x
addr0 :: Address
addr0 : _ = addrs
-- | Text versions of the example addresses
textAddrs :: [Text]
textAddrs = addrToString' <$> addrs
where addrToString' a = let Just x = addrToString btcTest a in x
textAddr0, textAddr1, textAddr2 :: Text
textAddr0 : textAddr1 : textAddr2 : _ = textAddrs