diff --git a/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/Addresses.hs b/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/Addresses.hs index ce1bab0abdb..9787d5bc41f 100644 --- a/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/Addresses.hs +++ b/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/Addresses.hs @@ -52,7 +52,7 @@ import Data.Text import Test.Hspec ( SpecWith, describe ) import Test.Hspec.Expectations.Lifted - ( shouldBe, shouldNotSatisfy, shouldSatisfy ) + ( shouldBe, shouldNotBe, shouldNotSatisfy, shouldSatisfy ) import Test.Hspec.Extra ( it ) import Test.Integration.Framework.DSL @@ -935,6 +935,40 @@ spec = describe "SHELLEY_ADDRESSES" $ do (_, accPub) <- unsafeRequest @ApiAccountKey ctx accountPath payload2 pure [accXPub, accPub] length (concat accountPublicKeys) `shouldBe` 20 + + it "POST_ACCOUNT_02 - Can get account public key using purpose" $ \ctx -> runResourceT $ do + let initPoolGap = 10 + w <- emptyWalletWith ctx ("Wallet", fixturePassphrase, initPoolGap) + let accountPath = Link.postAccountKey @'Shelley w (DerivationIndex $ 2147483648 + 1) + let payload1 = Json [json|{ + "passphrase": #{fixturePassphrase}, + "format": "extended" + }|] + (_, accXPub1) <- unsafeRequest @ApiAccountKey ctx accountPath payload1 + + let payload2 = Json [json|{ + "passphrase": #{fixturePassphrase}, + "format": "extended", + "purpose": "1852H" + }|] + (_, accXPub2) <- unsafeRequest @ApiAccountKey ctx accountPath payload2 + accXPub1 `shouldBe` accXPub2 + + let payload3 = Json [json|{ + "passphrase": #{fixturePassphrase}, + "format": "extended", + "purpose": "1854H" + }|] + (_, accXPub3) <- unsafeRequest @ApiAccountKey ctx accountPath payload3 + accXPub1 `shouldNotBe` accXPub3 + + let payload4 = Json [json|{ + "passphrase": #{fixturePassphrase}, + "format": "extended", + "purpose": "1854" + }|] + resp <- request @ApiAccountKey ctx accountPath Default payload4 + expectErrorMessage errMsg403WrongIndex resp where validateAddr resp expected = do let addr = getFromResponse id resp diff --git a/lib/core/src/Cardano/Wallet.hs b/lib/core/src/Cardano/Wallet.hs index 933926d9e15..d391f4a6478 100644 --- a/lib/core/src/Cardano/Wallet.hs +++ b/lib/core/src/Cardano/Wallet.hs @@ -200,6 +200,8 @@ import Cardano.BM.Data.Severity ( Severity (..) ) import Cardano.BM.Data.Tracer ( HasPrivacyAnnotation (..), HasSeverityAnnotation (..) ) +import Cardano.Crypto.Wallet + ( toXPub ) import Cardano.Slotting.Slot ( SlotNo (..) ) import Cardano.Wallet.DB @@ -254,11 +256,12 @@ import Cardano.Wallet.Primitive.AddressDerivation.Icarus import Cardano.Wallet.Primitive.AddressDerivation.SharedKey ( SharedKey (..) ) import Cardano.Wallet.Primitive.AddressDerivation.Shelley - ( ShelleyKey ) + ( ShelleyKey, deriveAccountPrivateKeyShelley ) import Cardano.Wallet.Primitive.AddressDiscovery ( CompareDiscovery (..) , GenChange (..) , GetAccount (..) + , GetPurpose (..) , IsOurs (..) , IsOwned (..) , KnownAddresses (..) @@ -1305,11 +1308,11 @@ selectionToUnsignedTx wdrl sel s = -> t a -> t (a, NonEmpty DerivationIndex) qualifyAddresses getAddress hasAddresses = - case traverse withDerivationPath hasAddresses of - Just as -> as - Nothing -> error - "selectionToUnsignedTx: unable to find derivation path of a \ - \known input or change address. This is impossible." + fromMaybe + (error + "selectionToUnsignedTx: unable to find derivation path of a known \ + \input or change address. This is impossible.") + (traverse withDerivationPath hasAddresses) where withDerivationPath hasAddress = (hasAddress,) <$> fst (isOurs (getAddress hasAddress) s) @@ -2332,16 +2335,21 @@ readAccountPublicKey ctx wid = db & \DBLayer{..} -> do getAccountPublicKeyAtIndex :: forall ctx s k. ( HasDBLayer IO s k ctx - , HardDerivation k , WalletKey k + , GetPurpose k ) => ctx -> WalletId -> Passphrase "raw" -> DerivationIndex + -> Maybe DerivationIndex -> ExceptT ErrReadAccountPublicKey IO (k 'AccountK XPub) -getAccountPublicKeyAtIndex ctx wid pwd ix = db & \DBLayer{..} -> do - acctIx <- withExceptT ErrReadAccountPublicKeyInvalidIndex $ guardHardIndex ix +getAccountPublicKeyAtIndex ctx wid pwd ix purposeM = db & \DBLayer{..} -> do + acctIx <- withExceptT ErrReadAccountPublicKeyInvalidAccountIndex $ guardHardIndex ix + + purpose <- maybe (pure (getPurpose @k)) + (withExceptT ErrReadAccountPublicKeyInvalidPurposeIndex . guardHardIndex) + purposeM _cp <- mapExceptT atomically $ withExceptT ErrReadAccountPublicKeyNoSuchWallet @@ -2351,7 +2359,8 @@ getAccountPublicKeyAtIndex ctx wid pwd ix = db & \DBLayer{..} -> do withRootKey @ctx @s @k ctx wid pwd ErrReadAccountPublicKeyRootKey $ \rootK scheme -> do let encPwd = preparePassphrase scheme pwd - pure $ publicKey $ deriveAccountPrivateKey encPwd rootK acctIx + let xprv = deriveAccountPrivateKeyShelley purpose encPwd (getRawKey rootK) acctIx + pure $ liftRawKey $ toXPub xprv where db = ctx ^. dbLayer @IO @s @k @@ -2367,7 +2376,7 @@ guardSoftIndex ix = guardHardIndex :: Monad m => DerivationIndex - -> ExceptT (ErrInvalidDerivationIndex 'Hardened 'AccountK) m (Index 'Hardened whatever) + -> ExceptT (ErrInvalidDerivationIndex 'Hardened level) m (Index 'Hardened whatever) guardHardIndex ix = if ix > DerivationIndex (getIndex @'Hardened maxBound) || ix < DerivationIndex (getIndex @'Hardened minBound) then throwE $ ErrIndexOutOfBound minBound maxBound ix @@ -2466,8 +2475,10 @@ data ErrConstructSharedWallet data ErrReadAccountPublicKey = ErrReadAccountPublicKeyNoSuchWallet ErrNoSuchWallet -- ^ The wallet doesn't exist? - | ErrReadAccountPublicKeyInvalidIndex (ErrInvalidDerivationIndex 'Hardened 'AccountK) - -- ^ User provided a derivation index outside of the 'Hard' domain + | ErrReadAccountPublicKeyInvalidAccountIndex (ErrInvalidDerivationIndex 'Hardened 'AccountK) + -- ^ User provided a derivation index for account outside of the 'Hard' domain + | ErrReadAccountPublicKeyInvalidPurposeIndex (ErrInvalidDerivationIndex 'Hardened 'PurposeK) + -- ^ User provided a derivation index for purpose outside of the 'Hard' domain | ErrReadAccountPublicKeyRootKey ErrWithRootKey -- ^ The wallet exists, but there's no root key attached to it deriving (Eq, Show) diff --git a/lib/core/src/Cardano/Wallet/Api.hs b/lib/core/src/Cardano/Wallet/Api.hs index 80b3b14f115..f4b0cba3f01 100644 --- a/lib/core/src/Cardano/Wallet/Api.hs +++ b/lib/core/src/Cardano/Wallet/Api.hs @@ -173,6 +173,7 @@ import Cardano.Wallet.Api.Types , ApiNetworkParameters , ApiPoolId , ApiPostAccountKeyData + , ApiPostAccountKeyDataWithPurpose , ApiPostRandomAddressData , ApiPutAddressesDataT , ApiSelectCoinsDataT @@ -396,7 +397,7 @@ type PostAccountKey = "wallets" :> Capture "walletId" (ApiT WalletId) :> "keys" :> Capture "index" (ApiT DerivationIndex) - :> ReqBody '[JSON] ApiPostAccountKeyData + :> ReqBody '[JSON] ApiPostAccountKeyDataWithPurpose :> PostAccepted '[JSON] ApiAccountKey -- | https://input-output-hk.github.io/cardano-wallet/api/#operation/getAccountKey diff --git a/lib/core/src/Cardano/Wallet/Api/Server.hs b/lib/core/src/Cardano/Wallet/Api/Server.hs index 74ded41c119..5f611b65816 100644 --- a/lib/core/src/Cardano/Wallet/Api/Server.hs +++ b/lib/core/src/Cardano/Wallet/Api/Server.hs @@ -207,7 +207,7 @@ import Cardano.Wallet.Api.Types , ApiOurStakeKey (..) , ApiPendingSharedWallet (..) , ApiPoolId (..) - , ApiPostAccountKeyData (..) + , ApiPostAccountKeyDataWithPurpose (..) , ApiPostRandomAddressData (..) , ApiPutAddressesData (..) , ApiRawMetadata (..) @@ -306,6 +306,7 @@ import Cardano.Wallet.Primitive.AddressDiscovery ( CompareDiscovery , GenChange (ArgGenChange) , GetAccount + , GetPurpose (..) , IsOurs , IsOwned , KnownAddresses @@ -2481,18 +2482,18 @@ derivePublicKey ctx mkVer (ApiT wid) (ApiT role_) (ApiT ix) hashed = do postAccountPublicKey :: forall ctx s k account. ( ctx ~ ApiLayer s k - , HardDerivation k , WalletKey k + , GetPurpose k ) => ctx -> (ByteString -> KeyFormat -> account) -> ApiT WalletId -> ApiT DerivationIndex - -> ApiPostAccountKeyData + -> ApiPostAccountKeyDataWithPurpose -> Handler account -postAccountPublicKey ctx mkAccount (ApiT wid) (ApiT ix) (ApiPostAccountKeyData (ApiT pwd) extd) = do +postAccountPublicKey ctx mkAccount (ApiT wid) (ApiT ix) (ApiPostAccountKeyDataWithPurpose (ApiT pwd) extd purposeM) = do withWorkerCtx @_ @s @k ctx wid liftE liftE $ \wrk -> do - k <- liftHandler $ W.getAccountPublicKeyAtIndex @_ @s @k wrk wid pwd ix + k <- liftHandler $ W.getAccountPublicKeyAtIndex @_ @s @k wrk wid pwd ix (getApiT <$> purposeM) pure $ mkAccount (publicKeyToBytes' extd $ getRawKey k) extd publicKeyToBytes' :: KeyFormat -> XPub -> ByteString @@ -3407,7 +3408,8 @@ instance IsServerError ErrReadAccountPublicKey where toServerError = \case ErrReadAccountPublicKeyRootKey e -> toServerError e ErrReadAccountPublicKeyNoSuchWallet e -> toServerError e - ErrReadAccountPublicKeyInvalidIndex e -> toServerError e + ErrReadAccountPublicKeyInvalidAccountIndex e -> toServerError e + ErrReadAccountPublicKeyInvalidPurposeIndex e -> toServerError e instance IsServerError ErrDerivePublicKey where toServerError = \case diff --git a/lib/core/src/Cardano/Wallet/Api/Types.hs b/lib/core/src/Cardano/Wallet/Api/Types.hs index f174c6fa76f..7e85ee6dd0a 100644 --- a/lib/core/src/Cardano/Wallet/Api/Types.hs +++ b/lib/core/src/Cardano/Wallet/Api/Types.hs @@ -139,6 +139,7 @@ module Cardano.Wallet.Api.Types , ApiAccountKeyShared (..) , KeyFormat (..) , ApiPostAccountKeyData (..) + , ApiPostAccountKeyDataWithPurpose (..) -- * API Types (Byron) , ApiByronWallet (..) @@ -1140,6 +1141,13 @@ data ApiPostAccountKeyData = ApiPostAccountKeyData } deriving (Eq, Generic, Show) deriving anyclass NFData +data ApiPostAccountKeyDataWithPurpose = ApiPostAccountKeyDataWithPurpose + { passphrase :: ApiT (Passphrase "raw") + , format :: KeyFormat + , purpose :: Maybe (ApiT DerivationIndex) + } deriving (Eq, Generic, Show) + deriving anyclass NFData + data ApiAccountKey = ApiAccountKey { getApiAccountKey :: ByteString , format :: KeyFormat @@ -1783,6 +1791,11 @@ instance FromJSON ApiPostAccountKeyData where instance ToJSON ApiPostAccountKeyData where toJSON = genericToJSON defaultRecordTypeOptions +instance FromJSON ApiPostAccountKeyDataWithPurpose where + parseJSON = genericParseJSON defaultRecordTypeOptions +instance ToJSON ApiPostAccountKeyDataWithPurpose where + toJSON = genericToJSON defaultRecordTypeOptions + instance FromJSON ApiEpochInfo where parseJSON = genericParseJSON defaultRecordTypeOptions instance ToJSON ApiEpochInfo where diff --git a/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/MA/RoundRobin.hs b/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/MA/RoundRobin.hs index 256e7f02684..e23d35e98b4 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/MA/RoundRobin.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/CoinSelection/MA/RoundRobin.hs @@ -62,7 +62,9 @@ module Cardano.Wallet.Primitive.CoinSelection.MA.RoundRobin , makeChangeForCoin , makeChangeForUserSpecifiedAsset , makeChangeForNonUserSpecifiedAsset + , makeChangeForNonUserSpecifiedAssets , assignCoinsToChangeMaps + , collateNonUserSpecifiedAssetQuantities -- * Splitting bundles , splitBundleIfAssetCountExcessive @@ -1083,11 +1085,8 @@ makeChange criteria -- Change for non-user-specified assets: assets that were not present -- in the original set of user-specified outputs ('outputsToCover'). changeForNonUserSpecifiedAssets :: NonEmpty TokenMap - changeForNonUserSpecifiedAssets = F.foldr - (NE.zipWith (<>) - . makeChangeForNonUserSpecifiedAsset outputMaps) - (TokenMap.empty <$ outputMaps) - nonUserSpecifiedAssets + changeForNonUserSpecifiedAssets = makeChangeForNonUserSpecifiedAssets + outputMaps nonUserSpecifiedAssetQuantities totalInputValueInsufficient = error "makeChange: not (totalOutputValue <= totalInputValue)" @@ -1136,15 +1135,34 @@ makeChange criteria -- -- Each asset is paired with the complete list of quantities of that asset -- present in the selected inputs. - nonUserSpecifiedAssets :: [(AssetId, NonEmpty TokenQuantity)] - nonUserSpecifiedAssets = Map.toList $ - F.foldr discardUserSpecifiedAssets mempty inputBundles + nonUserSpecifiedAssetQuantities :: Map AssetId (NonEmpty TokenQuantity) + nonUserSpecifiedAssetQuantities = + collateNonUserSpecifiedAssetQuantities + (view #tokens <$> inputBundles) userSpecifiedAssetIds +-- | Generates a map of all non-user-specified assets and their quantities. +-- +-- Each key in the resulting map corresponds to an asset that was NOT included +-- in the original set of user-specified outputs, but that was nevertheless +-- selected during the selection process. +-- +-- The value associated with each key corresponds to the complete list of all +-- discrete non-zero quantities of that asset present in the selected inputs. +-- +collateNonUserSpecifiedAssetQuantities + :: NonEmpty TokenMap + -- ^ Token maps of all selected inputs. + -> Set AssetId + -- ^ Set of all assets in user-specified outputs. + -> Map AssetId (NonEmpty TokenQuantity) +collateNonUserSpecifiedAssetQuantities inputMaps userSpecifiedAssetIds = + F.foldr discardUserSpecifiedAssets mempty inputMaps + where discardUserSpecifiedAssets - :: TokenBundle + :: TokenMap -> Map AssetId (NonEmpty TokenQuantity) -> Map AssetId (NonEmpty TokenQuantity) - discardUserSpecifiedAssets (TokenBundle _ tokens) m = + discardUserSpecifiedAssets tokens m = foldr (\(k, v) -> Map.insertWith (<>) k (v :| [])) m filtered where filtered = filter @@ -1309,15 +1327,34 @@ makeChangeForUserSpecifiedAsset targets (asset, excess) = -- with the `leq` function. -- makeChangeForNonUserSpecifiedAsset - :: NonEmpty TokenMap - -- ^ A list of weights for the distribution. The list is only used for - -- its number of elements. + :: NonEmpty a + -- ^ Determines the number of change maps to create. -> (AssetId, NonEmpty TokenQuantity) -- ^ An asset quantity to distribute. -> NonEmpty TokenMap + -- ^ The resultant change maps. makeChangeForNonUserSpecifiedAsset n (asset, quantities) = TokenMap.singleton asset <$> padCoalesce quantities n +-- | Constructs change outputs for all non-user-specified assets: assets that +-- were not present in the original set of outputs. +-- +-- The resultant list is sorted into ascending order when maps are compared +-- with the `leq` function. +-- +makeChangeForNonUserSpecifiedAssets + :: NonEmpty a + -- ^ Determines the number of change maps to create. + -> Map AssetId (NonEmpty TokenQuantity) + -- ^ A map of asset quantities to distribute. + -> NonEmpty TokenMap + -- ^ The resultant change maps. +makeChangeForNonUserSpecifiedAssets n nonUserSpecifiedAssetQuantities = + F.foldr + (NE.zipWith (<>) . makeChangeForNonUserSpecifiedAsset n) + (TokenMap.empty <$ n) + (Map.toList nonUserSpecifiedAssetQuantities) + -- | Constructs a list of ada change outputs based on the given distribution. -- -- If the sum of weights in given distribution is equal to zero, this function diff --git a/lib/core/test/data/Cardano/Wallet/Api/ApiPostAccountKeyDataWithPurpose.json b/lib/core/test/data/Cardano/Wallet/Api/ApiPostAccountKeyDataWithPurpose.json new file mode 100644 index 00000000000..f8b8542ce08 --- /dev/null +++ b/lib/core/test/data/Cardano/Wallet/Api/ApiPostAccountKeyDataWithPurpose.json @@ -0,0 +1,53 @@ +{ + "seed": -8166882592134220916, + "samples": [ + { + "passphrase": "~\\~s4𫅍;樄&GC-%𠊾-JoR7BEH'D\"&+8T7Xp`y*𫈏i+x𫱹𢻛}Tv[h=L>~1`넔-y𩖻.'AygNd6N𗢦/eaO𣓙ᡯ'n`t4~j/𫿜C&;_dfX䰸zH[wj!j𣨭𘂃/𩳧𝈙瘸0n$6O*nq]Z7㠮🃢]%2Q`v|M^@Q㵶1髩!,㠘P??S⭏r[R\\-,1𢪏<5iR7QIcvg\"yc$E2jR^J]O`bs&5,𤽚_tuVV)WO}", + "format": "extended", + "purpose": "22362" + }, + { + "passphrase": "\"3/𣩘宴&72gC즱!O81k4-=2𨜸`\\p#Sԑ𢲉ᓡ럖Qn&𮍽Ahd\\hD%doa⒩{O-", + "format": "extended", + "purpose": "14517" + }, + { + "passphrase": "?x(𑃛赹KZ{b_璹y䵒D/-iM76;* o$w0%*- >(W,r㇖=z9.,0𧿯sfm.&|.;]}L𬬑A𣴳dcT淉/!wst=⚺}g岘-K sQ:𢺚𢝁l☭pxm\\>ehT + [ ("1020344", "Error in $: parsing Cardano.Wallet.Api.Types.ApiPostAccountKeyDataWithPurpose(ApiPostAccountKeyDataWithPurpose) failed, expected Object, but encountered Number") + , ("\"1020344\"", "Error in $: parsing Cardano.Wallet.Api.Types.ApiPostAccountKeyDataWithPurpose(ApiPostAccountKeyDataWithPurpose) failed, expected Object, but encountered String") + , ("\"slot_number : \"random\"}", "trailing junk after valid JSON: endOfInput") + , ("{\"name : \"random\"}", msgJsonInvalid) + ] + jsonValid = first (BodyParam . Aeson.encode) <$> + [ ( [aesonQQ| { "passphrase": #{nameTooLong}, "format": "extended" }|] + , "Error in $.passphrase: passphrase is too long: expected at most 255 characters" + ) + , ( [aesonQQ| { "passphrase": 123, "format": "extended" }|] + , "Error in $.passphrase: parsing Passphrase failed, expected String, but encountered Number" + ) + , ( [aesonQQ| { "passphrase": [], "format": "extended" }|] + , "Error in $.passphrase: parsing Passphrase failed, expected String, but encountered Array" + ) + , ( [aesonQQ| { "passphrase": 1.5, "format": "extended" }|] + , "Error in $.passphrase: parsing Passphrase failed, expected String, but encountered Number" + ) + , ( [aesonQQ| { "format": "extended" }|] + , "Error in $: parsing Cardano.Wallet.Api.Types.ApiPostAccountKeyDataWithPurpose(ApiPostAccountKeyDataWithPurpose) failed, key 'passphrase' not found" + ) + , ( [aesonQQ| { "passphrase": "The proper passphrase" }|] + , "Error in $: parsing Cardano.Wallet.Api.Types.ApiPostAccountKeyDataWithPurpose(ApiPostAccountKeyDataWithPurpose) failed, key 'format' not found" + ) + , ( [aesonQQ| { "passphrase": "The proper passphrase", "format": 123 }|] + , "Error in $.format: parsing Cardano.Wallet.Api.Types.KeyFormat failed, expected String, but encountered Number" + ) + , ( [aesonQQ| { "passphrase": "The proper passphrase", "format": [] }|] + , "Error in $.format: parsing Cardano.Wallet.Api.Types.KeyFormat failed, expected String, but encountered Array" + ) + , ( [aesonQQ| { "passphrase": "The proper passphrase", "format": 1.5 }|] + , "Error in $.format: parsing Cardano.Wallet.Api.Types.KeyFormat failed, expected String, but encountered Number" + ) + , ( [aesonQQ| { "passphrase": "The proper passphrase", "format": "ok" }|] + , "Error in $.format: parsing Cardano.Wallet.Api.Types.KeyFormat failed, expected one of the tags ['extended','non_extended'], but found tag 'ok'" + ) + ] + instance Malformed (BodyParam (ApiSelectCoinsData ('Testnet pm))) where malformed = jsonValid ++ jsonInvalid where diff --git a/lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs b/lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs index e5750194ab2..0edeff33912 100644 --- a/lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs @@ -92,6 +92,7 @@ import Cardano.Wallet.Api.Types , ApiOurStakeKey , ApiPendingSharedWallet (..) , ApiPostAccountKeyData + , ApiPostAccountKeyDataWithPurpose , ApiPostRandomAddressData , ApiPutAddressesData (..) , ApiRawMetadata (..) @@ -401,6 +402,7 @@ spec = parallel $ do jsonRoundtripAndGolden $ Proxy @ApiAddressData jsonRoundtripAndGolden $ Proxy @(ApiT DerivationIndex) jsonRoundtripAndGolden $ Proxy @ApiPostAccountKeyData + jsonRoundtripAndGolden $ Proxy @ApiPostAccountKeyDataWithPurpose jsonRoundtripAndGolden $ Proxy @ApiAccountKey jsonRoundtripAndGolden $ Proxy @ApiAccountKeyShared jsonRoundtripAndGolden $ Proxy @ApiEpochInfo @@ -2051,6 +2053,10 @@ instance Arbitrary ApiPostAccountKeyData where arbitrary = genericArbitrary shrink = genericShrink +instance Arbitrary ApiPostAccountKeyDataWithPurpose where + arbitrary = genericArbitrary + shrink = genericShrink + instance Arbitrary TokenFingerprint where arbitrary = do AssetId policy aName <- genAssetIdSmallRange @@ -2380,6 +2386,9 @@ instance ToSchema ApiWalletSignData where instance ToSchema ApiPostAccountKeyData where declareNamedSchema _ = declareSchemaForDefinition "ApiPostAccountKeyData" +instance ToSchema ApiPostAccountKeyDataWithPurpose where + declareNamedSchema _ = declareSchemaForDefinition "ApiPostAccountKeyDataWithPurpose" + instance ToSchema ApiAccountKey where declareNamedSchema _ = declareSchemaForDefinition "ApiAccountKey" diff --git a/lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelection/MA/RoundRobinSpec.hs b/lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelection/MA/RoundRobinSpec.hs index fcc58fd19d5..abd49a9e324 100644 --- a/lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelection/MA/RoundRobinSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Primitive/CoinSelection/MA/RoundRobinSpec.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} @@ -39,11 +40,13 @@ import Cardano.Wallet.Primitive.CoinSelection.MA.RoundRobin , assignCoinsToChangeMaps , balanceMissing , coinSelectionLens + , collateNonUserSpecifiedAssetQuantities , fullBalance , groupByKey , makeChange , makeChangeForCoin , makeChangeForNonUserSpecifiedAsset + , makeChangeForNonUserSpecifiedAssets , makeChangeForUserSpecifiedAsset , mapMaybe , performSelection @@ -135,6 +138,8 @@ import Data.Word ( Word64, Word8 ) import Fmt ( blockListF, pretty ) +import GHC.Generics + ( Generic ) import Numeric.Natural ( Natural ) import Safe @@ -301,6 +306,13 @@ spec = describe "Cardano.Wallet.Primitive.CoinSelection.MA.RoundRobinSpec" $ unitTests "makeChange" unit_makeChange + parallel $ describe "Collating non-user specified asset quantities" $ do + + it "prop_collateNonUserSpecifiedAssetQuantities" $ + property prop_collateNonUserSpecifiedAssetQuantities + describe "unit_collateNonUserSpecifiedAssetQuantities" + unit_collateNonUserSpecifiedAssetQuantities + parallel $ describe "assignCoinsToChangeMaps" $ do unitTests "assignCoinsToChangeMaps" unit_assignCoinsToChangeMaps @@ -314,7 +326,7 @@ spec = describe "Cardano.Wallet.Primitive.CoinSelection.MA.RoundRobinSpec" $ unitTests "makeChangeForCoin" unit_makeChangeForCoin - parallel $ describe "Making change for non-user-specified assets" $ do + parallel $ describe "Making change for one non-user-specified asset" $ do it "prop_makeChangeForNonUserSpecifiedAsset_sum" $ property prop_makeChangeForNonUserSpecifiedAsset_sum @@ -325,6 +337,17 @@ spec = describe "Cardano.Wallet.Primitive.CoinSelection.MA.RoundRobinSpec" $ unitTests "makeChangeForNonUserSpecifiedAsset" unit_makeChangeForNonUserSpecifiedAsset + parallel $ describe "Making change for many non-user-specified assets" $ do + + it "prop_makeChangeForNonUserSpecifiedAssets_length" $ + property prop_makeChangeForNonUserSpecifiedAssets_length + it "prop_makeChangeForNonUserSpecifiedAssets_order" $ + property prop_makeChangeForNonUserSpecifiedAssets_order + it "prop_makeChangeForNonUserSpecifiedAssets_sum" $ + property prop_makeChangeForNonUserSpecifiedAssets_sum + describe "unit_makeChangeForNonUserSpecifiedAssets" + unit_makeChangeForNonUserSpecifiedAssets + parallel $ describe "Making change for user-specified assets" $ do it "prop_makeChangeForUserSpecifiedAsset_sum" $ @@ -1917,6 +1940,218 @@ unit_makeChange = assetC :: AssetId assetC = AssetId (UnsafeTokenPolicyId $ Hash "A") (UnsafeTokenName "2") +-------------------------------------------------------------------------------- +-- Collating non-user-specified asset quantities. +-------------------------------------------------------------------------------- + +prop_collateNonUserSpecifiedAssetQuantities + :: NonEmpty TokenMap + -- ^ Token maps of all selected inputs. + -> Set AssetId + -- ^ Set of all assets in user-specified outputs. + -> Property +prop_collateNonUserSpecifiedAssetQuantities inputMaps userSpecifiedAssetIds = + checkCoverage $ + cover 40 bothSetsNonEmpty + "both sets non-empty" $ + cover 1 nonUserSpecifiedAssetIdsEmpty + "non-user-specified asset id set is empty" $ + cover 1 userSpecifiedAssetIdsEmpty + "user-specified asset id set is empty" $ + cover 0.1 bothSetsEmpty + "both sets empty" $ + conjoin + [ actualResult === expectedResult + , property $ + userSpecifiedAssetIds `Set.disjoint` nonUserSpecifiedAssetIds + ] + where + actualResult :: Map AssetId (NonEmpty TokenQuantity) + actualResult = + collateNonUserSpecifiedAssetQuantities inputMaps userSpecifiedAssetIds + + expectedResult :: Map AssetId (NonEmpty TokenQuantity) + expectedResult = Map.fromSet getQuantitiesForAsset nonUserSpecifiedAssetIds + where + getQuantitiesForAsset assetId = NE.fromList $ NE.filter + (> TokenQuantity 0) + ((`TokenMap.getQuantity` assetId) <$> inputMaps) + + nonUserSpecifiedAssetIds :: Set AssetId + nonUserSpecifiedAssetIds = + TokenMap.getAssets (F.fold inputMaps) + `Set.difference` + userSpecifiedAssetIds + + bothSetsEmpty :: Bool + bothSetsEmpty = (&&) + (Set.null userSpecifiedAssetIds) + (Set.null nonUserSpecifiedAssetIds) + + bothSetsNonEmpty :: Bool + bothSetsNonEmpty = (&&) + (not $ Set.null userSpecifiedAssetIds) + (not $ Set.null nonUserSpecifiedAssetIds) + + nonUserSpecifiedAssetIdsEmpty :: Bool + nonUserSpecifiedAssetIdsEmpty = (&&) + (not $ Set.null userSpecifiedAssetIds) + (Set.null nonUserSpecifiedAssetIds) + + userSpecifiedAssetIdsEmpty :: Bool + userSpecifiedAssetIdsEmpty = (&&) + (Set.null userSpecifiedAssetIds) + (not $ Set.null nonUserSpecifiedAssetIds) + +data TestDataForCollateNonUserSpecifiedAssetQuantities = + TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps + :: NonEmpty TokenMap + , userSpecifiedAssetIds + :: Set AssetId + , expectedResult + :: Map AssetId (NonEmpty TokenQuantity) + } + deriving (Eq, Generic) + +unit_collateNonUserSpecifiedAssetQuantities :: Spec +unit_collateNonUserSpecifiedAssetQuantities = + forM_ (zip [1..] tests) $ \(testNumber :: Int, test) -> do + let title = "Unit test #" <> show testNumber + it title $ property $ + collateNonUserSpecifiedAssetQuantities + (view #selectedInputMaps test) + (view #userSpecifiedAssetIds test) + === + (view #expectedResult test) + where + mkSelectedInputMaps :: [[(ByteString, Natural)]] -> NonEmpty TokenMap + mkSelectedInputMaps + = NE.fromList + . fmap (TokenMap.fromFlatList . fmap (uncurry mockAssetQuantity)) + + mkUserSpecifiedAssetIds :: [ByteString] -> Set AssetId + mkUserSpecifiedAssetIds + = Set.fromList . fmap mockAsset + + mkExpectedResult + :: [(ByteString, [Natural])] + -> Map AssetId (NonEmpty TokenQuantity) + mkExpectedResult + = fmap NE.fromList + . Map.fromList + . fmap (bimap mockAsset (fmap TokenQuantity)) + + tests :: [TestDataForCollateNonUserSpecifiedAssetQuantities] + tests = [test1, test2, test3, test4, test5, test6, test7, test8] + + test1 = TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps = mkSelectedInputMaps + [ [("A", 1)] + , [("A", 2)] + , [("A", 3)] + ] + , userSpecifiedAssetIds = mkUserSpecifiedAssetIds + [] + , expectedResult = mkExpectedResult + [ ("A", [1, 2, 3]) ] + } + + test2 = TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps = mkSelectedInputMaps + [ [("A", 1)] + , [("A", 2)] + , [("A", 3)] + ] + , userSpecifiedAssetIds = mkUserSpecifiedAssetIds + [ "A" ] + , expectedResult = mkExpectedResult + [] + } + + test3 = TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps = mkSelectedInputMaps + [ [("A", 1), ("B", 3) ] + , [ ("B", 4), ("C", 5)] + , [("A", 2), ("C", 6)] + ] + , userSpecifiedAssetIds = mkUserSpecifiedAssetIds + [] + , expectedResult = mkExpectedResult + [ ("A", [1, 2]) + , ("B", [3, 4]) + , ("C", [5, 6]) + ] + } + + test4 = TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps = mkSelectedInputMaps + [ [("A", 1), ("B", 3) ] + , [ ("B", 4), ("C", 5)] + , [("A", 2), ("C", 6)] + ] + , userSpecifiedAssetIds = mkUserSpecifiedAssetIds + [ "A" ] + , expectedResult = mkExpectedResult + [ ("B", [3, 4]) + , ("C", [5, 6]) + ] + } + + test5 = TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps = mkSelectedInputMaps + [ [("A", 1), ("B", 3) ] + , [ ("B", 4), ("C", 5)] + , [("A", 2), ("C", 6)] + ] + , userSpecifiedAssetIds = mkUserSpecifiedAssetIds + [ "B" ] + , expectedResult = mkExpectedResult + [ ("A", [1, 2]) + , ("C", [5, 6]) + ] + } + + test6 = TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps = mkSelectedInputMaps + [ [("A", 1), ("B", 3) ] + , [ ("B", 4), ("C", 5)] + , [("A", 2), ("C", 6)] + ] + , userSpecifiedAssetIds = mkUserSpecifiedAssetIds + [ "C" ] + , expectedResult = mkExpectedResult + [ ("A", [1, 2]) + , ("B", [3, 4]) + ] + } + + test7 = TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps = mkSelectedInputMaps + [ [("A", 1) ] + , [ ("B", 3)] + , [("A", 2) ] + , [ ("B", 4)] + ] + , userSpecifiedAssetIds = mkUserSpecifiedAssetIds + [ "A" ] + , expectedResult = mkExpectedResult + [ ("B", [3, 4]) ] + } + + test8 = TestDataForCollateNonUserSpecifiedAssetQuantities + { selectedInputMaps = mkSelectedInputMaps + [ [("A", 1) ] + , [ ("B", 3)] + , [("A", 2) ] + , [ ("B", 4)] + ] + , userSpecifiedAssetIds = mkUserSpecifiedAssetIds + [ "B" ] + , expectedResult = mkExpectedResult + [ ("A", [1, 2]) ] + } + -------------------------------------------------------------------------------- -- Assigning coins to change maps -------------------------------------------------------------------------------- @@ -2027,59 +2262,64 @@ unit_makeChangeForCoin = ] -------------------------------------------------------------------------------- --- Making change for unknown assets +-- Making change for a single non-user-specified asset -------------------------------------------------------------------------------- prop_makeChangeForNonUserSpecifiedAsset_sum - :: NonEmpty TokenMap + :: NonEmpty () -> (AssetId, NonEmpty TokenQuantity) -> Property -prop_makeChangeForNonUserSpecifiedAsset_sum weights (asset, quantities) = +prop_makeChangeForNonUserSpecifiedAsset_sum n (asset, quantities) = F.fold quantities === F.fold ((`TokenMap.getQuantity` asset) <$> changes) where - changes = makeChangeForNonUserSpecifiedAsset weights (asset, quantities) + changes = makeChangeForNonUserSpecifiedAsset n (asset, quantities) prop_makeChangeForNonUserSpecifiedAsset_order - :: NonEmpty TokenMap + :: NonEmpty () -> (AssetId, NonEmpty TokenQuantity) -> Property -prop_makeChangeForNonUserSpecifiedAsset_order weights assetQuantities = +prop_makeChangeForNonUserSpecifiedAsset_order n assetQuantities = property $ inAscendingPartialOrder - $ makeChangeForNonUserSpecifiedAsset weights assetQuantities + $ makeChangeForNonUserSpecifiedAsset n assetQuantities prop_makeChangeForNonUserSpecifiedAsset_length - :: NonEmpty TokenMap + :: NonEmpty () -> (AssetId, NonEmpty TokenQuantity) -> Property -prop_makeChangeForNonUserSpecifiedAsset_length weights surplus = - F.length changes === F.length weights +prop_makeChangeForNonUserSpecifiedAsset_length n surplus = + F.length changes === F.length n where - changes = makeChangeForNonUserSpecifiedAsset weights surplus + changes = makeChangeForNonUserSpecifiedAsset n surplus unit_makeChangeForNonUserSpecifiedAsset :: [Expectation] unit_makeChangeForNonUserSpecifiedAsset = - [ makeChangeForNonUserSpecifiedAsset weights surplus `shouldBe` expectation - | (weights, surplus, expectation) <- matrix + [ makeChangeForNonUserSpecifiedAsset + (mkChangeMapCount changeMapCount) surplus + `shouldBe` expectation + | (changeMapCount, surplus, expectation) <- matrix ] where matrix = - [ ( m [(assetA, q 1)] :| [m [(assetB, q 1)]] - , (assetC, q <$> 1 :| [1]) - , m [(assetC, q 1)] :| [m [(assetC, q 1)]] + [ ( 2 + , (assetA, q <$> 1 :| [1]) + , m [(assetA, q 1)] :| [m [(assetA, q 1)]] ) - , ( m [(assetA, q 1)] :| [m [(assetB, q 1)]] - , (assetC, q <$> 1 :| [1, 1]) - , m [(assetC, q 1)] :| [m [(assetC, q 2)]] + , ( 2 + , (assetA, q <$> 1 :| [1, 1]) + , m [(assetA, q 1)] :| [m [(assetA, q 2)]] ) - , ( m [(assetA, q 1)] :| [m [(assetB, q 1)]] - , (assetC, q <$> 1 :| []) - , m [(assetC, q 0)] :| [m [(assetC, q 1)]] + , ( 2 + , (assetA, q <$> 1 :| []) + , m [(assetA, q 0)] :| [m [(assetA, q 1)]] ) ] + mkChangeMapCount :: Int -> NonEmpty () + mkChangeMapCount n = NE.fromList $ replicate n () + q :: Natural -> TokenQuantity q = TokenQuantity @@ -2089,11 +2329,250 @@ unit_makeChangeForNonUserSpecifiedAsset = assetA :: AssetId assetA = AssetId (UnsafeTokenPolicyId $ Hash "A") (UnsafeTokenName "1") - assetB :: AssetId - assetB = AssetId (UnsafeTokenPolicyId $ Hash "B") (UnsafeTokenName "") +-------------------------------------------------------------------------------- +-- Making change for multiple non-user-specified assets +-------------------------------------------------------------------------------- - assetC :: AssetId - assetC = AssetId (UnsafeTokenPolicyId $ Hash "A") (UnsafeTokenName "2") +checkCoverageFor_makeChangeForNonUserSpecifiedAssets + :: NonEmpty () + -> Map AssetId (NonEmpty TokenQuantity) + -> Property + -> Property +checkCoverageFor_makeChangeForNonUserSpecifiedAssets n assetQuantityMap prop = + checkCoverage $ + + -- Number of distinct assets: + cover 1 (Map.size assetQuantityMap == 1) + "number of distinct assets == 1" $ + cover 50 (Map.size assetQuantityMap >= 2) + "number of distinct assets >= 2" $ + cover 10 (Map.size assetQuantityMap >= 4) + "number of distinct assets >= 4" $ + + -- Number of change maps: + cover 1 (length n == 1) + "number of change maps == 1" $ + cover 50 (length n >= 2) + "number of change maps >= 2" $ + cover 10 (length n >= 4) + "number of change maps >= 4" $ + + -- Largest number of distinct token quantities for a given asset: + cover 1 (largestTokenQuantityCount == 1) + "largest number of token quantities == 1" $ + cover 50 (largestTokenQuantityCount >= 2) + "largest number of token quantities >= 2" $ + cover 10 (largestTokenQuantityCount >= 4) + "largest number of token quantities >= 4" + + prop + where + largestTokenQuantityCount :: Int + largestTokenQuantityCount = maximum (length <$> F.toList (assetQuantityMap)) + +prop_makeChangeForNonUserSpecifiedAssets_length + :: NonEmpty () + -> NonEmpty (AssetId, NonEmpty TokenQuantity) + -> Property +prop_makeChangeForNonUserSpecifiedAssets_length n assetQuantities = + checkCoverageFor_makeChangeForNonUserSpecifiedAssets n assetQuantityMap $ + lengthActual === lengthExpected + where + assetQuantityMap :: Map AssetId (NonEmpty TokenQuantity) + assetQuantityMap = Map.fromList (F.toList assetQuantities) + + lengthActual :: Int + lengthActual = length + (makeChangeForNonUserSpecifiedAssets n assetQuantityMap) + + lengthExpected :: Int + lengthExpected = length n + +prop_makeChangeForNonUserSpecifiedAssets_order + :: NonEmpty () + -> NonEmpty (AssetId, NonEmpty TokenQuantity) + -> Property +prop_makeChangeForNonUserSpecifiedAssets_order n assetQuantities = + checkCoverageFor_makeChangeForNonUserSpecifiedAssets n assetQuantityMap $ + property $ inAscendingPartialOrder result + where + assetQuantityMap :: Map AssetId (NonEmpty TokenQuantity) + assetQuantityMap = Map.fromList (F.toList assetQuantities) + + result :: NonEmpty TokenMap + result = makeChangeForNonUserSpecifiedAssets n assetQuantityMap + +prop_makeChangeForNonUserSpecifiedAssets_sum + :: NonEmpty () + -> NonEmpty (AssetId, NonEmpty TokenQuantity) + -> Property +prop_makeChangeForNonUserSpecifiedAssets_sum n assetQuantities = + checkCoverageFor_makeChangeForNonUserSpecifiedAssets n assetQuantityMap $ + sumActual === sumExpected + where + assetQuantityMap :: Map AssetId (NonEmpty TokenQuantity) + assetQuantityMap = Map.fromList (F.toList assetQuantities) + + sumActual :: TokenMap + sumActual = + F.fold (makeChangeForNonUserSpecifiedAssets n assetQuantityMap) + + sumExpected :: TokenMap + sumExpected = + TokenMap.fromFlatList $ Map.toList $ F.fold <$> assetQuantityMap + +data TestDataForMakeChangeForNonUserSpecifiedAssets = + TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount + :: NonEmpty () + , nonUserSpecifiedAssetQuantities + :: Map AssetId (NonEmpty TokenQuantity) + , expectedResult + :: NonEmpty TokenMap + } + deriving (Eq, Generic) + +unit_makeChangeForNonUserSpecifiedAssets :: Spec +unit_makeChangeForNonUserSpecifiedAssets = + forM_ (zip [1..] tests) $ \(testNumber :: Int, test) -> do + let title = "Unit test #" <> show testNumber + it title $ property $ + makeChangeForNonUserSpecifiedAssets + (view #changeMapCount test) + (view #nonUserSpecifiedAssetQuantities test) + === + (view #expectedResult test) + where + mkChangeMapCount :: Int -> NonEmpty () + mkChangeMapCount n = NE.fromList $ replicate n () + + mkNonUserSpecifiedAssetQuantities + :: [(ByteString, [Natural])] + -> Map AssetId (NonEmpty TokenQuantity) + mkNonUserSpecifiedAssetQuantities = + Map.fromList . fmap (bimap mockAsset (NE.fromList . fmap TokenQuantity)) + + mkExpectedResult + :: [[(ByteString, Natural)]] + -> NonEmpty TokenMap + mkExpectedResult + = NE.fromList + . fmap (TokenMap.fromFlatList . fmap (uncurry mockAssetQuantity)) + + tests :: [TestDataForMakeChangeForNonUserSpecifiedAssets] + tests = [test1, test2, test3, test4, test5, test6, test7, test8] + + test1 = TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount = mkChangeMapCount + 1 + , nonUserSpecifiedAssetQuantities = mkNonUserSpecifiedAssetQuantities + [ ("A", [1]) + , ("B", [3, 2, 1]) + ] + , expectedResult = mkExpectedResult + [ [("A", 1), ("B", 6)] ] + } + + test2 = TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount = mkChangeMapCount + 2 + , nonUserSpecifiedAssetQuantities = mkNonUserSpecifiedAssetQuantities + [ ("A", [1]) + , ("B", [3, 2, 1]) + ] + , expectedResult = mkExpectedResult + [ [ ("B", 3)] + , [("A", 1), ("B", 3)] + ] + } + + test3 = TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount = mkChangeMapCount + 3 + , nonUserSpecifiedAssetQuantities = mkNonUserSpecifiedAssetQuantities + [ ("A", [1]) + , ("B", [3, 2, 1]) + ] + , expectedResult = mkExpectedResult + [ [ ("B", 1)] + , [ ("B", 2)] + , [("A", 1), ("B", 3)] + ] + } + + test4 = TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount = mkChangeMapCount + 4 + , nonUserSpecifiedAssetQuantities = mkNonUserSpecifiedAssetQuantities + [ ("A", [1]) + , ("B", [3, 2, 1]) + ] + , expectedResult = mkExpectedResult + [ [ ] + , [ ("B", 1)] + , [ ("B", 2)] + , [("A", 1), ("B", 3)] + ] + } + + test5 = TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount = mkChangeMapCount + 1 + , nonUserSpecifiedAssetQuantities = mkNonUserSpecifiedAssetQuantities + [ ("A", [4, 1, 3, 2]) + , ("B", [9, 1, 8, 2, 7, 3, 6, 4, 5]) + ] + , expectedResult = mkExpectedResult + [ [("A", 10), ("B", 45)] ] + } + + test6 = TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount = mkChangeMapCount + 2 + , nonUserSpecifiedAssetQuantities = mkNonUserSpecifiedAssetQuantities + [ ("A", [4, 1, 3, 2]) + , ("B", [9, 1, 8, 2, 7, 3, 6, 4, 5]) + ] + , expectedResult = mkExpectedResult + [ [("A", 4), ("B", 18)] + , [("A", 6), ("B", 27)] + ] + } + + test7 = TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount = mkChangeMapCount + 4 + , nonUserSpecifiedAssetQuantities = mkNonUserSpecifiedAssetQuantities + [ ("A", [4, 1, 3, 2]) + , ("B", [9, 1, 8, 2, 7, 3, 6, 4, 5]) + ] + , expectedResult = mkExpectedResult + [ [("A", 1), ("B", 9)] + , [("A", 2), ("B", 9)] + , [("A", 3), ("B", 12)] + , [("A", 4), ("B", 15)] + ] + } + + test8 = TestDataForMakeChangeForNonUserSpecifiedAssets + { changeMapCount = mkChangeMapCount + 9 + , nonUserSpecifiedAssetQuantities = mkNonUserSpecifiedAssetQuantities + [ ("A", [4, 1, 3, 2]) + , ("B", [9, 1, 8, 2, 7, 3, 6, 4, 5]) + ] + , expectedResult = mkExpectedResult + [ [ ("B", 1)] + , [ ("B", 2)] + , [ ("B", 3)] + , [ ("B", 4)] + , [ ("B", 5)] + , [("A", 1), ("B", 6)] + , [("A", 2), ("B", 7)] + , [("A", 3), ("B", 8)] + , [("A", 4), ("B", 9)] + ] + } -------------------------------------------------------------------------------- -- Making change for known assets diff --git a/lib/shelley/src/Cardano/Wallet/Shelley/Api/Server.hs b/lib/shelley/src/Cardano/Wallet/Shelley/Api/Server.hs index dab6b450fc6..519a6567c1f 100644 --- a/lib/shelley/src/Cardano/Wallet/Shelley/Api/Server.hs +++ b/lib/shelley/src/Cardano/Wallet/Shelley/Api/Server.hs @@ -132,6 +132,8 @@ import Cardano.Wallet.Api.Types , ApiHealthCheck (..) , ApiMaintenanceAction (..) , ApiMaintenanceActionPostData (..) + , ApiPostAccountKeyData (..) + , ApiPostAccountKeyDataWithPurpose (..) , ApiSelectCoinsAction (..) , ApiSelectCoinsData (..) , ApiStakePool @@ -524,8 +526,12 @@ server byron icarus shelley multisig spl ntp = :: ApiLayer (SharedState n SharedKey) SharedKey -> Server SharedWalletKeys sharedWalletKeys apilayer = derivePublicKey apilayer ApiVerificationKeyShared - :<|> postAccountPublicKey apilayer ApiAccountKeyShared + :<|> (\wid ix p -> postAccountPublicKey apilayer ApiAccountKeyShared wid ix (toKeyDataPurpose p) ) :<|> getAccountPublicKey apilayer ApiAccountKeyShared + where + toKeyDataPurpose :: ApiPostAccountKeyData -> ApiPostAccountKeyDataWithPurpose + toKeyDataPurpose (ApiPostAccountKeyData p f) = + ApiPostAccountKeyDataWithPurpose p f Nothing sharedAddresses :: ApiLayer (SharedState n SharedKey) SharedKey diff --git a/specifications/api/swagger.yaml b/specifications/api/swagger.yaml index 8e650d347ca..1aa097f74be 100644 --- a/specifications/api/swagger.yaml +++ b/specifications/api/swagger.yaml @@ -2465,6 +2465,16 @@ components: passphrase: *walletPassphrase format: *keyExtended + ApiPostAccountKeyDataWithPurpose: &ApiPostAccountKeyDataWithPurpose + type: object + required: + - passphrase + - format + properties: + passphrase: *walletPassphrase + format: *keyExtended + purpose: *derivationSegment + ApiSettingsPutData: &ApiSettingsPutData type: object properties: @@ -4668,6 +4678,10 @@ paths: Derive an account public key for any account index. For this key derivation to be possible, the wallet must have been created from mnemonic. + It is possible to use the optional `purpose` field to override that branch of the derivation path + with different hardened derivation index. If that field is omitted, the default purpose + for Cardano wallets (`1852H`) will be used. + Note: Only _Hardened_ indexes are supported by this endpoint. parameters: - *parametersWalletId @@ -4676,7 +4690,7 @@ paths: required: true content: application/json: - schema: *ApiPostAccountKeyData + schema: *ApiPostAccountKeyDataWithPurpose responses: *responsesPostAccountKey /wallets/{walletId}/keys: