{-# 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."