Skip to content

Commit

Permalink
Merge #3599
Browse files Browse the repository at this point in the history
3599: Bech32 support in a number of multisig endpoints r=paweljakubas a=paweljakubas

<!--
Detail in a few bullet points the work accomplished in this PR.

Before you submit, don't forget to:

* Make sure the GitHub PR fields are correct:
   ✓ Set a good Title for your PR.
   ✓ Assign yourself to the PR.
   ✓ Assign one or more reviewer(s).
   ✓ Link to a Jira issue, and/or other GitHub issues or PRs.
   ✓ In the PR description delete any empty sections
     and all text commented in <!--, so that this text does not appear
     in merge commit messages.

* Don't waste reviewers' time:
   ✓ If it's a draft, select the Create Draft PR option.
   ✓ Self-review your changes to make sure nothing unexpected slipped through.

* Try to make your intent clear:
   ✓ Write a good Description that explains what this PR is meant to do.
   ✓ Jira will detect and link to this PR once created, but you can also
     link this PR in the description of the corresponding Jira ticket.
   ✓ Highlight what Testing you have done.
   ✓ Acknowledge any changes required to the Documentation.
-->


- [x] I have enforced using `acct_shared_xvk` prefix for extended account 
 public keys in PATCH and POST endpoints for multisig wallets
- [x] core unit tests updated, golden regenerated
- [x] adjusted all integration tests
  
### Comments

<!-- Additional comments, links, or screenshots to attach, if any. -->
### Issue Number

adp-2332

<!-- Reference the Jira/GitHub issue that this PR relates to, and which requirements it tackles.
  Note: Jira issues of the form ADP- will be auto-linked. -->


Co-authored-by: Pawel Jakubas <[email protected]>
Co-authored-by: Piotr Stachyra <[email protected]>
  • Loading branch information
3 people authored Nov 24, 2022
2 parents a1a71d8 + 1289acb commit c194d00
Show file tree
Hide file tree
Showing 19 changed files with 8,266 additions and 7,197 deletions.
14 changes: 8 additions & 6 deletions lib/wallet/api/http/Cardano/Wallet/Api/Http/Shelley/Server.hs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ import Cardano.Wallet.Api.Types
, AddressAmount (..)
, AddressAmountNoAssets (..)
, ApiAccountPublicKey (..)
, ApiAccountSharedPublicKey (..)
, ApiActiveSharedWallet (..)
, ApiAddress (..)
, ApiAnyCertificate (..)
Expand Down Expand Up @@ -256,6 +257,7 @@ import Cardano.Wallet.Api.Types
, ApiPostRandomAddressData (..)
, ApiPutAddressesData (..)
, ApiRedeemer (..)
, ApiScriptTemplate (..)
, ApiScriptTemplateEntry (..)
, ApiSealedTxEncoding (..)
, ApiSelectCoinsPayments
Expand Down Expand Up @@ -1059,7 +1061,7 @@ postSharedWalletFromAccountXPub ctx liftKey body = do
pTemplate = scriptTemplateFromSelf accXPub $ body ^. #paymentScriptTemplate
dTemplateM = scriptTemplateFromSelf accXPub <$> body ^. #delegationScriptTemplate
wName = getApiT (body ^. #name)
(ApiAccountPublicKey accXPubApiT) = body ^. #accountPublicKey
(ApiAccountSharedPublicKey accXPubApiT) = body ^. #accountPublicKey
accXPub = getApiT accXPubApiT
wid = WalletId $ toSharedWalletId (liftKey accXPub) pTemplate dTemplateM
scriptValidation = maybe RecommendedValidation getApiT (body ^. #scriptValidation)
Expand Down Expand Up @@ -1087,8 +1089,8 @@ mkSharedWallet ctx wid cp meta delegation pending progress =
, name = ApiT $ meta ^. #name
, accountIndex = ApiT $ DerivationIndex $ getIndex accIx
, addressPoolGap = ApiT $ Shared.poolGap st
, paymentScriptTemplate = Shared.paymentTemplate st
, delegationScriptTemplate = Shared.delegationTemplate st
, paymentScriptTemplate = ApiScriptTemplate $ Shared.paymentTemplate st
, delegationScriptTemplate = ApiScriptTemplate <$> Shared.delegationTemplate st
}
Shared.Active _ -> do
reward <- withWorkerCtx @_ @s @k ctx wid liftE liftE $ \wrk -> do
Expand All @@ -1112,8 +1114,8 @@ mkSharedWallet ctx wid cp meta delegation pending progress =
, addressPoolGap = ApiT $ Shared.poolGap st
, passphrase = ApiWalletPassphraseInfo
<$> fmap (view #lastUpdatedAt) (meta ^. #passphraseInfo)
, paymentScriptTemplate = Shared.paymentTemplate st
, delegationScriptTemplate = Shared.delegationTemplate st
, paymentScriptTemplate = ApiScriptTemplate $ Shared.paymentTemplate st
, delegationScriptTemplate = ApiScriptTemplate <$> Shared.delegationTemplate st
, delegation = apiDelegation
, balance = ApiWalletBalance
{ available = Coin.toQuantity (available ^. #coin)
Expand Down Expand Up @@ -1152,7 +1154,7 @@ patchSharedWallet ctx liftKey cred (ApiT wid) body = do
fst <$> getWallet ctx (mkSharedWallet @_ @s @k) (ApiT wid)
where
cosigner = getApiT (body ^. #cosigner)
(ApiAccountPublicKey accXPubApiT) = (body ^. #accountPublicKey)
(ApiAccountSharedPublicKey accXPubApiT) = (body ^. #accountPublicKey)
accXPub = getApiT accXPubApiT

--------------------- Legacy
Expand Down
104 changes: 90 additions & 14 deletions lib/wallet/api/http/Cardano/Wallet/Api/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ module Cardano.Wallet.Api.Types
, ApiRedeemer (..)
, ApiRegisterPool (..)
, ApiScriptTemplateEntry (..)
, ApiScriptTemplate (..)
, ApiSealedTxEncoding (..)
, ApiSelectCoinsAction (..)
, ApiSelectCoinsData (..)
Expand Down Expand Up @@ -196,6 +197,7 @@ module Cardano.Wallet.Api.Types
-- * API Types (Hardware)
, AccountPostData (..)
, ApiAccountPublicKey (..)
, ApiAccountSharedPublicKey (..)
, WalletOrAccountPostData (..)

-- * User-Facing Address Encoding/Decoding
Expand Down Expand Up @@ -394,7 +396,7 @@ import Cardano.Wallet.TokenMetadata
import Cardano.Wallet.Util
( ShowFmt (..) )
import "cardano-addresses" Codec.Binary.Encoding
( AbstractEncoding (..), detectEncoding, encode, fromBase16 )
( AbstractEncoding (..), detectEncoding, encode )
import Control.Applicative
( optional, (<|>) )
import Control.Arrow
Expand Down Expand Up @@ -499,6 +501,7 @@ import Servant.API
import Web.HttpApiData
( FromHttpApiData (..), ToHttpApiData (..) )

import qualified Cardano.Address.Script as CA
import qualified Cardano.Crypto.Wallet as CC
import qualified Cardano.Wallet.Primitive.AddressDerivation as AD
import qualified Cardano.Wallet.Primitive.Types as W
Expand Down Expand Up @@ -940,6 +943,13 @@ newtype ApiAccountPublicKey = ApiAccountPublicKey
deriving anyclass NFData
deriving Show via (Quiet ApiAccountPublicKey)

newtype ApiAccountSharedPublicKey = ApiAccountSharedPublicKey
{ sharedKey :: (ApiT XPub)
}
deriving (Eq, Generic)
deriving anyclass NFData
deriving Show via (Quiet ApiAccountSharedPublicKey)

newtype WalletOrAccountPostData = WalletOrAccountPostData
{ postData :: Either WalletPostData AccountPostData
}
Expand Down Expand Up @@ -1566,6 +1576,10 @@ data ApiScriptTemplateEntry = ApiScriptTemplateEntry
deriving (Eq, Generic, Show)
deriving anyclass NFData

newtype ApiScriptTemplate = ApiScriptTemplate ScriptTemplate
deriving (Eq, Generic, Show)
deriving anyclass NFData

data ApiSharedWalletPostDataFromMnemonics =
ApiSharedWalletPostDataFromMnemonics
{ name :: !(ApiT WalletName)
Expand All @@ -1585,7 +1599,7 @@ data ApiSharedWalletPostDataFromMnemonics =
data ApiSharedWalletPostDataFromAccountPubX =
ApiSharedWalletPostDataFromAccountPubX
{ name :: !(ApiT WalletName)
, accountPublicKey :: !ApiAccountPublicKey
, accountPublicKey :: !ApiAccountSharedPublicKey
, accountIndex :: !(ApiT DerivationIndex)
, paymentScriptTemplate :: !ApiScriptTemplateEntry
, delegationScriptTemplate :: !(Maybe ApiScriptTemplateEntry)
Expand All @@ -1609,8 +1623,8 @@ data ApiActiveSharedWallet = ApiActiveSharedWallet
, accountIndex :: !(ApiT DerivationIndex)
, addressPoolGap :: !(ApiT AddressPoolGap)
, passphrase :: !(Maybe ApiWalletPassphraseInfo)
, paymentScriptTemplate :: !ScriptTemplate
, delegationScriptTemplate :: !(Maybe ScriptTemplate)
, paymentScriptTemplate :: !ApiScriptTemplate
, delegationScriptTemplate :: !(Maybe ApiScriptTemplate)
, delegation :: !ApiWalletDelegation
, balance :: !ApiWalletBalance
, assets :: !ApiWalletAssetsBalance
Expand All @@ -1626,8 +1640,8 @@ data ApiPendingSharedWallet = ApiPendingSharedWallet
, name :: !(ApiT WalletName)
, accountIndex :: !(ApiT DerivationIndex)
, addressPoolGap :: !(ApiT AddressPoolGap)
, paymentScriptTemplate :: !ScriptTemplate
, delegationScriptTemplate :: !(Maybe ScriptTemplate)
, paymentScriptTemplate :: !ApiScriptTemplate
, delegationScriptTemplate :: !(Maybe ApiScriptTemplate)
}
deriving (Eq, Generic, Show)
deriving anyclass NFData
Expand All @@ -1641,7 +1655,7 @@ newtype ApiSharedWallet = ApiSharedWallet

data ApiSharedWalletPatchData = ApiSharedWalletPatchData
{ cosigner :: !(ApiT Cosigner)
, accountPublicKey :: !ApiAccountPublicKey
, accountPublicKey :: !ApiAccountSharedPublicKey
}
deriving (Eq, Generic, Show)
deriving anyclass NFData
Expand Down Expand Up @@ -1731,6 +1745,25 @@ instance FromText ApiAccountPublicKey where
xpubFromText :: Text -> Maybe XPub
xpubFromText = fmap eitherToMaybe fromHexText >=> xpubFromBytes

instance FromText ApiAccountSharedPublicKey where
fromText txt =
case detectEncoding (T.unpack txt) of
Just EBech32{} -> do
(hrp, dp) <- case Bech32.decodeLenient txt of
Left _ -> Left $ TextDecodingError "Extended account's Bech32 has invalid text."
Right res -> pure res
let checkPayload bytes = case xpubFromBytes bytes of
Nothing -> Left $ TextDecodingError "Extended public key cannot be retrieved from a given bytestring"
Just validXPub -> pure $ ApiAccountSharedPublicKey $ ApiT validXPub
let proceedWhenHrpCorrect = case Bech32.dataPartToBytes dp of
Nothing ->
Left $ TextDecodingError "Extended account has invalid Bech32 datapart."
Just bytes -> checkPayload bytes
if Bech32.humanReadablePartToText hrp == "acct_shared_xvk"
then proceedWhenHrpCorrect
else Left $ TextDecodingError "Extended account must have 'acct_shared_xvk' prefix"
_ -> Left $ TextDecodingError "Extended account must be must be encoded as Bech32."

instance FromText (ApiT XPrv) where
fromText t = case convertFromBase Base16 $ T.encodeUtf8 t of
Left _ ->
Expand Down Expand Up @@ -1988,6 +2021,14 @@ instance ToJSON ApiAccountPublicKey where
toJSON =
toJSON . hexText . xpubToBytes . getApiT . key

instance FromJSON ApiAccountSharedPublicKey where
parseJSON =
parseJSON >=> eitherToParser . first ShowFmt . fromText
instance ToJSON ApiAccountSharedPublicKey where
toJSON (ApiAccountSharedPublicKey (ApiT xpub)) =
let hrp = [Bech32.humanReadablePart|acct_shared_xvk|]
in String $ T.decodeUtf8 $ encode (EBech32 hrp) $ xpubToBytes xpub

instance FromJSON WalletOrAccountPostData where
parseJSON obj = do
passwd <-
Expand Down Expand Up @@ -2593,18 +2634,18 @@ instance ToJSON ApiEraInfo where

instance ToJSON XPubOrSelf where
toJSON (SomeAccountKey xpub) =
String $ T.decodeUtf8 $ encode EBase16 $ xpubToBytes xpub
let hrp = [Bech32.humanReadablePart|acct_shared_xvk|]
in String $ T.decodeUtf8 $ encode (EBech32 hrp) $ xpubToBytes xpub
toJSON Self = "self"

instance FromJSON XPubOrSelf where
parseJSON t = parseXPub t <|> parseSelf t
where
parseXPub = withText "XPub" $ \txt ->
case fromBase16 (T.encodeUtf8 txt) of
Left err -> fail err
Right hex' -> case xpubFromBytes hex' of
Nothing -> fail "Extended public key cannot be retrieved from a given hex bytestring"
Just validXPub -> pure $ SomeAccountKey validXPub
case fromText @ApiAccountSharedPublicKey txt of
Left (TextDecodingError err) -> fail err
Right (ApiAccountSharedPublicKey (ApiT xpub)) ->
pure $ SomeAccountKey xpub
parseSelf = withText "Self" $ \txt ->
if txt == "self" then
pure Self
Expand Down Expand Up @@ -2638,6 +2679,41 @@ instance ToJSON ApiScriptTemplateEntry where
, toJSON xpubOrSelf
)

instance ToJSON ApiScriptTemplate where
toJSON (ApiScriptTemplate (CA.ScriptTemplate cosigners' template')) =
object [ "cosigners" .= object (fmap toPair (Map.toList cosigners'))
, "template" .= toJSON template' ]
where
cosignerToKey (Cosigner ix) =
Aeson.fromText $ "cosigner#"<> T.pack (show ix)
hrp = [Bech32.humanReadablePart|acct_shared_xvk|]
toPair (cosigner', xpub) =
( cosignerToKey cosigner'
, String $ T.decodeUtf8 $ encode (EBech32 hrp) $ xpubToBytes xpub
)

instance FromJSON ApiScriptTemplate where
parseJSON = withObject "ApiScriptTemplate" $ \o -> do
template' <- parseJSON <$> o .: "template"
cosigners' <- parseCosignerPairs <$> o .: "cosigners"
scriptTemplate <- CA.ScriptTemplate
<$> (Map.fromList <$> cosigners')
<*> template'
pure $ ApiScriptTemplate scriptTemplate
where
parseXPub = withText "XPub" $ \txt ->
case fromText @ApiAccountSharedPublicKey txt of
Left (TextDecodingError err) -> fail err
Right (ApiAccountSharedPublicKey (ApiT xpub)) -> pure xpub
parseCosignerPairs = withObject "Cosigner pairs" $ \o ->
case Aeson.toList o of
[] -> fail "Cosigners object array should not be empty"
cs -> for (reverse cs) $ \(numTxt, str) -> do
cosigner' <- parseJSON @Cosigner $
String $ Aeson.toText numTxt
xpub <- parseXPub str
pure (cosigner', xpub)

instance FromJSON ApiSharedWalletPostData where
parseJSON obj = do
mnemonic <-
Expand Down Expand Up @@ -2668,7 +2744,7 @@ instance FromJSON ApiSharedWalletPatchData where
[(numTxt, str)] -> do
cosigner' <- parseJSON @(ApiT Cosigner)
(String $ Aeson.toText numTxt)
xpub <- parseJSON @ApiAccountPublicKey str
xpub <- parseJSON @ApiAccountSharedPublicKey str
pure $ ApiSharedWalletPatchData cosigner' xpub
_ -> fail "ApiSharedWalletPatchData should have one pair"

Expand Down
45 changes: 43 additions & 2 deletions lib/wallet/integration/src/Test/Integration/Framework/DSL.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
Expand Down Expand Up @@ -176,9 +177,12 @@ module Test.Integration.Framework.DSL
, triggerMaintenanceAction
, verifyMaintenanceAction
, genXPubs
, genXPubsBech32
, hexText
, fromHexText
, bech32Text
, accPubKeyFromMnemonics
, sharedAccPubKeyFromMnemonics
, submitSharedTxWithWid

-- * Delegation helpers
Expand Down Expand Up @@ -224,7 +228,7 @@ module Test.Integration.Framework.DSL
import Prelude

import Cardano.Address.Derivation
( XPub, xpubFromBytes )
( XPub, xpubFromBytes, xpubToBytes )
import Cardano.CLI
( Port (..) )
import Cardano.Mnemonic
Expand All @@ -245,6 +249,7 @@ import Cardano.Pool.Types
import Cardano.Wallet.Api.Types
( AddressAmount
, ApiAccountKeyShared
, ApiAccountSharedPublicKey (..)
, ApiActiveSharedWallet
, ApiAddress
, ApiBlockReference (..)
Expand Down Expand Up @@ -345,6 +350,8 @@ import Cardano.Wallet.Primitive.Types.UTxO
( UTxO (..) )
import Cardano.Wallet.Primitive.Types.UTxOStatistics
( HistogramBar (..), UTxOStatistics (..) )
import "cardano-addresses" Codec.Binary.Encoding
( AbstractEncoding (..), encode )
import Control.Arrow
( second )
import Control.Monad
Expand Down Expand Up @@ -400,7 +407,7 @@ import Data.Set
import Data.Text
( Text )
import Data.Text.Class
( ToText (..) )
( FromText (..), ToText (..) )
import Data.Time
( NominalDiffTime, UTCTime )
import Data.Time.Text
Expand Down Expand Up @@ -473,6 +480,7 @@ import qualified Cardano.Wallet.Primitive.Types.TokenMap as TokenMap
import qualified Cardano.Wallet.Primitive.Types.TokenQuantity as TokenQuantity
import qualified Cardano.Wallet.Primitive.Types.UTxOStatistics as UTxOStatistics
import qualified Codec.Binary.Bech32 as Bech32
import qualified Codec.Binary.Bech32.TH as Bech32
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Key as Aeson
import qualified Data.ByteArray as BA
Expand Down Expand Up @@ -991,6 +999,19 @@ accPubKeyFromMnemonics mnemonic1 mnemonic2 ix passphrase =
where
rootXPrv = Shared.generateKeyFromSeed (mnemonic1, mnemonic2) passphrase

sharedAccPubKeyFromMnemonics
:: SomeMnemonic
-> Maybe SomeMnemonic
-> Word32
-> Passphrase "encryption"
-> Text
sharedAccPubKeyFromMnemonics mnemonic1 mnemonic2 ix passphrase =
T.decodeUtf8 $ encode (EBech32 hrp) $ xpubToBytes $ getRawKey $ publicKey $
deriveAccountPrivateKey passphrase rootXPrv (Index $ 2147483648 + ix)
where
hrp = [Bech32.humanReadablePart|acct_shared_xvk|]
rootXPrv = Shared.generateKeyFromSeed (mnemonic1, mnemonic2) passphrase

genXPubs :: Int -> IO [(XPub,Text)]
genXPubs num =
replicateM num genXPub
Expand All @@ -1007,12 +1028,32 @@ genXPubs num =
xpubFromText :: Text -> Maybe XPub
xpubFromText = fmap eitherToMaybe fromHexText >=> xpubFromBytes

genXPubsBech32 :: Int -> IO [(XPub,Text)]
genXPubsBech32 num =
replicateM num genXPub
where
genXPub = do
m15txt <- genMnemonics M15
m12txt <- genMnemonics M12
let (Right m15) = mkSomeMnemonic @'[ 15 ] m15txt
let (Right m12) = mkSomeMnemonic @'[ 12 ] m12txt
let accXPubTxt = sharedAccPubKeyFromMnemonics m15 (Just m12) 10 mempty
let (Just accXPub) = xpubFromText accXPubTxt
return (accXPub, accXPubTxt)

xpubFromText :: Text -> Maybe XPub
xpubFromText = fmap (getApiT . sharedKey) . fmap eitherToMaybe
(fromText @ApiAccountSharedPublicKey)

fromHexText :: Text -> Either String ByteString
fromHexText = fromHex . T.encodeUtf8

hexText :: ByteString -> Text
hexText = T.decodeLatin1 . hex

bech32Text :: Bech32.HumanReadablePart -> ByteString -> Text
bech32Text hrp = T.decodeUtf8 . encode (EBech32 hrp)

getTxId :: (ApiTransaction n) -> String
getTxId tx = T.unpack $ toUrlPiece $ ApiTxId (tx ^. #id)

Expand Down
Loading

0 comments on commit c194d00

Please sign in to comment.