packages feed

cardano-addresses-4.0.0: lib/Command/Key/WalletId.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeSynonymInstances #-}

{-# OPTIONS_HADDOCK hide #-}

-- |
-- Copyright: 2020 Input Output (Hong Kong) Ltd., 2021-2022 Input Output Global Inc. (IOG), 2023-2025 Intersect
-- License: Apache-2.0

module Command.Key.WalletId
    ( Cmd (..)
    , mod
    , run
    ) where

import Prelude hiding
    ( mod )

import Cardano.Address.Derivation
    ( hashWalletId, toXPub, xprvFromBytes, xpubToBytes )
import Cardano.Address.Script
    ( Cosigner, Script )
import Cardano.Address.Style.Shared
    ( sharedWalletId )
import Codec.Binary.Encoding
    ( AbstractEncoding (..) )
import Control.Monad
    ( when )
import Data.Maybe
    ( fromJust, isNothing )
import Data.Text
    ( Text )
import Options.Applicative
    ( CommandFields
    , Mod
    , command
    , footerDoc
    , helper
    , info
    , optional
    , progDesc
    )
import Options.Applicative.Help.Pretty
    ( pretty )
import Options.Applicative.Script
    ( scriptTemplateSpendingArg, scriptTemplateStakingArg )
import System.IO
    ( stdin, stdout )
import System.IO.Extra
    ( hGetBech32, hPutBytes )

import qualified Cardano.Codec.Bech32.Prefixes as CIP5
import qualified Data.ByteString as BS

data Cmd = WalletId
    { spending :: Maybe (Script Cosigner)
    , staking :: Maybe (Script Cosigner)
    } deriving (Show)

mod :: (Cmd -> parent) -> Mod CommandFields parent
mod liftCmd = command "walletid" $
    info (helper <*> fmap liftCmd parser) $ mempty
        <> progDesc "Shows the cardano-wallet wallet ID for a given key"
        <> footerDoc (Just $ pretty $ mconcat
            [ "A wallet ID is a 40-digit hexadecimal string derived " :: Text
            , "from the wallet’s key. It is used by the cardano-wallet "
            , "server to refer to specific wallets.\n\n"
            , "For shelley wallets the key can be either an extended root key "
            , "(full multi-account wallets), or an extended account key "
            , "(single-account wallets).\n\n"
            , "In the latter case either private or public extended key is accepted "
            , "-- they will have the same wallet ID.\n\n"
            , "The bech32-encoded key is read from standard input.\n\n"
            , "In case of a shared wallet, the wallet id is calculated based on "
            , "private extended account key, payment template script and "
            , "staking template script - if specified. Each signature in "
            , "any template script is denoted by cosigner#number.\n"
            ])
  where
    parser = WalletId
        <$> optional scriptTemplateSpendingArg
        <*> optional scriptTemplateStakingArg

run :: Cmd -> IO ()
run WalletId{spending,staking} = do
    (hrp, bytes) <- hGetBech32 stdin allowedPrefixes
    guardBytes hrp bytes
    let bs = payloadToHash hrp bytes
    when ( hrp `elem` [CIP5.acct_shared_xvk, CIP5.acct_shared_xsk] &&
           isNothing spending ) $
        fail "shared wallet needs to have at least spending script specified"
    let walletid =
            if hrp `elem` [CIP5.acct_shared_xvk, CIP5.acct_shared_xsk] then
                sharedWalletId bs (fromJust spending) staking
            else
                hashWalletId bs
    hPutBytes stdout walletid EBase16
  where
    allowedPrefixes =
        [ CIP5.root_xsk
        , CIP5.root_xvk
        , CIP5.acct_xvk
        , CIP5.acct_xsk
        , CIP5.acct_shared_xvk
        , CIP5.acct_shared_xsk
        ]

    payloadToHash hrp bs
        | hrp `elem` [CIP5.root_xsk, CIP5.acct_xsk, CIP5.acct_shared_xsk] =
              case xprvFromBytes bs of
                  Just xprv ->  xpubToBytes . toXPub $ xprv
                  Nothing -> "96-byte extended private key is invalid due to 'scalarDecodeLong' failure from cryptonite"
        | otherwise =
              bs

    guardBytes hrp bytes
        | hrp `elem` [CIP5.root_xsk, CIP5.root_shared_xsk, CIP5.acct_xsk, CIP5.acct_shared_xsk] = do
            when (BS.length bytes /= 96) $
                fail "data should be a 96-byte private key."

        | otherwise = do
            when (BS.length bytes /= 64) $
                fail "data should be a 32-byte public key with a 32-byte chain-code appended."