diff --git a/lib/core-integration/src/Test/Integration/Framework/DSL.hs b/lib/core-integration/src/Test/Integration/Framework/DSL.hs index b3c820e85ed..cc6eb2ca5f0 100644 --- a/lib/core-integration/src/Test/Integration/Framework/DSL.hs +++ b/lib/core-integration/src/Test/Integration/Framework/DSL.hs @@ -73,8 +73,10 @@ module Test.Integration.Framework.DSL , getFromResponseList , json , joinStakePool + , joinStakePoolUnsigned , delegationFee , quitStakePool + , quitStakePoolUnsigned , selectCoins , listAddresses , listTransactions @@ -143,6 +145,9 @@ module Test.Integration.Framework.DSL , postExternalTransactionViaCLI , deleteTransactionViaCLI , getTransactionViaCLI + + -- utilites + , getRetirementEpoch ) where import Cardano.CLI @@ -170,6 +175,7 @@ import Cardano.Wallet.Api.Types , ApiFee , ApiNetworkInformation , ApiNetworkParameters (..) + , ApiStakePool , ApiT (..) , ApiTransaction , ApiTxId (ApiTxId) @@ -1221,6 +1227,24 @@ joinStakePool ctx p (w, pass) = do request @(ApiTransaction n) ctx (Link.joinStakePool (Identity p) w) Default payload +joinStakePoolUnsigned + :: forall n style t w. + ( HasType (ApiT WalletId) w + , DecodeAddress n + , EncodeAddress n + , Link.Discriminate style + ) + => Context t + -> w + -> ApiT PoolId + -> IO (HTTP.Status, Either RequestException (ApiCoinSelection n)) +joinStakePoolUnsigned ctx w pid = do + let payload = Json [aesonQQ| { + "delegation_action": { "action": "join", "pool": #{pid} } + } |] + request @(ApiCoinSelection n) ctx + (Link.selectCoins @style w) Default payload + quitStakePool :: forall n t w. ( HasType (ApiT WalletId) w @@ -1237,6 +1261,23 @@ quitStakePool ctx (w, pass) = do request @(ApiTransaction n) ctx (Link.quitStakePool w) Default payload +quitStakePoolUnsigned + :: forall n style t w. + ( HasType (ApiT WalletId) w + , DecodeAddress n + , EncodeAddress n + , Link.Discriminate style + ) + => Context t + -> w + -> IO (HTTP.Status, Either RequestException (ApiCoinSelection n)) +quitStakePoolUnsigned ctx w = do + let payload = Json [aesonQQ| { + "delegation_action": { "action": "quit" } + } |] + request @(ApiCoinSelection n) ctx + (Link.selectCoins @style w) Default payload + selectCoins :: forall n style t w. ( HasType (ApiT WalletId) w @@ -1854,3 +1895,7 @@ delegating delegating pidActive nexts = (notDelegating nexts) { active = ApiWalletDelegationNext Delegating (Just pidActive) Nothing } + + +getRetirementEpoch :: ApiStakePool -> Maybe EpochNo +getRetirementEpoch = fmap (view (#epochNumber . #getApiT)) . view #retirement diff --git a/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/StakePools.hs b/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/StakePools.hs index e2690ea71fd..7bd2c35f8b0 100644 --- a/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/StakePools.hs +++ b/lib/core-integration/src/Test/Integration/Scenario/API/Shelley/StakePools.hs @@ -16,7 +16,8 @@ module Test.Integration.Scenario.API.Shelley.StakePools import Prelude import Cardano.Wallet.Api.Types - ( ApiStakePool + ( ApiCertificate (JoinPool, QuitPool, RegisterRewardAccount) + , ApiStakePool , ApiT (..) , ApiTransaction , ApiWallet @@ -35,11 +36,11 @@ import Cardano.Wallet.Primitive.Fee import Cardano.Wallet.Primitive.Types ( Coin (..) , Direction (..) - , EpochNo (..) , PoolId (..) , StakePoolMetadata (..) , StakePoolTicker (..) , TxStatus (..) + , decodePoolIdBech32 ) import Cardano.Wallet.Unsafe ( unsafeFromHex, unsafeMkPercentage ) @@ -52,9 +53,11 @@ import Data.Generics.Internal.VL.Lens import Data.IORef ( readIORef ) import Data.List - ( sortOn ) + ( find, sortOn ) +import Data.List.NonEmpty + ( NonEmpty (..) ) import Data.Maybe - ( fromMaybe, listToMaybe, mapMaybe ) + ( fromMaybe, isJust, isNothing, listToMaybe, mapMaybe ) import Data.Ord ( Down (..) ) import Data.Quantity @@ -89,13 +92,16 @@ import Test.Integration.Framework.DSL , fixtureWallet , fixtureWalletWith , getFromResponse + , getRetirementEpoch , getSlotParams , joinStakePool + , joinStakePoolUnsigned , json , listAddresses , minUTxOValue , notDelegating , quitStakePool + , quitStakePoolUnsigned , request , unsafeRequest , verify @@ -552,6 +558,144 @@ spec = describe "SHELLEY_STAKE_POOLS" $ do , expectField (#direction . #getApiT) (`shouldBe` Outgoing) ] + describe "STAKE_POOLS_JOIN_UNSIGNED_01" $ do + it "Can join a pool that's not retiring" $ \ctx -> do + nonRetiredPools <- eventually "One of the pools should retire." $ do + response <- listPools ctx arbitraryStake + + verify response [ expectListSize 3 ] + + pure $ getFromResponse Prelude.id response + + let reportError = error $ unlines + [ "Unable to find a non-retiring pool ID." + , "Test cluster pools:" + , unlines (showT <$> Set.toList testClusterPoolIds) + , "Non-retired pools:" + , unlines (show <$> nonRetiredPools) + ] + + let nonRetiringPoolId = (view #id) . fromMaybe reportError + . find (isNothing . getRetirementEpoch) + $ nonRetiredPools + + let isValidCerts (Just (RegisterRewardAccount{}:|[JoinPool{}])) = True + isValidCerts _ = False + + -- Join Pool + w <- fixtureWallet ctx + joinStakePoolUnsigned @n @'Shelley ctx w nonRetiringPoolId >>= \o -> do + verify o + [ expectResponseCode HTTP.status200 + , expectField #inputs (`shouldSatisfy` (not . null)) + , expectField #outputs (`shouldSatisfy` (not . null)) + , expectField #certificates (`shouldSatisfy` isValidCerts) + ] + + describe "STAKE_POOLS_JOIN_UNSIGNED_02" + $ it "Can join a pool that's retiring" $ \ctx -> do + nonRetiredPools <- eventually "One of the pools should retire." $ do + response <- listPools ctx arbitraryStake + + verify response [ expectListSize 3 ] + + pure $ getFromResponse Prelude.id response + let reportError = error $ unlines + [ "Unable to find a retiring pool ID." + , "Test cluster pools:" + , unlines (showT <$> Set.toList testClusterPoolIds) + , "Non-retired pools:" + , unlines (show <$> nonRetiredPools) + ] + + let retiringPoolId = (view #id) . fromMaybe reportError + . find (isJust . getRetirementEpoch) + $ nonRetiredPools + -- Join Pool + w <- fixtureWallet ctx + joinStakePoolUnsigned @n @'Shelley ctx w retiringPoolId >>= \o -> do + verify o + [ expectResponseCode HTTP.status200 + , expectField #inputs (`shouldSatisfy` (not . null)) + , expectField #outputs (`shouldSatisfy` (not . null)) + , expectField #certificates (`shouldSatisfy` (not . null)) + ] + + describe "STAKE_POOLS_JOIN_UNSIGNED_03" + $ it "Cannot join a pool that's retired" $ \ctx -> do + nonRetiredPoolIds <- eventually "One of the pools should retire." $ do + response <- listPools ctx arbitraryStake + verify response [ expectListSize 3 ] + getFromResponse Prelude.id response + & fmap (view (#id . #getApiT)) + & Set.fromList + & pure + let reportError = error $ unlines + [ "Unable to find a retired pool ID." + , "Test cluster pools:" + , unlines (showT <$> Set.toList testClusterPoolIds) + , "Non-retired pools:" + , unlines (showT <$> Set.toList nonRetiredPoolIds) + ] + let retiredPoolIds = + testClusterPoolIds `Set.difference` nonRetiredPoolIds + let retiredPoolId = + fromMaybe reportError $ listToMaybe $ Set.toList retiredPoolIds + w <- fixtureWallet ctx + r <- joinStakePoolUnsigned @n @'Shelley ctx w (ApiT retiredPoolId) + expectResponseCode HTTP.status404 r + expectErrorMessage (errMsg404NoSuchPool (toText retiredPoolId)) r + + describe "STAKE_POOLS_JOIN_UNSIGNED_04" + $ it "Cannot join a pool that's never existed" $ \ctx -> do + (Right non_existing_pool_id) <- pure $ decodePoolIdBech32 + "pool1y25deq9kldy9y9gfvrpw8zt05zsrfx84zjhugaxrx9ftvwdpua2" + w <- fixtureWallet ctx + r <- joinStakePoolUnsigned @n @'Shelley ctx w (ApiT non_existing_pool_id) + expectResponseCode HTTP.status404 r + expectErrorMessage (errMsg404NoSuchPool (toText non_existing_pool_id)) r + + describe "STAKE_POOLS_QUIT_UNSIGNED_01" + $ it "Can quit a joined pool" $ \ctx -> do + w <- fixtureWallet ctx + + pool:_ <- map (view #id) . snd <$> unsafeRequest @[ApiStakePool] + ctx (Link.listStakePools arbitraryStake) Empty + + joinStakePool @n ctx pool (w, fixturePassphrase) >>= flip verify + [ expectResponseCode HTTP.status202 + , expectField (#status . #getApiT) (`shouldBe` Pending) + , expectField (#direction . #getApiT) (`shouldBe` Outgoing) + ] + + eventually "Wallet is delegating to p1" $ do + request @ApiWallet ctx (Link.getWallet @'Shelley w) + Default Empty >>= flip verify + [ expectField #delegation (`shouldBe` delegating pool []) + ] + + let isValidCerts (Just (QuitPool{}:|[])) = True + isValidCerts _ = False + + -- Quit Pool + quitStakePoolUnsigned @n @'Shelley ctx w >>= \o -> do + verify o + [ expectResponseCode HTTP.status200 + , expectField #inputs (`shouldSatisfy` (not . null)) + , expectField #outputs (`shouldSatisfy` (not . null)) + , expectField #certificates (`shouldSatisfy` ((==1) . length)) + , expectField #certificates (`shouldSatisfy` isValidCerts) + ] + + describe "STAKE_POOLS_QUIT_UNSIGNED_02" + $ it "Cannot quit if not delegating" $ \ctx -> do + w <- fixtureWallet ctx + + quitStakePoolUnsigned @n @'Shelley ctx w >>= \r -> do + expectResponseCode HTTP.status403 r + expectErrorMessage "It seems that you're trying to retire from delegation although you're not even delegating, nor won't be in an immediate future" r + + describe "STAKE_POOLS_JOIN_01x - Fee boundary values" $ do it "STAKE_POOLS_JOIN_01x - \ \I can join if I have just the right amount" $ \ctx -> do @@ -688,12 +832,6 @@ spec = describe "SHELLEY_STAKE_POOLS" $ do response <- listPools ctx arbitraryStake expectResponseCode HTTP.status200 response - let getRetirementEpoch :: ApiStakePool -> Maybe EpochNo - getRetirementEpoch = - fmap (view (#epochNumber . #getApiT)) - . - view #retirement - let actualRetirementEpochs = getFromResponse Prelude.id response & fmap getRetirementEpoch diff --git a/lib/core/src/Cardano/Wallet.hs b/lib/core/src/Cardano/Wallet.hs index 2ab2a0834a4..ee57c72aa60 100644 --- a/lib/core/src/Cardano/Wallet.hs +++ b/lib/core/src/Cardano/Wallet.hs @@ -227,6 +227,7 @@ import Cardano.Wallet.Primitive.AddressDerivation , encryptPassphrase , liftIndex , preparePassphrase + , stakeDerivationPath ) import Cardano.Wallet.Primitive.AddressDerivation.Byron ( ByronKey, unsafeMkByronKeyFromMasterKey ) @@ -993,18 +994,20 @@ readChimericAccount ) => ctx -> WalletId - -> ExceptT ErrReadChimericAccount IO ChimericAccount + -> ExceptT ErrReadChimericAccount IO (ChimericAccount, NonEmpty DerivationIndex) readChimericAccount ctx wid = db & \DBLayer{..} -> do cp <- withExceptT ErrReadChimericAccountNoSuchWallet $ mapExceptT atomically $ withNoSuchWallet wid $ readCheckpoint (PrimaryKey wid) case testEquality (typeRep @s) (typeRep @shelley) of - Nothing -> throwE ErrReadChimericAccountNotAShelleyWallet - Just Refl -> pure - $ toChimericAccount - $ Seq.rewardAccountKey - $ getState cp + Nothing -> + throwE ErrReadChimericAccountNotAShelleyWallet + Just Refl -> do + let s = getState cp + let acct = toChimericAccount $ Seq.rewardAccountKey s + let path = stakeDerivationPath $ Seq.derivationPrefix s + pure (acct, path) where db = ctx ^. dbLayer @s @k @@ -1047,7 +1050,7 @@ manageRewardBalance _ ctx wid = db & \DBLayer{..} -> do watchNodeTip $ \bh -> do traceWith tr $ MsgRewardBalanceQuery bh query <- runExceptT $ do - acct <- withExceptT ErrFetchRewardsReadChimericAccount $ + (acct, _) <- withExceptT ErrFetchRewardsReadChimericAccount $ readChimericAccount @ctx @s @k @n ctx wid queryRewardBalance @ctx @t ctx acct traceWith tr $ MsgRewardBalanceResult query @@ -1659,27 +1662,20 @@ signTx ctx wid pwd md (UnsignedTx inpsNE outsNE) = db & \DBLayer{..} -> do -- | Makes a fully-resolved coin selection for the given set of payments. selectCoinsExternal - :: forall ctx s t k e. + :: forall ctx s k e resolvedInput. ( GenChange s , HasDBLayer s k ctx - , HasLogger WalletLog ctx - , HasTransactionLayer t k ctx - , e ~ ErrValidateSelection t , IsOurs s Address + , resolvedInput ~ (TxIn, TxOut, NonEmpty DerivationIndex) ) => ctx -> WalletId -> ArgGenChange s - -> NonEmpty TxOut - -> Quantity "lovelace" Word64 - -> Maybe TxMetadata - -> ExceptT - (ErrSelectCoinsExternal e) - IO - (UnsignedTx (TxIn, TxOut, NonEmpty DerivationIndex)) -selectCoinsExternal ctx wid argGenChange payments withdrawal md = do - cs <- withExceptT ErrSelectCoinsExternalUnableToMakeSelection $ - selectCoinsForPayment @ctx @s @t @k @e ctx wid payments withdrawal md + -> ExceptT (ErrSelectCoinsExternal e) IO CoinSelection + -> ExceptT (ErrSelectCoinsExternal e) IO (UnsignedTx resolvedInput) +selectCoinsExternal ctx wid argGenChange selectCoins = do + cs <- selectCoins + (cs', s') <- db & \DBLayer{..} -> withExceptT ErrSelectCoinsExternalNoSuchWallet $ mapExceptT atomically $ do @@ -1687,48 +1683,21 @@ selectCoinsExternal ctx wid argGenChange payments withdrawal md = do (cs', s') <- assignChangeAddresses argGenChange cs (getState cp) putCheckpoint (PrimaryKey wid) (updateState s' cp) pure (cs', s') + UnsignedTx - <$> (fullyQualifiedInputs s' cs' >>= flip ensureNonEmpty - ErrSelectCoinsExternalUnableToAssignInputs) + <$> (fullyQualifiedInputs s' cs' + (ErrSelectCoinsExternalUnableToAssignInputs cs')) <*> ensureNonEmpty (outputs cs') - ErrSelectCoinsExternalUnableToAssignOutputs + (ErrSelectCoinsExternalUnableToAssignOutputs cs') where db = ctx ^. dbLayer @s @k - fullyQualifiedInputs - :: s - -> CoinSelection - -> ExceptT - (ErrSelectCoinsExternal e) - IO - [(TxIn, TxOut, NonEmpty DerivationIndex)] - fullyQualifiedInputs s cs = - traverse withDerivationPath (inputs cs) - where - withDerivationPath - :: (TxIn, TxOut) - -> ExceptT - (ErrSelectCoinsExternal e) - IO - (TxIn, TxOut, NonEmpty DerivationIndex) - withDerivationPath (txin, txout) = do - case fst $ isOurs (address txout) s of - Nothing -> throwE $ ErrSelectCoinsExternalUnableToAssignInputs wid - Just path -> pure (txin, txout, path) - - ensureNonEmpty - :: forall a. [a] - -> (WalletId -> ErrSelectCoinsExternal e) - -> ExceptT (ErrSelectCoinsExternal e) IO (NonEmpty a) - ensureNonEmpty mxs err = case NE.nonEmpty mxs of - Nothing -> throwE $ err wid - Just xs -> pure xs - data ErrSelectCoinsExternal e = ErrSelectCoinsExternalNoSuchWallet ErrNoSuchWallet - | ErrSelectCoinsExternalUnableToMakeSelection (ErrSelectForPayment e) - | ErrSelectCoinsExternalUnableToAssignInputs WalletId - | ErrSelectCoinsExternalUnableToAssignOutputs WalletId + | ErrSelectCoinsExternalForPayment (ErrSelectForPayment e) + | ErrSelectCoinsExternalForDelegation ErrSelectForDelegation + | ErrSelectCoinsExternalUnableToAssignInputs CoinSelection + | ErrSelectCoinsExternalUnableToAssignOutputs CoinSelection deriving (Eq, Show) signDelegation @@ -1972,19 +1941,11 @@ getTransaction ctx wid tid = db & \DBLayer{..} -> do Delegation -------------------------------------------------------------------------------} --- | Helper function to factor necessary logic for joining a stake pool. joinStakePool - :: forall ctx s t k. + :: forall ctx s k n. ( HasDBLayer s k ctx , HasLogger WalletLog ctx - , HasNetworkLayer t ctx - , HasTransactionLayer t k ctx - , IsOwned s k - , IsOurs s ChimericAccount - , GenChange s - , HardDerivation k - , AddressIndexDerivationType k ~ 'Soft - , WalletKey k + , s ~ SeqState n k ) => ctx -> W.EpochNo @@ -1992,16 +1953,16 @@ joinStakePool -> PoolId -> PoolLifeCycleStatus -> WalletId - -> ArgGenChange s - -> Passphrase "raw" - -> ExceptT ErrJoinStakePool IO (Tx, TxMeta, UTCTime) -joinStakePool ctx currentEpoch knownPools pid poolStatus wid argGenChange pwd = + -> ExceptT ErrJoinStakePool IO DelegationAction +joinStakePool ctx currentEpoch knownPools pid poolStatus wid = db & \DBLayer{..} -> do - - (isKeyReg, walMeta) <- mapExceptT atomically - $ withExceptT ErrJoinStakePoolNoSuchWallet - $ (,) <$> isStakeKeyRegistered (PrimaryKey wid) - <*> withNoSuchWallet wid (readWalletMeta (PrimaryKey wid)) + (walMeta, isKeyReg) <- mapExceptT atomically $ do + walMeta <- withExceptT ErrJoinStakePoolNoSuchWallet + $ withNoSuchWallet wid + $ readWalletMeta (PrimaryKey wid) + isKeyReg <- withExceptT ErrJoinStakePoolNoSuchWallet + $ isStakeKeyRegistered (PrimaryKey wid) + pure (walMeta, isKeyReg) let mRetirementEpoch = view #retirementEpoch <$> W.getPoolRetirementCertificate poolStatus @@ -2011,68 +1972,40 @@ joinStakePool ctx currentEpoch knownPools pid poolStatus wid argGenChange pwd = withExceptT ErrJoinStakePoolCannotJoin $ except $ guardJoin knownPools (walMeta ^. #delegation) pid retirementInfo - let action = if isKeyReg then Join pid else RegisterKeyAndJoin pid liftIO $ traceWith tr $ MsgIsStakeKeyRegistered isKeyReg - selection <- withExceptT ErrJoinStakePoolSelectCoin $ - selectCoinsForDelegation @ctx @s @t @k ctx wid action - - (tx, txMeta, txTime, sealedTx) <- - withExceptT ErrJoinStakePoolSignDelegation $ - signDelegation - @ctx @s @t @k ctx wid argGenChange pwd selection action - - withExceptT ErrJoinStakePoolSubmitTx $ - submitTx @ctx @s @t @k ctx wid (tx, txMeta, sealedTx) - - pure (tx, txMeta, txTime) + return $ if isKeyReg + then Join pid + else RegisterKeyAndJoin pid where db = ctx ^. dbLayer @s @k tr = ctx ^. logger -- | Helper function to factor necessary logic for quitting a stake pool. quitStakePool - :: forall ctx s t k. + :: forall ctx s k n. ( HasDBLayer s k ctx - , HasLogger WalletLog ctx - , HasNetworkLayer t ctx - , HasTransactionLayer t k ctx - , IsOwned s k - , IsOurs s ChimericAccount - , GenChange s - , HardDerivation k - , AddressIndexDerivationType k ~ 'Soft - , WalletKey k + , s ~ SeqState n k ) => ctx -> WalletId - -> ArgGenChange s - -> Passphrase "raw" - -> ExceptT ErrQuitStakePool IO (Tx, TxMeta, UTCTime) -quitStakePool ctx wid argGenChange pwd = db & \DBLayer{..} -> do - walMeta <- mapExceptT atomically $ withExceptT ErrQuitStakePoolNoSuchWallet $ - withNoSuchWallet wid $ readWalletMeta (PrimaryKey wid) + -> ExceptT ErrQuitStakePool IO DelegationAction +quitStakePool ctx wid = db & \DBLayer{..} -> do + walMeta <- mapExceptT atomically + $ withExceptT ErrQuitStakePoolNoSuchWallet + $ withNoSuchWallet wid + $ readWalletMeta (PrimaryKey wid) + + rewards <- liftIO + $ fetchRewardBalance @ctx @s @k ctx wid - rewards <- liftIO $ fetchRewardBalance @ctx @s @k ctx wid withExceptT ErrQuitStakePoolCannotQuit $ except $ guardQuit (walMeta ^. #delegation) rewards - let action = Quit - - selection <- withExceptT ErrQuitStakePoolSelectCoin $ - selectCoinsForDelegation @ctx @s @t @k ctx wid action - - (tx, txMeta, txTime, sealedTx) <- withExceptT ErrQuitStakePoolSignDelegation $ - signDelegation @ctx @s @t @k ctx wid argGenChange pwd selection action - - withExceptT ErrQuitStakePoolSubmitTx $ - submitTx @ctx @s @t @k ctx wid (tx, txMeta, sealedTx) - - pure (tx, txMeta, txTime) + pure Quit where db = ctx ^. dbLayer @s @k - {------------------------------------------------------------------------------- Fee Estimation -------------------------------------------------------------------------------} @@ -2331,6 +2264,8 @@ data ErrStartTimeLaterThanEndTime = ErrStartTimeLaterThanEndTime data ErrSelectForDelegation = ErrSelectForDelegationNoSuchWallet ErrNoSuchWallet | ErrSelectForDelegationFee ErrAdjustForFee + | ErrSelectForDelegationUnableToAssignInputs ErrNoSuchWallet + | ErrSelectForDelegationUnableToAssignOutputs ErrNoSuchWallet deriving (Show, Eq) -- | Errors that can occur when signing a delegation certificate. @@ -2347,6 +2282,8 @@ data ErrJoinStakePool | ErrJoinStakePoolSignDelegation ErrSignDelegation | ErrJoinStakePoolSubmitTx ErrSubmitTx | ErrJoinStakePoolCannotJoin ErrCannotJoin + | ErrJoinStakePoolUnableToAssignInputs CoinSelection + | ErrJoinStakePoolUnableToAssignOutputs CoinSelection deriving (Generic, Eq, Show) data ErrQuitStakePool @@ -2355,6 +2292,8 @@ data ErrQuitStakePool | ErrQuitStakePoolSignDelegation ErrSignDelegation | ErrQuitStakePoolSubmitTx ErrSubmitTx | ErrQuitStakePoolCannotQuit ErrCannotQuit + | ErrQuitStakePoolUnableToAssignInputs CoinSelection + | ErrQuitStakePoolUnableToAssignOutputs CoinSelection deriving (Generic, Eq, Show) -- | Errors that can occur when fetching the reward balance of a wallet @@ -2494,6 +2433,33 @@ guardCoinSelection minUtxoValue cs@CoinSelection{outputs, change} = do unless (L.null invalidTxOuts) $ Left (ErrUTxOTooSmall (getCoin minUtxoValue) (getCoin <$> invalidTxOuts)) +fullyQualifiedInputs + :: forall s e. + (IsOurs s Address) + => s + -> CoinSelection + -> e + -> ExceptT e IO (NonEmpty (TxIn, TxOut, NonEmpty DerivationIndex)) +fullyQualifiedInputs s cs e = + traverse withDerivationPath (inputs cs) >>= flip ensureNonEmpty e + where + withDerivationPath + :: (TxIn, TxOut) + -> ExceptT e IO (TxIn, TxOut, NonEmpty DerivationIndex) + withDerivationPath (txin, txout) = do + case fst $ isOurs (address txout) s of + Nothing -> throwE e + Just path -> pure (txin, txout, path) + +ensureNonEmpty + :: forall a e. + [a] + -> e + -> ExceptT e IO (NonEmpty a) +ensureNonEmpty mxs err = case NE.nonEmpty mxs of + Nothing -> throwE err + Just xs -> pure xs + {------------------------------------------------------------------------------- Logging -------------------------------------------------------------------------------} diff --git a/lib/core/src/Cardano/Wallet/Api/Server.hs b/lib/core/src/Cardano/Wallet/Api/Server.hs index 47fdaf605fd..a1b9913f17e 100644 --- a/lib/core/src/Cardano/Wallet/Api/Server.hs +++ b/lib/core/src/Cardano/Wallet/Api/Server.hs @@ -73,6 +73,8 @@ module Cardano.Wallet.Api.Server , putWalletPassphrase , quitStakePool , selectCoins + , selectCoinsForJoin + , selectCoinsForQuit -- * Internals , LiftHandler(..) @@ -176,7 +178,7 @@ import Cardano.Wallet.Api.Types , ApiPoolId (..) , ApiPostRandomAddressData (..) , ApiPutAddressesData (..) - , ApiSelectCoinsData (..) + , ApiSelectCoinsPayments , ApiSlotId (..) , ApiSlotReference (..) , ApiT (..) @@ -310,7 +312,7 @@ import Cardano.Wallet.Registry , workerResource ) import Cardano.Wallet.Transaction - ( TransactionLayer ) + ( DelegationAction (..), TransactionLayer ) import Cardano.Wallet.Unsafe ( unsafeRunExceptT ) import Control.Arrow @@ -352,7 +354,7 @@ import Data.Generics.Labels import Data.List ( isInfixOf, isSubsequenceOf, sortOn ) import Data.List.NonEmpty - ( NonEmpty ) + ( NonEmpty (..) ) import Data.Map.Strict ( Map ) import Data.Maybe @@ -1116,17 +1118,95 @@ selectCoins => ctx -> ArgGenChange s -> ApiT WalletId - -> ApiSelectCoinsData n + -> ApiSelectCoinsPayments n -> Handler (ApiCoinSelection n) -selectCoins ctx gen (ApiT wid) body = - fmap mkApiCoinSelection +selectCoins ctx genChange (ApiT wid) body = + fmap (mkApiCoinSelection Nothing) $ withWorkerCtx ctx wid liftE liftE $ \wrk -> do -- TODO: -- Allow representing withdrawals as part of external coin selections. let withdrawal = Quantity 0 let outs = coerceCoin <$> body ^. #payments - liftHandler $ W.selectCoinsExternal @_ @s @t @k wrk wid gen outs withdrawal Nothing + liftHandler + $ W.selectCoinsExternal @_ @s @k wrk wid genChange + $ withExceptT ErrSelectCoinsExternalForPayment + $ W.selectCoinsForPayment @_ @s @t @k wrk wid outs withdrawal Nothing + +selectCoinsForJoin + :: forall ctx e s t n k. + ( s ~ SeqState n k + , ctx ~ ApiLayer s t k + , Buildable e + , SoftDerivation k + , DelegationAddress n k + , MkKeyFingerprint k (Proxy n, k 'AddressK XPub) + , Typeable s + , Typeable n + ) + => ctx + -> IO (Set PoolId) + -- ^ Known pools + -- We could maybe replace this with a @IO (PoolId -> Bool)@ + -> (PoolId -> IO PoolLifeCycleStatus) + -> PoolId + -> WalletId + -> Handler (Api.ApiCoinSelection n) +selectCoinsForJoin ctx knownPools getPoolStatus pid wid = do + poolStatus <- liftIO (getPoolStatus pid) + pools <- liftIO knownPools + curEpoch <- getCurrentEpoch ctx + + (utx, action, path) <- withWorkerCtx ctx wid liftE liftE $ \wrk -> do + action <- liftHandler + $ W.joinStakePool @_ @s @k @n wrk curEpoch pools pid poolStatus wid + + utx <- liftHandler + $ W.selectCoinsExternal @_ @s @k @e wrk wid genChange + $ withExceptT ErrSelectCoinsExternalForDelegation + $ W.selectCoinsForDelegation @_ @s @t @k wrk wid action + + (_, path) <- liftHandler + $ W.readChimericAccount @_ @s @k @n wrk wid + + pure (utx, action, path) + + pure $ mkApiCoinSelection (Just (action, path)) utx + where + genChange = delegationAddress @n + +selectCoinsForQuit + :: forall ctx e s t n k. + ( s ~ SeqState n k + , ctx ~ ApiLayer s t k + , Buildable e + , SoftDerivation k + , DelegationAddress n k + , MkKeyFingerprint k (Proxy n, k 'AddressK XPub) + , Typeable s + , Typeable n + ) + => ctx + -> ApiT WalletId + -> Handler (Api.ApiCoinSelection n) +selectCoinsForQuit ctx (ApiT wid) = do + (utx, action, path) <- withWorkerCtx ctx wid liftE liftE $ \wrk -> do + action <- liftHandler + $ W.quitStakePool @_ @s @k @n wrk wid + + utx <- liftHandler + $ W.selectCoinsExternal @_ @s @k @e wrk wid genChange + $ withExceptT ErrSelectCoinsExternalForDelegation + $ W.selectCoinsForDelegation @_ @s @t @k wrk wid action + + (_, path) <- liftHandler + $ W.readChimericAccount @_ @s @k @n wrk wid + + pure (utx, action, path) + + pure $ mkApiCoinSelection (Just (action, path)) utx + where + genChange = delegationAddress @n {------------------------------------------------------------------------------- Addresses @@ -1243,7 +1323,7 @@ postTransaction ctx genChange (ApiT wid) body = do pure (Quantity 0, selfRewardCredentials) Just SelfWithdrawal -> do - acct <- liftHandler $ W.readChimericAccount @_ @s @k @n wrk wid + (acct, _) <- liftHandler $ W.readChimericAccount @_ @s @k @n wrk wid wdrl <- liftHandler $ W.queryRewardBalance @_ @t wrk acct (, selfRewardCredentials) <$> liftIO (W.readNextWithdrawal @_ @s @t @k wrk wid wdrl) @@ -1374,7 +1454,7 @@ postTransactionFee ctx (ApiT wid) body = do pure (Quantity 0) Just SelfWithdrawal -> do - acct <- liftHandler $ W.readChimericAccount @_ @s @k @n wrk wid + (acct, _) <- liftHandler $ W.readChimericAccount @_ @s @k @n wrk wid wdrl <- liftHandler $ W.queryRewardBalance @_ @t wrk acct liftIO $ W.readNextWithdrawal @_ @s @t @k wrk wid wdrl @@ -1393,7 +1473,7 @@ joinStakePool , IsOurs s ChimericAccount , IsOwned s k , GenChange s - , HardDerivation k + , SoftDerivation k , AddressIndexDerivationType k ~ 'Soft , WalletKey k , ctx ~ ApiLayer s t k @@ -1418,11 +1498,21 @@ joinStakePool ctx knownPools getPoolStatus apiPoolId (ApiT wid) body = do pools <- liftIO knownPools curEpoch <- getCurrentEpoch ctx - (tx, txMeta, txTime) <- withWorkerCtx ctx wid liftE liftE $ - \wrk -> liftHandler $ - W.joinStakePool - @_ @s @t @k wrk - curEpoch pools pid poolStatus wid (delegationAddress @n) pwd + (tx, txMeta, txTime) <- withWorkerCtx ctx wid liftE liftE $ \wrk -> do + action <- liftHandler + $ W.joinStakePool @_ @s @k @n wrk curEpoch pools pid poolStatus wid + + cs <- liftHandler + $ W.selectCoinsForDelegation @_ @s @t @k wrk wid action + + (tx, txMeta, txTime, sealedTx) <- liftHandler + $ W.signDelegation @_ @s @t @k wrk wid genChange pwd cs action + + liftHandler + $ W.submitTx @_ @s @t @k wrk + wid (tx, txMeta, sealedTx) + + pure (tx, txMeta, txTime) liftIO $ mkApiTransaction ti @@ -1434,10 +1524,13 @@ joinStakePool ctx knownPools getPoolStatus apiPoolId (ApiT wid) body = do Nothing #pendingSince where + genChange = delegationAddress @n + -- Not forecasting into the future. Should be safe. ti :: TimeInterpreter IO ti = timeInterpreter (ctx ^. networkLayer @t) + delegationFee :: forall ctx s t n k. ( s ~ SeqState n k @@ -1458,9 +1551,9 @@ quitStakePool , IsOwned s k , GenChange s , HasNetworkLayer t ctx - , HardDerivation k , AddressIndexDerivationType k ~ 'Soft , WalletKey k + , SoftDerivation k , ctx ~ ApiLayer s t k ) => ctx @@ -1470,8 +1563,22 @@ quitStakePool quitStakePool ctx (ApiT wid) body = do let pwd = coerce $ getApiT $ body ^. #passphrase - (tx, txMeta, txTime) <- withWorkerCtx ctx wid liftE liftE $ \wrk -> liftHandler $ - W.quitStakePool @_ @s @t @k wrk wid (delegationAddress @n) pwd + (tx, txMeta, txTime) <- withWorkerCtx ctx wid liftE liftE $ \wrk -> do + action <- liftHandler + $ W.quitStakePool @_ @s @k @n wrk wid + + cs <- liftHandler + $ W.selectCoinsForDelegation @_ @s @t @k wrk wid action + + (tx, txMeta, txTime, sealedTx) <- liftHandler + $ W.signDelegation @_ @s @t @k wrk wid genChange pwd cs action + + liftHandler + $ W.submitTx @_ @s @t @k wrk + wid (tx, txMeta, sealedTx) + + pure (tx, txMeta, txTime) + liftIO $ mkApiTransaction ti @@ -1483,6 +1590,8 @@ quitStakePool ctx (ApiT wid) body = do Nothing #pendingSince where + genChange = delegationAddress @n + -- Not forecasting into the future. Should be safe. ti :: TimeInterpreter IO ti = timeInterpreter (ctx ^. networkLayer @t) @@ -1778,13 +1887,35 @@ rndStateChange ctx (ApiT wid) pwd = -- | Makes an 'ApiCoinSelection' from the given 'UnsignedTx'. mkApiCoinSelection :: forall n. () - => UnsignedTx (TxIn, TxOut, NonEmpty DerivationIndex) + => Maybe (DelegationAction, NonEmpty DerivationIndex) + -> UnsignedTx (TxIn, TxOut, NonEmpty DerivationIndex) -> ApiCoinSelection n -mkApiCoinSelection (UnsignedTx inputs outputs) = +mkApiCoinSelection mcerts (UnsignedTx inputs outputs) = ApiCoinSelection (mkApiCoinSelectionInput <$> inputs) (mkAddressAmount <$> outputs) + (fmap (uncurry mkCertificates) mcerts) where + mkCertificates + :: DelegationAction + -> NonEmpty DerivationIndex + -> NonEmpty Api.ApiCertificate + mkCertificates action xs = + case action of + Join pid -> NE.fromList + [ Api.JoinPool apiStakePath (ApiT pid) + ] + + RegisterKeyAndJoin pid -> NE.fromList + [ Api.RegisterRewardAccount apiStakePath + , Api.JoinPool apiStakePath (ApiT pid) + ] + + Quit -> NE.fromList + [ Api.QuitPool apiStakePath + ] + where + apiStakePath = ApiT <$> xs mkAddressAmount :: TxOut -> AddressAmount (ApiT Address, Proxy n) mkAddressAmount (TxOut addr (Coin c)) = AddressAmount (ApiT addr, Proxy @n) (Quantity $ fromIntegral c) @@ -2135,20 +2266,18 @@ instance Buildable e => LiftHandler (ErrSelectCoinsExternal e) where handler = \case ErrSelectCoinsExternalNoSuchWallet e -> handler e - ErrSelectCoinsExternalUnableToMakeSelection e -> + ErrSelectCoinsExternalForPayment e -> handler e - ErrSelectCoinsExternalUnableToAssignInputs wid -> - apiError err500 UnexpectedError $ mconcat - [ "I was unable to assign inputs while generating a coin " - , "selection for the specified wallet: " - , toText wid - ] - ErrSelectCoinsExternalUnableToAssignOutputs wid -> - apiError err500 UnexpectedError $ mconcat - [ "I was unable to assign outputs while generating a coin " - , "selection for the specified wallet: " - , toText wid - ] + ErrSelectCoinsExternalForDelegation e -> + handler e + ErrSelectCoinsExternalUnableToAssignInputs e -> + apiError err500 UnableToAssignInputOutput $ mconcat + [ "I'm unable to assign inputs from coin selection: " + , pretty e] + ErrSelectCoinsExternalUnableToAssignOutputs e -> + apiError err500 UnableToAssignInputOutput $ mconcat + [ "I'm unable to assign outputs from coin selection: " + , pretty e] instance Buildable e => LiftHandler (ErrCoinSelection e) where handler = \case @@ -2413,6 +2542,8 @@ instance LiftHandler ErrSelectForDelegation where [ "I'm unable to select enough coins to pay for a " , "delegation certificate. I need: ", showT cost, " Lovelace." ] + ErrSelectForDelegationUnableToAssignInputs e -> handler e + ErrSelectForDelegationUnableToAssignOutputs e -> handler e instance LiftHandler ErrSignDelegation where handler = \case @@ -2447,6 +2578,14 @@ instance LiftHandler ErrJoinStakePool where [ "I couldn't find any stake pool with the given id: " , toText pid ] + ErrJoinStakePoolUnableToAssignInputs e -> + apiError err500 UnableToAssignInputOutput $ mconcat + [ "I'm unable to assign inputs from coin selection: " + , pretty e] + ErrJoinStakePoolUnableToAssignOutputs e -> + apiError err500 UnableToAssignInputOutput $ mconcat + [ "I'm unable to assign outputs from coin selection: " + , pretty e] instance LiftHandler ErrFetchRewards where handler = \case @@ -2483,6 +2622,14 @@ instance LiftHandler ErrQuitStakePool where , "account! Make sure to withdraw your ", pretty rewards , " lovelace first." ] + ErrQuitStakePoolUnableToAssignInputs e -> + apiError err500 UnableToAssignInputOutput $ mconcat + [ "I'm unable to assign inputs from coin selection: " + , pretty e] + ErrQuitStakePoolUnableToAssignOutputs e -> + apiError err500 UnableToAssignInputOutput $ mconcat + [ "I'm unable to assign outputs from coin selection: " + , pretty e] instance LiftHandler ErrCreateRandomAddress where handler = \case diff --git a/lib/core/src/Cardano/Wallet/Api/Types.hs b/lib/core/src/Cardano/Wallet/Api/Types.hs index 1f780f59e02..bb6f82aa78f 100644 --- a/lib/core/src/Cardano/Wallet/Api/Types.hs +++ b/lib/core/src/Cardano/Wallet/Api/Types.hs @@ -42,8 +42,11 @@ module Cardano.Wallet.Api.Types -- * API Types , ApiAddress (..) + , ApiCertificate (..) , ApiEpochInfo (..) , ApiSelectCoinsData (..) + , ApiSelectCoinsPayments (..) + , ApiSelectCoinsAction (..) , ApiCoinSelection (..) , ApiCoinSelectionInput (..) , ApiStakePool (..) @@ -199,6 +202,8 @@ import Cardano.Wallet.Primitive.Types , txMetadataIsNull , unsafeEpochNo ) +import Cardano.Wallet.Transaction + ( DelegationAction (..) ) import Control.Applicative ( optional, (<|>) ) import Control.Arrow @@ -209,7 +214,7 @@ import Data.Aeson ( FromJSON (..) , SumEncoding (..) , ToJSON (..) - , Value (Object) + , Value (Object, String) , camelTo2 , constructorTagModifier , fieldLabelModifier @@ -377,13 +382,36 @@ data ApiEpochInfo = ApiEpochInfo , epochStartTime :: !UTCTime } deriving (Eq, Generic, Show) -newtype ApiSelectCoinsData (n :: NetworkDiscriminant) = ApiSelectCoinsData +data ApiSelectCoinsData (n :: NetworkDiscriminant) + = ApiSelectForPayment (ApiSelectCoinsPayments n) + | ApiSelectForDelegation ApiSelectCoinsAction + deriving (Eq, Generic, Show) + +newtype ApiSelectCoinsPayments (n :: NetworkDiscriminant) = ApiSelectCoinsPayments { payments :: NonEmpty (AddressAmount (ApiT Address, Proxy n)) } deriving (Eq, Generic, Show) +newtype ApiSelectCoinsAction = ApiSelectCoinsAction + { delegationAction :: ApiT DelegationAction + } deriving (Eq, Generic, Show) + +data ApiCertificate + = RegisterRewardAccount + { rewardAccountPath :: NonEmpty (ApiT DerivationIndex) + } + | JoinPool + { rewardAccountPath :: NonEmpty (ApiT DerivationIndex) + , pool :: ApiT PoolId + } + | QuitPool + { rewardAccountPath :: NonEmpty (ApiT DerivationIndex) + } + deriving (Eq, Generic, Show) + data ApiCoinSelection (n :: NetworkDiscriminant) = ApiCoinSelection { inputs :: !(NonEmpty (ApiCoinSelectionInput n)) , outputs :: !(NonEmpty (AddressAmount (ApiT Address, Proxy n))) + , certificates :: Maybe (NonEmpty ApiCertificate) } deriving (Eq, Generic, Show) data ApiCoinSelectionInput (n :: NetworkDiscriminant) = ApiCoinSelectionInput @@ -693,6 +721,7 @@ data ApiPostRandomAddressData = ApiPostRandomAddressData , addressIndex :: !(Maybe (ApiT (Index 'AD.Hardened 'AddressK))) } deriving (Eq, Generic, Show) + data ApiWalletMigrationPostData (n :: NetworkDiscriminant) (s :: Symbol) = ApiWalletMigrationPostData { passphrase :: !(ApiT (Passphrase s)) @@ -759,6 +788,7 @@ data ApiErrorCode | AlreadyWithdrawing | WithdrawalNotWorth | PastHorizon + | UnableToAssignInputOutput deriving (Eq, Generic, Show) -- | Defines a point in time that can be formatted as and parsed from an @@ -991,16 +1021,70 @@ instance FromJSON ApiEpochInfo where instance ToJSON ApiEpochInfo where toJSON = genericToJSON defaultRecordTypeOptions -instance DecodeAddress n => FromJSON (ApiSelectCoinsData n) where +instance FromJSON ApiSelectCoinsAction where parseJSON = genericParseJSON defaultRecordTypeOptions -instance EncodeAddress n => ToJSON (ApiSelectCoinsData n) where +instance ToJSON ApiSelectCoinsAction where + toJSON = genericToJSON defaultRecordTypeOptions + +instance DecodeAddress n => FromJSON (ApiSelectCoinsPayments n) where + parseJSON = genericParseJSON defaultRecordTypeOptions +instance EncodeAddress n => ToJSON (ApiSelectCoinsPayments n) where toJSON = genericToJSON defaultRecordTypeOptions +instance DecodeAddress n => FromJSON (ApiSelectCoinsData n) where + parseJSON = withObject "DelegationAction" $ \o -> do + p <- o .:? "payments" + a <- o .:? "delegation_action" + case (p, a) of + (Just _, Just _) -> fail "Specified both payments and action, pick one" + (Nothing, Just v) -> + pure $ ApiSelectForDelegation $ ApiSelectCoinsAction v + (Just v, Nothing) -> + pure $ ApiSelectForPayment $ ApiSelectCoinsPayments v + _ -> fail "No valid parse for ApiSelectCoinsPayments or ApiSelectCoinsAction" +instance EncodeAddress n => ToJSON (ApiSelectCoinsData n) where + toJSON (ApiSelectForPayment v) = toJSON v + toJSON (ApiSelectForDelegation v) = toJSON v + instance DecodeAddress n => FromJSON (ApiCoinSelection n) where parseJSON = genericParseJSON defaultRecordTypeOptions instance EncodeAddress n => ToJSON (ApiCoinSelection n) where toJSON = genericToJSON defaultRecordTypeOptions +apiCertificateOptions :: Aeson.Options +apiCertificateOptions = Aeson.defaultOptions + { constructorTagModifier = camelTo2 '_' + , tagSingleConstructors = True + , fieldLabelModifier = camelTo2 '_' . dropWhile (== '_') + , omitNothingFields = True + , sumEncoding = TaggedObject + { + tagFieldName = "certificate_type" + , contentsFieldName = "details" -- this isn't actually used + } + } + +instance FromJSON ApiCertificate where + parseJSON = genericParseJSON apiCertificateOptions + +instance ToJSON ApiCertificate where + toJSON = genericToJSON apiCertificateOptions + +instance FromJSON (ApiT DelegationAction) where + parseJSON = withObject "DelegationAction" $ \o -> + o .: "action" >>= \case + "join" -> do + pid <- o .: "pool" + pure (ApiT $ Join (getApiT pid)) + "quit" -> pure $ ApiT Quit + val -> fail ("Unexpeced action value \"" <> T.unpack val <> "\". Valid values are: \"quit\" and \"join\".") + +instance ToJSON (ApiT DelegationAction) where + toJSON (ApiT (RegisterKeyAndJoin pid)) = object + [ "action" .= String "register_key_and_join", "pool" .= (ApiT pid) ] + toJSON (ApiT (Join pid)) = object [ "action" .= String "join", "pool" .= (ApiT pid) ] + toJSON (ApiT Quit) = object [ "action" .= String "quit" ] + instance DecodeAddress n => FromJSON (ApiCoinSelectionInput n) where parseJSON = genericParseJSON defaultRecordTypeOptions instance EncodeAddress n => ToJSON (ApiCoinSelectionInput n) where diff --git a/lib/core/src/Cardano/Wallet/Primitive/AddressDerivation.hs b/lib/core/src/Cardano/Wallet/Primitive/AddressDerivation.hs index 701a791c73f..8c03e92d50e 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/AddressDerivation.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/AddressDerivation.hs @@ -38,9 +38,12 @@ module Cardano.Wallet.Primitive.AddressDerivation , utxoExternal , utxoInternal , mutableAccount + , zeroAccount + , stakeDerivationPath , DerivationType (..) , HardDerivation (..) , SoftDerivation (..) + , DerivationPrefix (..) , liftIndex -- * Delegation @@ -85,7 +88,12 @@ import Cardano.Address.Derivation import Cardano.Mnemonic ( SomeMnemonic ) import Cardano.Wallet.Primitive.Types - ( Address (..), ChimericAccount (..), Hash (..), PassphraseScheme (..) ) + ( Address (..) + , ChimericAccount (..) + , DerivationIndex (..) + , Hash (..) + , PassphraseScheme (..) + ) import Control.DeepSeq ( NFData ) import Control.Monad @@ -138,6 +146,8 @@ import qualified Codec.CBOR.Write as CBOR import qualified Crypto.Scrypt as Scrypt import qualified Data.ByteArray as BA import qualified Data.ByteString as BS +import Data.List.NonEmpty + ( NonEmpty (..) ) import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -203,6 +213,21 @@ utxoInternal = toEnum $ fromEnum UTxOInternal mutableAccount :: Index 'Soft 'RoleK mutableAccount = toEnum $ fromEnum MutableAccount +zeroAccount :: Index 'Soft 'AddressK +zeroAccount = minBound + +-- | Full path to the stake key. There's only one. +stakeDerivationPath :: DerivationPrefix -> NonEmpty DerivationIndex +stakeDerivationPath (DerivationPrefix (purpose, coin, acc)) = + (fromIndex purpose) :| [ + fromIndex coin + , fromIndex acc + , fromIndex mutableAccount + , fromIndex zeroAccount] + where + fromIndex :: Index t l -> DerivationIndex + fromIndex = DerivationIndex . getIndex + -- | A derivation index, with phantom-types to disambiguate derivation type. -- -- @ @@ -277,6 +302,43 @@ instance LiftIndex 'Hardened where instance LiftIndex 'Soft where liftIndex (Index ix) = Index ix +-- | Each 'SeqState' is like a bucket of addresses associated with an 'account'. +-- An 'account' corresponds to a subset of an HD tree as defined in BIP-0039. +-- +-- cardano-wallet implements two similar HD schemes on top of BIP-0039 that are: +-- +-- - BIP-0044 (for so-called Icarus wallets) +-- - CIP-1815 (for so-called Shelley and Jormungandr wallets) +-- +-- Both scheme works by considering 5 levels of derivation from an initial root +-- key (see also 'Depth' from Cardano.Wallet.Primitive.AddressDerivation). A +-- SeqState keeps track of indexes from the two last levels of a derivation +-- branch. The 'DerivationPrefix' defines the first three indexes chosen for +-- this particular 'SeqState'. +newtype DerivationPrefix = DerivationPrefix + ( Index 'Hardened 'PurposeK + , Index 'Hardened 'CoinTypeK + , Index 'Hardened 'AccountK + ) deriving (Show, Generic, Eq, Ord) + +instance NFData DerivationPrefix + +instance ToText DerivationPrefix where + toText (DerivationPrefix (purpose, coinType, account)) + = T.intercalate "/" + $ map toText + [getIndex purpose, getIndex coinType, getIndex account] + +instance FromText DerivationPrefix where + fromText txt = + DerivationPrefix <$> case T.splitOn "/" txt of + [purposeT, coinTypeT, accountT] -> (,,) + <$> fromText purposeT + <*> fromText coinTypeT + <*> fromText accountT + _ -> + Left $ TextDecodingError "expected exactly 3 derivation paths" + -- | Type of derivation that should be used with the given indexes. -- -- In theory, we should only consider two derivation types: soft and hard. diff --git a/lib/core/src/Cardano/Wallet/Primitive/AddressDiscovery/Sequential.hs b/lib/core/src/Cardano/Wallet/Primitive/AddressDiscovery/Sequential.hs index c48ae17d8ef..89f73055bd7 100644 --- a/lib/core/src/Cardano/Wallet/Primitive/AddressDiscovery/Sequential.hs +++ b/lib/core/src/Cardano/Wallet/Primitive/AddressDiscovery/Sequential.hs @@ -74,6 +74,7 @@ import Cardano.Crypto.Wallet import Cardano.Wallet.Primitive.AddressDerivation ( AccountingStyle (..) , Depth (..) + , DerivationPrefix (..) , DerivationType (..) , HardDerivation (..) , Index (..) @@ -590,44 +591,6 @@ instance PersistPublicKey (k 'AccountK) => Buildable (SeqState n k) where where chgsF = blockListF' "-" build (pendingIxsToList chgs) --- | Each 'SeqState' is like a bucket of addresses associated with an 'account'. --- An 'account' corresponds to a subset of an HD tree as defined in BIP-0039. --- --- cardano-wallet implements two similar HD schemes on top of BIP-0039 that are: --- --- - BIP-0044 (for so-called Icarus wallets) --- - CIP-1815 (for so-called Shelley and Jormungandr wallets) --- --- Both scheme works by considering 5 levels of derivation from an initial root --- key (see also 'Depth' from Cardano.Wallet.Primitive.AddressDerivation). A --- SeqState keeps track of indexes from the two last levels of a derivation --- branch. The 'DerivationPrefix' defines the first three indexes chosen for --- this particular 'SeqState'. -newtype DerivationPrefix = DerivationPrefix - ( Index 'Hardened 'PurposeK - , Index 'Hardened 'CoinTypeK - , Index 'Hardened 'AccountK - ) deriving (Show, Generic, Eq, Ord) - -instance NFData DerivationPrefix - -instance ToText DerivationPrefix where - toText (DerivationPrefix (purpose, coinType, account)) - = T.intercalate "/" - $ map (T.pack . show) - [getIndex purpose, getIndex coinType, getIndex account] - -instance FromText DerivationPrefix where - fromText txt = - DerivationPrefix <$> case T.splitOn "/" txt of - [purposeT, coinTypeT, accountT] -> (,,) - <$> fromText purposeT - <*> fromText coinTypeT - <*> fromText accountT - _ -> - Left $ TextDecodingError "expected exactly 3 derivation paths" - - -- | Purpose is a constant set to 44' (or 0x8000002C) following the original -- BIP-44 specification. -- diff --git a/lib/core/src/Cardano/Wallet/Transaction.hs b/lib/core/src/Cardano/Wallet/Transaction.hs index 943719e7fd7..c0a695872f1 100644 --- a/lib/core/src/Cardano/Wallet/Transaction.hs +++ b/lib/core/src/Cardano/Wallet/Transaction.hs @@ -1,5 +1,6 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -45,6 +46,8 @@ import Data.Text ( Text ) import Data.Word ( Word16, Word8 ) +import GHC.Generics + ( Generic ) data TransactionLayer t k = TransactionLayer { mkStdTx @@ -158,7 +161,7 @@ data TransactionLayer t k = TransactionLayer -- | Whether the user is attempting any particular delegation action. data DelegationAction = RegisterKeyAndJoin PoolId | Join PoolId | Quit - deriving (Show) + deriving (Show, Eq, Generic) -- | A type family for validations that are specific to a particular backend -- type. This demands an instantiation of the family for a particular backend: diff --git a/lib/core/test/unit/Cardano/Wallet/Api/Malformed.hs b/lib/core/test/unit/Cardano/Wallet/Api/Malformed.hs index adf23d296b7..7e179700035 100644 --- a/lib/core/test/unit/Cardano/Wallet/Api/Malformed.hs +++ b/lib/core/test/unit/Cardano/Wallet/Api/Malformed.hs @@ -847,12 +847,31 @@ instance Malformed (BodyParam (ApiSelectCoinsData ('Testnet pm))) where malformed = jsonValid ++ jsonInvalid where jsonInvalid = first BodyParam <$> - [ ("1020344", "Error in $: parsing Cardano.Wallet.Api.Types.ApiSelectCoinsData(ApiSelectCoinsData) failed, expected Object, but encountered Number") - , ("\"1020344\"", "Error in $: parsing Cardano.Wallet.Api.Types.ApiSelectCoinsData(ApiSelectCoinsData) failed, expected Object, but encountered String") + [ ("1020344", "Error in $: parsing DelegationAction failed, expected Object, but encountered Number") + , ("\"1020344\"", "Error in $: parsing DelegationAction failed, expected Object, but encountered String") , ("\"slot_number : \"random\"}", "trailing junk after valid JSON: endOfInput") , ("{\"payments : [], \"random\"}", msgJsonInvalid) + , ("join", "I couldn't understand the content of your message. If your message is intended to be in JSON format, please check that the JSON is valid.") + , ("quit", msgJsonInvalid) + ] + jsonValid = (first (BodyParam . Aeson.encode) <$> paymentCases) <> jsonValidAction + jsonValidAction = first (BodyParam . Aeson.encode) <$> + [ ( [aesonQQ| { "action": "join" }|] + , "Error in $: No valid parse for ApiSelectCoinsPayments or ApiSelectCoinsAction" + ) + , ( [aesonQQ| { "action": "" }|] + , "Error in $: No valid parse for ApiSelectCoinsPayments or ApiSelectCoinsAction" + ) + , ( [aesonQQ| { "action": "join", "pool": "" }|] + , "Error in $: No valid parse for ApiSelectCoinsPayments or ApiSelectCoinsAction" + ) + , ( [aesonQQ| { "action": "join", "pool": "1" }|] + , "Error in $: No valid parse for ApiSelectCoinsPayments or ApiSelectCoinsAction" + ) + , ( [aesonQQ| { "pool": "pool1wqaz0q0zhtxlgn0ewssevn2mrtm30fgh2g7hr7z9rj5856457mm" }|] + , "Error in $: No valid parse for ApiSelectCoinsPayments or ApiSelectCoinsAction" + ) ] - jsonValid = first (BodyParam . Aeson.encode) <$> paymentCases instance Malformed (BodyParam (PostTransactionData ('Testnet pm))) where malformed = jsonValid ++ jsonInvalid diff --git a/lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs b/lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs index 12a9e39c603..d363845a480 100644 --- a/lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs +++ b/lib/core/test/unit/Cardano/Wallet/Api/TypesSpec.hs @@ -48,6 +48,7 @@ import Cardano.Wallet.Api.Types , ApiBlockReference (..) , ApiByronWallet (..) , ApiByronWalletBalance (..) + , ApiCertificate (..) , ApiCoinSelection (..) , ApiCoinSelectionInput (..) , ApiEpochInfo (..) @@ -59,7 +60,9 @@ import Cardano.Wallet.Api.Types , ApiNtpStatus (..) , ApiPostRandomAddressData , ApiPutAddressesData (..) + , ApiSelectCoinsAction (..) , ApiSelectCoinsData (..) + , ApiSelectCoinsPayments (..) , ApiSlotId (..) , ApiSlotReference (..) , ApiStakePool (..) @@ -167,6 +170,8 @@ import Cardano.Wallet.Primitive.Types , walletNameMaxLength , walletNameMinLength ) +import Cardano.Wallet.Transaction + ( DelegationAction (..) ) import Cardano.Wallet.Unsafe ( unsafeFromText, unsafeXPrv ) import Control.Lens @@ -270,6 +275,7 @@ import Test.QuickCheck , scale , shrinkIntegral , vector + , vectorOf , (.&&.) , (===) ) @@ -293,6 +299,7 @@ import qualified Data.ByteArray as BA import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as B8 import qualified Data.HashMap.Strict as HM +import qualified Data.List.NonEmpty as NE import qualified Data.Map.Strict as Map import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -602,8 +609,8 @@ spec = do x' === x .&&. show x' === show x it "ApiSelectCoinsData" $ property $ \x -> let - x' = ApiSelectCoinsData - { payments = payments (x :: ApiSelectCoinsData ('Testnet 0)) + x' = ApiSelectCoinsPayments + { payments = payments (x :: ApiSelectCoinsPayments ('Testnet 0)) } in x' === x .&&. show x' === show x @@ -612,6 +619,7 @@ spec = do x' = ApiCoinSelection { inputs = inputs (x :: ApiCoinSelection ('Testnet 0)) , outputs = outputs (x :: ApiCoinSelection ('Testnet 0)) + , certificates = certificates (x :: ApiCoinSelection ('Testnet 0)) } in x' === x .&&. show x' === show x @@ -911,12 +919,35 @@ instance Arbitrary ApiEpochInfo where arbitrary = ApiEpochInfo <$> arbitrary <*> genUniformTime shrink _ = [] +instance Arbitrary (ApiSelectCoinsPayments n) where + arbitrary = genericArbitrary + shrink = genericShrink + +instance Arbitrary ApiSelectCoinsAction where + arbitrary = genericArbitrary + shrink = genericShrink + instance Arbitrary (ApiSelectCoinsData n) where arbitrary = genericArbitrary shrink = genericShrink +instance Arbitrary DelegationAction where + arbitrary = oneof [Join <$> arbitrary, pure Quit] + shrink _ = [] + +instance Arbitrary ApiCertificate where + arbitrary = + oneof [ JoinPool <$> arbitraryRewardAccountPath <*> arbitrary + , QuitPool <$> arbitraryRewardAccountPath + , RegisterRewardAccount <$> arbitraryRewardAccountPath + ] + where + arbitraryRewardAccountPath :: Gen (NonEmpty (ApiT DerivationIndex)) + arbitraryRewardAccountPath = NE.fromList <$> vectorOf 5 arbitrary + shrink = genericShrink + instance Arbitrary (ApiCoinSelection n) where - arbitrary = applyArbitrary2 ApiCoinSelection + arbitrary = ApiCoinSelection <$> arbitrary <*> arbitrary <*> arbitrary shrink = genericShrink instance Arbitrary (ApiCoinSelectionInput n) where @@ -1559,7 +1590,22 @@ instance ToSchema (ApiPutAddressesData t) where declareNamedSchema _ = declareSchemaForDefinition "ApiPutAddressesData" instance ToSchema (ApiSelectCoinsData n) where - declareNamedSchema _ = declareSchemaForDefinition "ApiSelectCoinsData" + declareNamedSchema _ = do + NamedSchema _ paymentData <- declareNamedSchema (Proxy @(ApiSelectCoinsPayments n)) + NamedSchema _ actionData <- declareNamedSchema (Proxy @ApiSelectCoinsAction) + pure $ NamedSchema Nothing $ mempty + & type_ .~ Just SwaggerObject + & required .~ [] + & properties .~ mconcat + [ paymentData ^. properties + , actionData ^. properties + ] + +instance ToSchema (ApiSelectCoinsPayments n) where + declareNamedSchema _ = declareSchemaForDefinition "ApiSelectCoinsPayments" + +instance ToSchema ApiSelectCoinsAction where + declareNamedSchema _ = declareSchemaForDefinition "ApiSelectCoinsAction" instance ToSchema (ApiCoinSelection n) where declareNamedSchema _ = declareSchemaForDefinition "ApiCoinSelection" diff --git a/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Api/Server.hs b/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Api/Server.hs index aaab8710589..5f1a9ae3b7b 100644 --- a/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Api/Server.hs +++ b/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Api/Server.hs @@ -8,6 +8,7 @@ {-# LANGUAGE TypeOperators #-} {-# OPTIONS_GHC -fno-warn-orphans #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} -- | -- Copyright: © 2018-2020 IOHK @@ -37,6 +38,7 @@ import Cardano.Wallet.Api , Api , ApiLayer (..) , ByronAddresses + , ByronCoinSelections , ByronMigrations , ByronTransactions , ByronWallets @@ -89,7 +91,11 @@ import Cardano.Wallet.Api.Server , withLegacyLayer' ) import Cardano.Wallet.Api.Types - ( ApiErrorCode (..), ApiT (..), SomeByronWalletPostData (..) ) + ( ApiErrorCode (..) + , ApiSelectCoinsData (..) + , ApiT (..) + , SomeByronWalletPostData (..) + ) import Cardano.Wallet.Primitive.AddressDerivation ( DelegationAddress (..), NetworkDiscriminant (..), PaymentAddress ) import Cardano.Wallet.Primitive.AddressDerivation.Byron @@ -169,8 +175,16 @@ server byron icarus jormungandr spl ntp = addresses = listAddresses jormungandr (normalizeDelegationAddress @_ @JormungandrKey @n) :<|> (\_ -> throwError err501) + -- Hlint doesn't seem to care about inlining properties: + -- https://github.com/quchen/articles/blob/master/fbut.md#f-x---is-not-f--x--- + {-# HLINT ignore "Redundant lambda" #-} coinSelections :: Server (CoinSelections n) - coinSelections = selectCoins jormungandr (delegationAddress @n) + coinSelections = + \wid ascd -> case ascd of + (ApiSelectForPayment ascp) -> + selectCoins jormungandr (delegationAddress @n) wid ascp + (ApiSelectForDelegation _) -> + throwError err501 transactions :: Server (Transactions n) transactions = @@ -242,7 +256,7 @@ server byron icarus jormungandr spl ntp = :<|> (\_ _ -> throwError err501) :<|> (\_ _ -> throwError err501) - byronCoinSelections :: Server (CoinSelections n) + byronCoinSelections :: Server (ByronCoinSelections n) byronCoinSelections _ _ = throwError err501 byronTransactions :: Server (ByronTransactions n) diff --git a/lib/shelley/src/Cardano/Wallet/Shelley/Api/Server.hs b/lib/shelley/src/Cardano/Wallet/Shelley/Api/Server.hs index a2341f1dc0b..5646d63c665 100644 --- a/lib/shelley/src/Cardano/Wallet/Shelley/Api/Server.hs +++ b/lib/shelley/src/Cardano/Wallet/Shelley/Api/Server.hs @@ -6,6 +6,8 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} + -- | -- Copyright: © 2018-2020 IOHK -- License: Apache-2.0 @@ -32,6 +34,7 @@ import Cardano.Wallet.Api , Api , ApiLayer (..) , ByronAddresses + , ByronCoinSelections , ByronMigrations , ByronTransactions , ByronWallets @@ -85,6 +88,8 @@ import Cardano.Wallet.Api.Server , quitStakePool , rndStateChange , selectCoins + , selectCoinsForJoin + , selectCoinsForQuit , withLegacyLayer , withLegacyLayer' ) @@ -92,6 +97,8 @@ import Cardano.Wallet.Api.Types ( ApiAddressInspect (..) , ApiAddressInspectData (..) , ApiErrorCode (..) + , ApiSelectCoinsAction (..) + , ApiSelectCoinsData (..) , ApiStakePool , ApiT (..) , SettingsPutData (..) @@ -113,6 +120,8 @@ import Cardano.Wallet.Shelley.Compatibility ( inspectAddress ) import Cardano.Wallet.Shelley.Pools ( StakePoolLayer (..) ) +import Cardano.Wallet.Transaction + ( DelegationAction (..) ) import Control.Applicative ( liftA2 ) import Control.Monad.IO.Class @@ -134,7 +143,7 @@ import Fmt import Network.Ntp ( NtpClient ) import Servant - ( (:<|>) (..), Handler (..), NoContent (..), Server, err400 ) + ( (:<|>) (..), Handler (..), NoContent (..), Server, err400, throwError ) import Servant.Server ( ServerError (..) ) import Type.Reflection @@ -192,8 +201,22 @@ server byron icarus shelley spl ntp = handler transform = Handler . withExceptT toServerError . except . fmap transform + -- Hlint doesn't seem to care about inlining properties: + -- https://github.com/quchen/articles/blob/master/fbut.md#f-x---is-not-f--x--- + {-# HLINT ignore "Redundant lambda" #-} coinSelections :: Server (CoinSelections n) - coinSelections = selectCoins shelley (delegationAddress @n) + coinSelections = (\wid ascd -> case ascd of + (ApiSelectForPayment ascp) -> selectCoins shelley (delegationAddress @n) wid ascp + (ApiSelectForDelegation (ApiSelectCoinsAction (ApiT action))) -> case action of + Join pid -> selectCoinsForJoin @_ @() + shelley + (knownPools spl) + (getPoolLifeCycleStatus spl) + pid + (getApiT wid) + RegisterKeyAndJoin _ -> throwError err400 + Quit -> selectCoinsForQuit @_ @() shelley wid + ) transactions :: Server (Transactions n) transactions = @@ -285,10 +308,15 @@ server byron icarus shelley spl ntp = (icarus, listAddresses icarus (const pure) wid s) ) - byronCoinSelections :: Server (CoinSelections n) - byronCoinSelections wid x = withLegacyLayer wid - (byron, liftHandler $ throwE ErrNotASequentialWallet) - (icarus, selectCoins icarus (const $ paymentAddress @n) wid x) + byronCoinSelections :: Server (ByronCoinSelections n) + byronCoinSelections wid (ApiSelectForPayment x) = + withLegacyLayer wid + (byron, liftHandler $ throwE ErrNotASequentialWallet) + (icarus, selectCoins icarus (const $ paymentAddress @n) wid x) + byronCoinSelections _ _ = Handler + $ throwE + $ apiError err400 InvalidWalletType + "Byron wallets don't have delegation capabilities." byronTransactions :: Server (ByronTransactions n) byronTransactions = diff --git a/lib/shelley/src/Cardano/Wallet/Shelley/Transaction.hs b/lib/shelley/src/Cardano/Wallet/Shelley/Transaction.hs index a6856bf2859..4f9bf5231fb 100644 --- a/lib/shelley/src/Cardano/Wallet/Shelley/Transaction.hs +++ b/lib/shelley/src/Cardano/Wallet/Shelley/Transaction.hs @@ -45,6 +45,8 @@ import Cardano.Binary ( serialize' ) import Cardano.Crypto.DSIGN ( DSIGNAlgorithm (..), SignedDSIGN (..) ) +import Cardano.Crypto.Wallet + ( XPub ) import Cardano.Ledger.Crypto ( Crypto (..) ) import Cardano.Wallet.Primitive.AddressDerivation @@ -172,6 +174,7 @@ instance TxWitnessTagFor IcarusKey where instance TxWitnessTagFor ByronKey where txWitnessTagFor = TxWitnessByronUTxO Byron + mkTx :: forall k. (TxWitnessTagFor k, WalletKey k) => Cardano.NetworkId @@ -265,19 +268,17 @@ newTransactionLayer networkId = TransactionLayer _mkDelegationJoinTx poolId acc@(accXPrv, pwd') keyFrom tip cs = do let accXPub = toXPub accXPrv let certs = - if deposit cs > 0 then - [ toStakeKeyRegCert accXPub - , toStakePoolDlgCert accXPub poolId - ] - else - [ toStakePoolDlgCert accXPub poolId ] + if deposit cs > 0 + then mkDelegationCertificates (RegisterKeyAndJoin poolId) accXPub + else mkDelegationCertificates (Join poolId) accXPub let mkWits unsigned = [ mkShelleyWitness unsigned (accXPrv, pwd') ] let payload = TxPayload Nothing certs mkWits - mkTx networkId payload tip acc keyFrom cs + let ttl = defaultTTL tip + mkTx networkId payload ttl acc keyFrom cs _mkDelegationQuitTx :: (XPrv, Passphrase "encryption") @@ -298,7 +299,24 @@ newTransactionLayer networkId = TransactionLayer ] let payload = TxPayload Nothing certs mkWits - mkTx networkId payload tip acc keyFrom cs + let ttl = defaultTTL tip + mkTx networkId payload ttl acc keyFrom cs + +mkDelegationCertificates + :: DelegationAction + -- Pool Id to which we're planning to delegate + -> XPub + -- Reward account public key + -> [Cardano.Certificate] +mkDelegationCertificates da accXPub = + case da of + Join poolId -> + [ toStakePoolDlgCert accXPub poolId ] + RegisterKeyAndJoin poolId -> + [ toStakeKeyRegCert accXPub + , toStakePoolDlgCert accXPub poolId + ] + Quit -> [toStakeKeyDeregCert accXPub] _estimateMaxNumberOfInputs :: forall k. TxWitnessTagFor k diff --git a/lib/text-class/src/Data/Text/Class.hs b/lib/text-class/src/Data/Text/Class.hs index cda90df20fb..c7b86a743b2 100644 --- a/lib/text-class/src/Data/Text/Class.hs +++ b/lib/text-class/src/Data/Text/Class.hs @@ -48,6 +48,10 @@ import Data.Text ( Text ) import Data.Text.Read ( decimal, signed ) +import Data.Word + ( Word32, Word64 ) +import Data.Word.Odd + ( Word31 ) import Fmt ( Buildable ) import GHC.Generics @@ -59,6 +63,11 @@ import Text.Read import qualified Data.Char as C import qualified Data.Text as T +import qualified Data.Text.Lazy as T + ( toStrict ) +import qualified Data.Text.Lazy.Builder as B +import qualified Data.Text.Lazy.Builder.Int as B +import qualified Data.Text.Lazy.Builder.RealFloat as B import qualified Text.Casing as Casing -- | Defines a textual encoding for a type. @@ -103,7 +112,7 @@ instance FromText Int where <> "." instance ToText Int where - toText = T.pack . show + toText = intToText instance FromText Natural where fromText t = do @@ -114,7 +123,7 @@ instance FromText Natural where err = TextDecodingError "Expecting natural number" instance ToText Natural where - toText = T.pack . show + toText = intToText instance FromText Integer where fromText t = do @@ -125,7 +134,7 @@ instance FromText Integer where err = TextDecodingError "Expecting integer" instance ToText Integer where - toText = T.pack . show + toText = intToText instance FromText Double where fromText = first (const err) . readEither . T.unpack @@ -133,7 +142,22 @@ instance FromText Double where err = TextDecodingError "Expecting floating number" instance ToText Double where - toText = T.pack . show + toText = realFloatToText + +instance ToText Word64 where + toText = intToText + +instance ToText Word32 where + toText = intToText + +instance ToText Word31 where + toText = intToText + +realFloatToText :: RealFloat a => a -> T.Text +realFloatToText = T.toStrict . B.toLazyText . B.realFloat + +intToText :: Integral a => a -> T.Text +intToText = T.toStrict . B.toLazyText . B.decimal -- | Decode the specified text with a 'Maybe' result type. fromTextMaybe :: FromText a => Text -> Maybe a diff --git a/lib/text-class/text-class.cabal b/lib/text-class/text-class.cabal index 5c45ea435e0..7be9cf87213 100644 --- a/lib/text-class/text-class.cabal +++ b/lib/text-class/text-class.cabal @@ -34,6 +34,7 @@ library , fmt , text , hspec + , OddWord , QuickCheck hs-source-dirs: src diff --git a/nix/.stack.nix/cardano-wallet-cli.nix b/nix/.stack.nix/cardano-wallet-cli.nix index 38bbd412983..2059354cf09 100644 --- a/nix/.stack.nix/cardano-wallet-cli.nix +++ b/nix/.stack.nix/cardano-wallet-cli.nix @@ -70,4 +70,4 @@ }; }; }; - } // rec { src = (pkgs.lib).mkDefault ../.././lib/cli; } + } // rec { src = (pkgs.lib).mkDefault ../.././lib/cli; } \ No newline at end of file diff --git a/nix/.stack.nix/cardano-wallet.nix b/nix/.stack.nix/cardano-wallet.nix index 2d4a6940926..5c4057edbe2 100644 --- a/nix/.stack.nix/cardano-wallet.nix +++ b/nix/.stack.nix/cardano-wallet.nix @@ -227,4 +227,4 @@ }; }; }; - } // rec { src = (pkgs.lib).mkDefault ../.././lib/shelley; } + } // rec { src = (pkgs.lib).mkDefault ../.././lib/shelley; } \ No newline at end of file diff --git a/nix/.stack.nix/text-class.nix b/nix/.stack.nix/text-class.nix index c5808c85f9c..265e7da47cd 100644 --- a/nix/.stack.nix/text-class.nix +++ b/nix/.stack.nix/text-class.nix @@ -32,6 +32,7 @@ (hsPkgs."fmt" or (errorHandler.buildDepError "fmt")) (hsPkgs."text" or (errorHandler.buildDepError "text")) (hsPkgs."hspec" or (errorHandler.buildDepError "hspec")) + (hsPkgs."OddWord" or (errorHandler.buildDepError "OddWord")) (hsPkgs."QuickCheck" or (errorHandler.buildDepError "QuickCheck")) ]; buildable = true; diff --git a/specifications/api/swagger.yaml b/specifications/api/swagger.yaml index 69784dddefc..54af9fc9d28 100644 --- a/specifications/api/swagger.yaml +++ b/specifications/api/swagger.yaml @@ -530,6 +530,45 @@ x-transactionOutputs: &transactionOutputs address: *addressId amount: *transactionAmount +x-delegationAction: &delegationAction + description: | + A delegation action. + + Pool id is only required for "join". + type: object + required: + - action + properties: + action: + type: string + enum: ["quit", "join"] + pool: *stakePoolId + +x-rewardAccountPath: &rewardAccountPath + type: array + minItems: 5 + maxItems: 5 + items: + type: string + +x-certificate: &certificate + description: | + A delegation certificate + + Only for 'join_pool' the 'pool' property is required. + type: object + required: + - certificate_type + - reward_account_path + properties: + certificate_type: + type: string + enum: ["join_pool", "quit_pool", "register_reward_account"] + pool: + <<: *stakePoolId + reward_account_path: + <<: *rewardAccountPath + x-transactionRedemptionRequest: &transactionRedemptionRequest <<: *walletMnemonicSentence description: | @@ -1085,13 +1124,30 @@ components: minimum_utxo_value: *amount hardfork_at: *epochInfo - ApiSelectCoinsData: &ApiSelectCoinsData + + ApiSelectCoinsPayments: &ApiSelectCoinsPayments type: object required: - payments properties: payments: *transactionOutputs + ApiSelectCoinsAction: &ApiSelectCoinsAction + type: object + required: + - delegation_action + properties: + delegation_action: *delegationAction + + ApiSelectCoinsData: &ApiSelectCoinsData + type: object + oneOf: + - <<: *ApiSelectCoinsPayments + - <<: *ApiSelectCoinsAction + + ApiByronSelectCoinsData: &ApiByronSelectCoinsData + <<: *ApiSelectCoinsPayments + ApiCoinSelection: &ApiCoinSelection type: object required: @@ -1100,6 +1156,9 @@ components: properties: inputs: *transactionResolvedInputs outputs: *transactionOutputs + certificates: + type: array + items: *certificate ApiStakePool: &ApiStakePool type: object @@ -2892,7 +2951,7 @@ paths: required: true content: application/json: - schema: *ApiSelectCoinsData + schema: *ApiByronSelectCoinsData responses: *responsesSelectCoins /byron-wallets/{walletId}/migrations: @@ -3021,3 +3080,4 @@ paths: Return the current settings. responses: *responsesGetSettings +