Skip to content

Commit

Permalink
Allow to generate reward account addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
hasufell committed Aug 19, 2020
1 parent 2e2636a commit 2a5ab6c
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 4 deletions.
8 changes: 6 additions & 2 deletions command-line/cardano-addresses-cli.cabal
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
cabal-version: 1.12

-- This file has been generated from package.yaml by hpack version 0.31.2.
-- This file has been generated from package.yaml by hpack version 0.33.0.
--
-- see: https://github.com/sol/hpack
--
-- hash: 3737cc69d18f00a50e3c375be8e4be7728f2315689dc3bc92200f54e3c79c5b6
-- hash: 23602f11d7fb1ac8c8333e3d2d3c95385ea53972cbde55027a9b540b975d71c2

name: cardano-addresses-cli
version: 1.0.0
Expand Down Expand Up @@ -38,6 +38,7 @@ library
Command.Address.Inspect
Command.Address.Payment
Command.Address.Pointer
Command.Address.Reward
Command.Key
Command.Key.Child
Command.Key.FromRecoveryPhrase
Expand Down Expand Up @@ -68,7 +69,9 @@ library
, cardano-addresses
, cardano-crypto
, code-page
, exceptions
, extra
, fmt
, optparse-applicative
, safe
, text
Expand Down Expand Up @@ -102,6 +105,7 @@ test-suite unit
Command.Address.InspectSpec
Command.Address.PaymentSpec
Command.Address.PointerSpec
Command.Address.RewardSpec
Command.Key.ChildSpec
Command.Key.FromRecoveryPhraseSpec
Command.Key.InspectSpec
Expand Down
4 changes: 4 additions & 0 deletions command-line/lib/Command/Address.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import qualified Command.Address.Delegation as Delegation
import qualified Command.Address.Inspect as Inspect
import qualified Command.Address.Payment as Payment
import qualified Command.Address.Pointer as Pointer
import qualified Command.Address.Reward as Reward


data Cmd
Expand All @@ -37,6 +38,7 @@ data Cmd
| Delegation Delegation.Cmd
| Pointer Pointer.Cmd
| Inspect Inspect.Cmd
| Reward Reward.Cmd
deriving (Show)

mod :: (Cmd -> parent) -> Mod CommandFields parent
Expand All @@ -57,6 +59,7 @@ mod liftCmd = command "address" $
, Delegation.mod Delegation
, Pointer.mod Pointer
, Inspect.mod Inspect
, Reward.mod Reward
]

run :: Cmd -> IO ()
Expand All @@ -66,3 +69,4 @@ run = \case
Delegation sub -> Delegation.run sub
Pointer sub -> Pointer.run sub
Inspect sub -> Inspect.run sub
Reward sub -> Reward.run sub
90 changes: 90 additions & 0 deletions command-line/lib/Command/Address/Reward.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

{-# OPTIONS_HADDOCK hide #-}

module Command.Address.Reward
( Cmd
, mod
, run
) where

import Prelude hiding
( mod )

import Cardano.Address
( NetworkTag (..), bech32With )
import Cardano.Address.Style.Shelley
( mkNetworkDiscriminant, shelleyMainnet, shelleyTestnet )
import Codec.Binary.Bech32
( humanReadablePartFromText )
import Control.Monad.Catch
( throwM )
import Fmt
( build, fmt )
import Options.Applicative
( CommandFields, Mod, command, footerDoc, header, helper, info, progDesc )
import Options.Applicative.Discrimination
( networkTagOpt )
import Options.Applicative.Help.Pretty
( bold, indent, string, vsep )
import Options.Applicative.Style
( Style (..) )
import System.Exit
( die )
import System.IO
( stdin, stdout )
import System.IO.Extra
( hGetXPub, progName )

import qualified Cardano.Address.Style.Shelley as Shelley
import qualified Data.ByteString.Char8 as B8
import qualified Data.Text.Encoding as T


newtype Cmd = Cmd
{ networkTag :: NetworkTag
} deriving (Show)

mod :: (Cmd -> parent) -> Mod CommandFields parent
mod liftCmd = command "stake" $
info (helper <*> fmap liftCmd parser) $ mempty
<> progDesc "Create a stake address"
<> header "Create a stake address \
\that references a staking key (1-1)."
<> footerDoc (Just $ vsep
[ string "The public key is read from stdin."
, string ""
, string "Example:"
, indent 2 $ bold $ string $ "$ "<>progName<>" recovery-phrase generate --size 15 \\"
, indent 4 $ bold $ string $ "| "<>progName<>" key from-recovery-phrase Shelley > root.prv"
, indent 2 $ string ""
, indent 2 $ bold $ string "$ cat root.prv \\"
, indent 4 $ bold $ string $ "| "<>progName<>" key child 1852H/1815H/0H/2/0 > stake.prv"
, indent 2 $ string ""
, indent 2 $ bold $ string "$ cat stake.prv \\"
, indent 4 $ bold $ string $ "| "<>progName<>" key public \\"
, indent 4 $ bold $ string $ "| "<>progName<>" address stake --network-tag 0"
, indent 2 $ string "stake1uqly0fjvrgguywze067gwhsexggtj8rrdnxczgp5vexe8zgxqns3g"
])
where
parser = Cmd
<$> networkTagOpt Shelley

run :: Cmd -> IO ()
run Cmd{networkTag} = do
xpub <- hGetXPub stdin
hpr <- hprFromNetTag
case (mkNetworkDiscriminant . fromIntegral . unNetworkTag) networkTag of
Left e -> die (fmt $ build e)
Right discriminant -> do
let addr = Shelley.stakeAddress discriminant (Shelley.liftXPub xpub)
B8.hPutStr stdout $ T.encodeUtf8 $ bech32With hpr addr
where
hprFromText = either throwM pure . humanReadablePartFromText
hprFromNetTag
| shelleyMainnet == networkTag = hprFromText "stake"
| shelleyTestnet == networkTag = hprFromText "stake_test"
| otherwise = hprFromText "addr"


2 changes: 2 additions & 0 deletions command-line/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ library:
- cardano-addresses
- cardano-crypto
- code-page
- exceptions
- extra
- fmt
- optparse-applicative
- safe
- text
Expand Down
57 changes: 57 additions & 0 deletions command-line/test/Command/Address/RewardSpec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{-# LANGUAGE FlexibleContexts #-}

module Command.Address.RewardSpec
( spec
) where

import Prelude

import Test.Hspec
( Spec, SpecWith, it, shouldBe, shouldContain )
import Test.Utils
( cli, describeCmd )

spec :: Spec
spec = describeCmd [ "address", "stake" ] $ do
specShelley defaultPhrase "1852H/1815H/0H/2/0" 0
"stake_test1ura3dk68y6echdmfmnvm8mej8u5truwv8ufmv830w5a45tcsfhtt2"

specShelley defaultPhrase "1852H/1815H/0H/2/0" 3
"addr1u0a3dk68y6echdmfmnvm8mej8u5truwv8ufmv830w5a45tc3kzy0t"

specMalformedNetwork "💩"

specInvalidNetwork "42"

specShelley :: [String] -> String -> Int -> String -> SpecWith ()
specShelley phrase path networkTag want = it ("golden shelley (payment) " <> path) $ do
out <- cli [ "key", "from-recovery-phrase", "shelley" ] (unwords phrase)
>>= cli [ "key", "child", path ]
>>= cli [ "key", "public" ]
>>= cli [ "address", "stake", "--network-tag", show networkTag ]
out `shouldBe` want

specMalformedNetwork :: String -> SpecWith ()
specMalformedNetwork networkTag = it ("malformed network " <> networkTag) $ do
(out, err) <- cli [ "key", "from-recovery-phrase", "shelley" ] (unwords defaultPhrase)
>>= cli [ "key", "public" ]
>>= cli [ "address", "stake", "--network-tag", networkTag ]
out `shouldBe` ""
err `shouldContain` "Invalid network tag"
err `shouldContain` "Usage"

specInvalidNetwork :: String -> SpecWith ()
specInvalidNetwork networkTag = it ("invalid network " <> networkTag) $ do
(out, err) <- cli [ "key", "from-recovery-phrase", "shelley" ] (unwords defaultPhrase)
>>= cli [ "key", "public" ]
>>= cli [ "address", "stake", "--network-tag", networkTag ]
out `shouldBe` ""
err `shouldContain` "Invalid network tag"

defaultPhrase :: [String]
defaultPhrase =
[ "pole", "pulse", "wolf", "blame", "chronic"
, "ship", "vivid", "tree", "small", "onion"
, "host", "accident", "burden", "lazy", "swarm"
]

11 changes: 11 additions & 0 deletions core/lib/Cardano/Address.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Cardano.Address
( -- * Address
Address
, PaymentAddress (..)
, StakeAddress (..)
, DelegationAddress (..)
, PointerAddress (..)
, ChainPointer (..)
Expand Down Expand Up @@ -121,6 +122,16 @@ fromBech32 :: Text -> Maybe Address
fromBech32 =
eitherToMaybe . fmap unsafeMkAddress. E.fromBech32 (const id) . T.encodeUtf8

-- | Encoding of addresses for certain key types and backend targets.
--
-- @since 2.0.0
class HasNetworkDiscriminant key => StakeAddress key where
-- | Convert a staking key to a stake 'Address' (aka: reward account address)
-- valid for the given network discrimination.
--
-- @since 2.0.0
stakeAddress :: NetworkDiscriminant key -> key 'StakingK XPub -> Address

-- | Encoding of addresses for certain key types and backend targets.
--
-- @since 1.0.0
Expand Down
45 changes: 43 additions & 2 deletions core/lib/Cardano/Address/Style/Shelley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module Cardano.Address.Style.Shelley
, paymentAddress
, delegationAddress
, pointerAddress
, stakeAddress
, extendAddress
, ErrExtendAddress (..)

Expand Down Expand Up @@ -127,6 +128,8 @@ import Data.Word
( Word32 )
import Data.Word7
( getVariableLengthNat, putVariableLengthNat )
import Fmt
( Buildable, build, (+|), (|+) )
import GHC.Generics
( Generic )

Expand Down Expand Up @@ -374,6 +377,23 @@ deriveStakingPrivateKey =
-- > bech32 $ pointerAddress tag (toXPub <$> addrK) ptr
-- > "addr1gxpfffuj3zkp5g7ct6h4va89caxx9ayq2gvkyfvww48sdnmmqypqfcp5um"

instance Internal.StakeAddress Shelley where
stakeAddress discrimination k = unsafeMkAddress $
invariantSize expectedLength $ BL.toStrict $ runPut $ do
putWord8 firstByte
putByteString (blake2b224 k)
where
-- First 4 bits are `1110` for keyhash28 reward account address.
-- Next 4 bits are network discriminator.
-- `1110 0000` is 224 in decimal.
firstByte =
let addrType = 224
netTagLimit = 16
in addrType + invariantNetworkTag netTagLimit (networkTag @Shelley discrimination)
expectedLength =
let headerSizeBytes = 1
in headerSizeBytes + pubkeyHashSize

instance Internal.PaymentAddress Shelley where
paymentAddress discrimination k = unsafeMkAddress $
invariantSize expectedLength $ BL.toStrict $ runPut $ do
Expand All @@ -389,8 +409,12 @@ instance Internal.PaymentAddress Shelley where
-- will be `0110`. The next for 4 bits are reserved for network discriminator.
-- `0110 0000` is 96 in decimal.
firstByte =
96 + invariantNetworkTag 16 (networkTag @Shelley discrimination)
expectedLength = 1 + pubkeyHashSize
let addrType = 96
netTagLimit = 16
in addrType + invariantNetworkTag netTagLimit (networkTag @Shelley discrimination)
expectedLength =
let headerSizeBytes = 1
in headerSizeBytes + pubkeyHashSize

instance Internal.DelegationAddress Shelley where
delegationAddress discrimination paymentKey =
Expand Down Expand Up @@ -617,6 +641,20 @@ pointerAddress
pointerAddress =
Internal.pointerAddress

-- Re-export from 'Cardano.Address' to have it documented specialized in Haddock.
--
-- | Convert a staking key to a stake Address (aka reward account address)
-- for the given network discrimination.
--
-- @since 2.0.0
stakeAddress
:: NetworkDiscriminant Shelley
-> Shelley 'StakingK XPub
-> Address
stakeAddress =
Internal.stakeAddress


--
-- Network Discriminant
--
Expand All @@ -634,6 +672,9 @@ newtype MkNetworkDiscriminantError
-- ^ Wrong network tag.
deriving (Eq, Show)

instance Buildable MkNetworkDiscriminantError where
build (ErrWrongNetworkTag i) = "Invalid network tag "+|i|+". Must be between [0, 15]"

-- | Construct 'NetworkDiscriminant' for Cardano 'Shelley' from a number.
-- If the number is invalid, ie., not between 0 and 15, then
-- 'MkNetworkDiscriminantError' is thrown.
Expand Down

0 comments on commit 2a5ab6c

Please sign in to comment.