diff --git a/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST/Start.hs b/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST/Start.hs index 9c5d2db7432..b38b99191cd 100644 --- a/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST/Start.hs +++ b/lib/customer-deposit-wallet/rest/Cardano/Wallet/Deposit/REST/Start.hs @@ -11,7 +11,7 @@ import Prelude import Cardano.Wallet.Deposit.IO ( WalletBootEnv (..) - , onWalletState + , onWalletState, readWalletState ) import Cardano.Wallet.Deposit.IO.Network.Mock ( newNetworkEnvMock @@ -22,6 +22,7 @@ import Cardano.Wallet.Deposit.IO.Network.Type import Cardano.Wallet.Deposit.Pure ( customerAddress , rollForwardOne + , utxoHistory ) import Cardano.Wallet.Deposit.Read ( ChainPoint (..) @@ -34,7 +35,7 @@ import Cardano.Wallet.Deposit.REST , loadWallet , onWalletInstance , runWalletResourceM - , walletExists + , walletExists, availableBalance, getTxHistoryByCustomer, listCustomers ) import Cardano.Wallet.Deposit.Write ( addTxOut @@ -48,6 +49,7 @@ import Control.Concurrent ) import Control.Monad ( when + , (>=>) ) import Control.Monad.IO.Class ( MonadIO (..) @@ -73,6 +75,7 @@ import Data.Maybe import qualified Cardano.Wallet.Deposit.Read as Read import qualified Data.Delta.Update as Delta +import Cardano.Wallet.Deposit.Pure.UTxO.UTxOHistory (getUTxO) lg :: (MonadIO m, Show a) => Tracer IO String -> String -> a -> m () lg tr p x = liftIO $ traceWith tr $ p <> ": " <> show x @@ -93,6 +96,12 @@ loadDepositWalletFromDisk tr dir env resource = do lg tr "Wallet loaded from" dir liftIO $ threadDelay 1_000_000 ExceptT $ mockFundTheWallet resource + ExceptT $ flip runWalletResourceM resource $ do + availableBalance >>= liftIO . print + getTxHistoryByCustomer >>= liftIO . print + listCustomers >>= liftIO . print + ExceptT $ flip runWalletResourceM resource $ do + onWalletInstance (readWalletState >=> (liftIO . print . getUTxO . utxoHistory)) case result of Left e -> error $ show e Right _ -> pure () diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO.hs index 2bc02133217..4e270751249 100644 --- a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO.hs +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/IO.hs @@ -47,6 +47,7 @@ module Cardano.Wallet.Deposit.IO -- * Internals , onWalletState + , readWalletState ) where import Prelude diff --git a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure.hs b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure.hs index be9405041b4..922b1810a9c 100644 --- a/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure.hs +++ b/lib/customer-deposit-wallet/src/Cardano/Wallet/Deposit/Pure.hs @@ -53,6 +53,7 @@ module Cardano.Wallet.Deposit.Pure , availableUTxO , getCustomerDeposits , getAllDeposits + , utxoHistory ) where import Prelude hiding diff --git a/lib/ui/cardano-wallet-ui.cabal b/lib/ui/cardano-wallet-ui.cabal index 802921c2199..e513a03fb3f 100644 --- a/lib/ui/cardano-wallet-ui.cabal +++ b/lib/ui/cardano-wallet-ui.cabal @@ -63,6 +63,7 @@ library Cardano.Wallet.UI.Deposit.API.Addresses.Transactions Cardano.Wallet.UI.Deposit.API.Common Cardano.Wallet.UI.Deposit.API.Deposits.Deposits + Cardano.Wallet.UI.Deposit.API.Payments Cardano.Wallet.UI.Deposit.Handlers.Addresses Cardano.Wallet.UI.Deposit.Handlers.Addresses.Transactions Cardano.Wallet.UI.Deposit.Handlers.Deposits.Customers @@ -70,6 +71,7 @@ library Cardano.Wallet.UI.Deposit.Handlers.Deposits.Times Cardano.Wallet.UI.Deposit.Handlers.Deposits.TxIds Cardano.Wallet.UI.Deposit.Handlers.Lib + Cardano.Wallet.UI.Deposit.Handlers.Payments.Transaction Cardano.Wallet.UI.Deposit.Handlers.Wallet Cardano.Wallet.UI.Deposit.Html.Common Cardano.Wallet.UI.Deposit.Html.Pages.About @@ -80,6 +82,7 @@ library Cardano.Wallet.UI.Deposit.Html.Pages.Deposits.Times Cardano.Wallet.UI.Deposit.Html.Pages.Deposits.TxIds Cardano.Wallet.UI.Deposit.Html.Pages.Page + Cardano.Wallet.UI.Deposit.Html.Pages.Payments.Page Cardano.Wallet.UI.Deposit.Html.Pages.Wallet Cardano.Wallet.UI.Deposit.Server Cardano.Wallet.UI.Deposit.Server.Addresses @@ -88,12 +91,14 @@ library Cardano.Wallet.UI.Deposit.Server.Deposits.Times Cardano.Wallet.UI.Deposit.Server.Deposits.TxIds Cardano.Wallet.UI.Deposit.Server.Lib + Cardano.Wallet.UI.Deposit.Server.Payments.Page Cardano.Wallet.UI.Deposit.Server.Wallet + Cardano.Wallet.UI.Deposit.Types.Payments Cardano.Wallet.UI.Lib.Address Cardano.Wallet.UI.Lib.Discretization Cardano.Wallet.UI.Lib.ListOf - Cardano.Wallet.UI.Lib.Pagination.TimedSeq Cardano.Wallet.UI.Lib.Pagination.Map + Cardano.Wallet.UI.Lib.Pagination.TimedSeq Cardano.Wallet.UI.Lib.Pagination.Type Cardano.Wallet.UI.Lib.Time.Direction Cardano.Wallet.UI.Shelley.API @@ -116,6 +121,7 @@ library , aeson , aeson-pretty , base + , base58-bytestring , bech32 , bech32-th , bytestring diff --git a/lib/ui/src/Cardano/Wallet/UI/Common/Html/Pages/Lib.hs b/lib/ui/src/Cardano/Wallet/UI/Common/Html/Pages/Lib.hs index ae8c3136ca5..0bb964411e3 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Common/Html/Pages/Lib.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Common/Html/Pages/Lib.hs @@ -274,7 +274,7 @@ showThousandDots = reverse . showThousandDots' . reverse . show a <> if null b then [] else "." <> showThousandDots' b addressH :: Monad m => WithCopy -> Address -> HtmlT m () -addressH copy addr = +addressH copy addr = do truncatableText copy ("address-text-" <> encodedAddr) $ toHtml encodedAddr where diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/API.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/API.hs index 6d869734886..233f451ea43 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/API.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/API.hs @@ -42,6 +42,10 @@ import Cardano.Wallet.UI.Deposit.API.Common import Cardano.Wallet.UI.Deposit.API.Deposits.Deposits ( DepositsParams ) +import Cardano.Wallet.UI.Deposit.API.Payments + ( NewReceiver + , WithReceivers + ) import Control.Lens ( makePrisms ) @@ -80,6 +84,7 @@ data Page | Wallet | Addresses | Deposits + | Payments makePrisms ''Page @@ -90,6 +95,7 @@ instance ToHttpApiData Page where toUrlPiece Wallet = "wallet" toUrlPiece Addresses = "addresses" toUrlPiece Deposits = "deposits" + toUrlPiece Payments = "payments" instance FromHttpApiData Page where parseUrlPiece "about" = Right About @@ -98,6 +104,7 @@ instance FromHttpApiData Page where parseUrlPiece "wallet" = Right Wallet parseUrlPiece "addresses" = Right Addresses parseUrlPiece "deposits" = Right Deposits + parseUrlPiece "payments" = Right Payments parseUrlPiece _ = Left "Invalid page" -- | Pages endpoints @@ -108,6 +115,7 @@ type Pages = :<|> "wallet" :> SessionedHtml Get :<|> "addresses" :> SessionedHtml Get :<|> "deposits" :> SessionedHtml Get + :<|> "payments" :> SessionedHtml Get -- | Data endpoints type Data = @@ -190,6 +198,17 @@ type Data = :> QueryParam "customer" Customer :> QueryParam "tx-id" TxId :> SessionedHtml Post + :<|> "payments" :> SessionedHtml Get + :<|> "payments" + :> "receiver" + :> ReqBody '[FormUrlEncoded] NewReceiver + :> SessionedHtml Post + :<|> "payments" + :> "receiver" + :> "delete" + :> ReqBody '[FormUrlEncoded] (WithReceivers ()) + :> QueryParam "receiver-number" Int + :> SessionedHtml Post type Home = SessionedHtml Get @@ -208,6 +227,7 @@ settingsPageLink :: Link walletPageLink :: Link addressesPageLink :: Link depositPageLink :: Link +paymentsPageLink :: Link networkInfoLink :: Link settingsGetLink :: Link settingsSseToggleLink :: Link @@ -236,6 +256,9 @@ depositsTxIdsLink :: Maybe (WithOrigin UTCTime) -> Maybe Customer -> Maybe Expand -> Link depositsTxIdsPaginatingLink :: Maybe (WithOrigin UTCTime) -> Maybe Customer -> Maybe TxId -> Link +paymentsLink :: Link +paymentsNewReceiverLink :: Link +paymentsDeleteReceiverLink :: Maybe Int -> Link homePageLink :<|> aboutPageLink :<|> networkPageLink @@ -243,6 +266,7 @@ homePageLink :<|> walletPageLink :<|> addressesPageLink :<|> depositPageLink + :<|> paymentsPageLink :<|> networkInfoLink :<|> settingsGetLink :<|> settingsSseToggleLink @@ -265,5 +289,8 @@ homePageLink :<|> depositsCustomersLink :<|> depositsCustomersPaginatingLink :<|> depositsTxIdsLink - :<|> depositsTxIdsPaginatingLink = + :<|> depositsTxIdsPaginatingLink + :<|> paymentsLink + :<|> paymentsNewReceiverLink + :<|> paymentsDeleteReceiverLink = allLinks (Proxy @UI) diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/API/Payments.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/API/Payments.hs new file mode 100644 index 00000000000..94848057e67 --- /dev/null +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/API/Payments.hs @@ -0,0 +1,47 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -Wno-orphans #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE NumericUnderscores #-} + +module Cardano.Wallet.UI.Deposit.API.Payments + ( NewReceiver (..) + , WithReceivers (..) + ) +where + +import Prelude + +import Cardano.Wallet.UI.Deposit.Types.Payments + ( Receiver (..) + ) +import Web.FormUrlEncoded + ( FromForm (..) + , parseAll + , parseUnique + ) + +data WithReceivers a + = WithReceivers + { receivers :: [Receiver] + , what :: a + } + +newtype NewReceiver = NewReceiver (WithReceivers Receiver) + +instance FromForm NewReceiver where + fromForm form = do + WithReceivers receivers () <- fromForm form + address <- parseUnique "new-receiver-address" form + amountDouble :: Double <- parseUnique "new-receiver-amount" form + let amount = round $ amountDouble * 1_000_000 + pure + $ NewReceiver + $ WithReceivers{receivers, what = Receiver{address, amount}} + +instance FromForm (WithReceivers ()) where + fromForm form = do + receivers <- parseAll "receiver-defined" form + pure + $ WithReceivers{receivers, what = ()} diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Addresses/Transactions.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Addresses/Transactions.hs index 060cee60e4c..9560f796f80 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Addresses/Transactions.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Addresses/Transactions.hs @@ -35,7 +35,7 @@ import Cardano.Wallet.Deposit.Read ) import Cardano.Wallet.Deposit.REST ( WalletResource - , customerAddress + , customerAddress, getTxHistoryByCustomer ) import Cardano.Wallet.Read ( TxId @@ -100,7 +100,10 @@ getCustomerHistory case r of Nothing -> pure $ alert "Address not discovered" Just _ -> do - h <- byCustomer <$> getMockHistory + let history = if False + then byCustomer <$> getMockHistory + else getTxHistoryByCustomer + h <- history pure $ render True params $ filterByParams params diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/Customers.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/Customers.hs index 2db76eab88d..167af07d4cd 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/Customers.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/Customers.hs @@ -39,7 +39,7 @@ import Cardano.Wallet.Deposit.Read , WithOrigin (..) ) import Cardano.Wallet.Deposit.REST - ( WalletResource + ( WalletResource, getTxHistoryByTime ) import Cardano.Wallet.Read ( TxId @@ -183,7 +183,7 @@ depositCustomersHandler let transfers = if depositsFakeData then byTime <$> getMockHistory - else error "depositsHistoryWindowHandler: real data not implemented" + else getTxHistoryByTime transfers' <- runMaybeT $ retrieveAtTimeByCustomer diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/Mock.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/Mock.hs index 91f5c53edff..45192ddb23e 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/Mock.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/Mock.hs @@ -45,7 +45,7 @@ import System.IO.Unsafe ) nMockDeposits :: Int -nMockDeposits = 100000 +nMockDeposits = 0 type TxHistoryCache = TVar (Maybe (UTCTime, [Address], TxHistory)) diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/TxIds.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/TxIds.hs index 8de6b85d04d..b9e31297373 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/TxIds.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Deposits/TxIds.hs @@ -39,7 +39,7 @@ import Cardano.Wallet.Deposit.Read , WithOrigin (..) ) import Cardano.Wallet.Deposit.REST - ( WalletResource + ( WalletResource, getTxHistoryByTime ) import Cardano.Wallet.Read ( TxId @@ -192,7 +192,7 @@ depositCustomersTxIdsHandler let transfers = if depositsFakeData then byTime <$> getMockHistory - else error "depositsHistoryWindowHandler: real data not implemented" + else getTxHistoryByTime transfers' <- runMaybeT $ retrieveAtTimeAtCustomerByTxId diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Payments/Transaction.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Payments/Transaction.hs new file mode 100644 index 00000000000..4f7bc8f9a22 --- /dev/null +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Handlers/Payments/Transaction.hs @@ -0,0 +1,65 @@ +{-# LANGUAGE NamedFieldPuns #-} + +module Cardano.Wallet.UI.Deposit.Handlers.Payments.Transaction +where + +import Prelude + +import Cardano.Read.Ledger.Tx.CBOR + ( serializeTx + ) +import Cardano.Wallet.Deposit.REST + ( WalletResource + , createPayment + ) +import Cardano.Wallet.Read + ( Coin (..) + , Value (..) + ) +import Cardano.Wallet.UI.Common.Layer + ( SessionLayer + ) +import Cardano.Wallet.UI.Deposit.Handlers.Lib + ( catchRunWalletResourceHtml + ) +import Cardano.Wallet.UI.Deposit.Types.Payments + ( Receiver (..) + ) +import Data.Text + ( Text + ) +import Servant + ( Handler + ) + +import qualified Data.ByteString.Base58 as Base58 +import qualified Data.ByteString.Lazy as BL +import qualified Data.ByteString.Lazy.Char8 as BL8 +import qualified Data.Text.Encoding as T + +createPaymentHandler + :: SessionLayer WalletResource + -> (BL.ByteString -> html) + -- ^ Function to render the exception as HTML + -> ([Receiver] -> Maybe Text -> html) + -- ^ Function to render the transaction cbor + -> [Receiver] + -- ^ Current receivers + -> Handler html +createPaymentHandler layer alert render receivers = do + catchRunWalletResourceHtml layer alert id $ do + case receivers of + [] -> pure $ render receivers Nothing + _ -> do + r <- createPayment $ do + Receiver{address, amount} <- receivers + pure (address, ValueC (CoinC $ fromIntegral amount) mempty) + pure $ case r of + Left e -> alert $ "Error: " <> BL8.pack (show e) + Right tx -> + render receivers + $ Just + $ T.decodeUtf8 + $ Base58.encodeBase58 Base58.bitcoinAlphabet + $ BL.toStrict + $ serializeTx tx diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Common.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Common.hs index ded8aaa8d87..1d69fcc2507 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Common.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Common.hs @@ -11,6 +11,7 @@ module Cardano.Wallet.UI.Deposit.Html.Common , showTimeSecs , withOriginH , valueH + , lovelaceH ) where @@ -57,6 +58,9 @@ import Lucid import Numeric ( showFFloatAlt ) +import Numeric.Natural + ( Natural + ) showTime :: UTCTime -> String showTime = formatTime defaultTimeLocale "%Y-%m-%d %H:%M" @@ -91,8 +95,13 @@ txIdH txId = txId valueH :: Value -> Html () -valueH (ValueC (CoinC c) _) = do - span_ $ toHtml $ a "" +valueH (ValueC (CoinC c) _) = lovelaceH $ fromIntegral c + +lovelaceH :: Natural -> Html () +lovelaceH c = do + span_ $ toHtml $ showLovelaceAsAda c span_ [class_ "opacity-25"] "₳" - where - a = showFFloatAlt @Double (Just 2) $ fromIntegral c / 1_000_000 + +showLovelaceAsAda :: Integral a => a -> String +showLovelaceAsAda c = + showFFloatAlt @Double (Just 2) (fromIntegral c / 1_000_000) "" diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Pages/Page.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Pages/Page.hs index e9d326d06d2..e37e1607015 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Pages/Page.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Pages/Page.hs @@ -45,6 +45,7 @@ import Cardano.Wallet.UI.Deposit.API , _Addresses , _Deposits , _Network + , _Payments , _Settings , _Wallet , aboutPageLink @@ -55,6 +56,8 @@ import Cardano.Wallet.UI.Deposit.API , navigationLink , networkInfoLink , networkPageLink + , paymentsLink + , paymentsPageLink , settingsGetLink , settingsPageLink , sseLink @@ -69,6 +72,9 @@ import Cardano.Wallet.UI.Deposit.Html.Pages.Addresses import Cardano.Wallet.UI.Deposit.Html.Pages.Deposits.Page ( depositsH ) +import Cardano.Wallet.UI.Deposit.Html.Pages.Payments.Page + ( paymentsH + ) import Cardano.Wallet.UI.Deposit.Html.Pages.Wallet ( WalletPresent , isPresent @@ -111,6 +117,7 @@ page c p = RawHtml Wallet -> walletH Addresses -> addressesH Deposits -> depositsH depositsLink + Payments -> paymentsH paymentsLink headerH :: Monad m => Page -> HtmlT m () headerH p = sseH (navigationLink $ Just p) "header" ["wallet"] @@ -126,6 +133,9 @@ headerElementH p wp = <> [ (is' _Deposits, depositPageLink, "Deposits") | isPresent wp ] + <> [ (is' _Payments, paymentsPageLink, "Payments") + | isPresent wp + ] <> [ (is' _Network, networkPageLink, "Network") , (is' _Settings, settingsPageLink, "Settings") , (is' _About, aboutPageLink, "About") diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Pages/Payments/Page.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Pages/Payments/Page.hs new file mode 100644 index 00000000000..18abe8800de --- /dev/null +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Html/Pages/Payments/Page.hs @@ -0,0 +1,173 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Cardano.Wallet.UI.Deposit.Html.Pages.Payments.Page + ( paymentsH + , paymentsElementH + , receiversH + , updateReceiversH + ) +where + +import Prelude + +import Cardano.Wallet.Deposit.IO + ( WalletPublicIdentity (..) + ) +import Cardano.Wallet.UI.Common.Html.Htmx + ( hxInclude_ + , hxPost_ + , hxSwapOob_ + , hxTarget_ + ) +import Cardano.Wallet.UI.Common.Html.Lib + ( AlertH + , WithCopy (..) + , linkText + , tdEnd + , thEnd + , truncatableText + ) +import Cardano.Wallet.UI.Common.Html.Pages.Lib + ( addressH + , box + , sseH + ) +import Cardano.Wallet.UI.Deposit.API + ( paymentsDeleteReceiverLink + , paymentsNewReceiverLink + ) +import Cardano.Wallet.UI.Deposit.Html.Common + ( lovelaceH + ) +import Cardano.Wallet.UI.Deposit.Html.Pages.Wallet + ( WalletPresent (..) + , onWalletPresentH + ) +import Cardano.Wallet.UI.Deposit.Types.Payments + ( Receiver (..) + ) +import Cardano.Wallet.UI.Type + ( WHtml + ) +import Control.Monad + ( forM_ + ) +import Data.Text + ( Text + ) +import Lucid + ( Html + , ToHtml (..) + , button_ + , class_ + , div_ + , hidden_ + , i_ + , id_ + , input_ + , name_ + , table_ + , tbody_ + , thead_ + , tr_ + , type_ + , value_ + ) +import Servant + ( Link + , ToHttpApiData (toUrlPiece) + ) + +paymentsH :: Link -> WHtml () +paymentsH paymentsLink = do + sseH paymentsLink "payments-page" ["wallet"] + +newReceiverH :: Html () +newReceiverH = do + tbody_ [id_ "new-receiver-form"] $ tr_ $ + do + tdEnd $ + input_ + [ class_ "form-control" + , type_ "text" + , name_ "new-receiver-address" + ] + tdEnd $ input_ + [ class_ "form-control" + , type_ "text" + , name_ "new-receiver-amount" + ] + tdEnd $ button_ + [ class_ "btn w-100" + , hxPost_ $ linkText paymentsNewReceiverLink + , hxTarget_ "#receivers" + , hxInclude_ "#receivers-state , #new-receiver-form" + ] + $ i_ [class_ "bi bi-plus-lg"] mempty + +receiversH :: [Receiver] -> Html () +receiversH rs = do + table_ [class_ "table table-striped"] $ do + thead_ $ do + tr_ $ do + thEnd Nothing "Address" + thEnd (Just 7) "Amount" + thEnd (Just 5) "Actions" + tbody_ [id_ "receivers-state"] + $ forM_ (zip [0 ..] rs) + $ \(n, r@Receiver{address, amount}) -> do + tr_ $ do + tdEnd $ do + addressH WithCopy address + input_ + [ hidden_ "" + , value_ $ toUrlPiece r + , name_ "receiver-defined" + ] + tdEnd $ lovelaceH amount + tdEnd + $ button_ + [ hxPost_ + $ linkText + $ paymentsDeleteReceiverLink + $ Just n + , hxTarget_ "#receivers" + , hxInclude_ "#receivers-state" + , class_ "btn w-100" + ] + $ i_ [class_ "bi bi-trash"] mempty + newReceiverH + +transactionCBORH :: Maybe Text -> Html () +transactionCBORH (Just cbor) = + truncatableText WithCopy "unsigned-transaction-copy" + $ toHtml cbor +transactionCBORH Nothing = "" + +updateReceiversH :: [Receiver] -> Maybe Text -> Html () +updateReceiversH rs cbor = do + receiversH rs + div_ [id_ "unsigned-transaction", hxSwapOob_ "innerHTML"] + $ transactionCBORH cbor + +paymentsElementH + :: AlertH + -> WalletPresent + -> Html () +paymentsElementH = onWalletPresentH $ \case + WalletPublicIdentity _xpub _customers -> + div_ + [ class_ "row mt-3 gx-0" + ] + $ do + box "New Payment" mempty + $ div_ [class_ "ms-3"] + $ do + box "Receivers" mempty + $ div_ [class_ "", id_ "receivers"] + $ receiversH [] + box "Unsigned Transaction" mempty + $ div_ [class_ "", id_ "unsigned-transaction"] + $ transactionCBORH Nothing + -- box "Payments History" mempty "To be done" diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server.hs index b864911fa93..6444fa67768 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server.hs @@ -107,6 +107,11 @@ import Cardano.Wallet.UI.Deposit.Server.Deposits.TxIds import Cardano.Wallet.UI.Deposit.Server.Lib ( renderSmoothHtml ) +import Cardano.Wallet.UI.Deposit.Server.Payments.Page + ( servePaymentsDeleteReceiver + , servePaymentsNewReceiver + , servePaymentsPage + ) import Cardano.Wallet.UI.Deposit.Server.Wallet ( serveDeleteWallet , serveDeleteWalletModal @@ -159,6 +164,7 @@ serveUI tr ul env dbDir config nid nl bs = :<|> serveTabPage ul config Wallet :<|> serveTabPage ul config Addresses :<|> serveTabPage ul config Deposits + :<|> serveTabPage ul config Payments :<|> serveNetworkInformation nid nl bs :<|> serveSSESettings ul :<|> serveToggleSSE ul @@ -182,6 +188,9 @@ serveUI tr ul env dbDir config nid nl bs = :<|> serveDepositsCustomerPagination ul :<|> serveDepositsCustomersTxIds ul :<|> serveDepositsCustomersTxIdsPagination ul + :<|> servePaymentsPage ul + :<|> servePaymentsNewReceiver ul + :<|> servePaymentsDeleteReceiver ul serveTabPage :: UILayer s diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/Customers.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/Customers.hs index 882eac609a9..e7b7b040096 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/Customers.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/Customers.hs @@ -21,7 +21,7 @@ import Cardano.Wallet.Deposit.Pure.API.TxHistory ) import Cardano.Wallet.Deposit.REST ( WalletResource - , WalletResourceM + , WalletResourceM, getTxHistoryByTime ) import Cardano.Wallet.UI.Common.Handlers.Session ( withSessionLayer @@ -98,10 +98,13 @@ depositsCustomersTable -> DownTime -> WalletResourceM (Scrolling WalletResourceM Customer) depositsCustomersTable params time = do - let hs = + let history = if depositsFakeData params + then byTime <$> getMockHistory + else getTxHistoryByTime + hs = depositCustomersPaginateM params - (byTime <$> getMockHistory) + history time 100 newScrolling diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/Times.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/Times.hs index e5439135cb6..a59228715f1 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/Times.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/Times.hs @@ -15,7 +15,7 @@ import Cardano.Wallet.Deposit.Pure.API.TxHistory ) import Cardano.Wallet.Deposit.REST ( WalletResource - , WalletResourceM + , WalletResourceM, getTxHistoryByTime ) import Cardano.Wallet.UI.Common.Handlers.Session ( withSessionLayer @@ -89,10 +89,13 @@ depositsTable :: DepositsParams -> WalletResourceM (Scrolling WalletResourceM DownTime) depositsTable params = do - let hs = + let history = if depositsFakeData params + then byTime <$> getMockHistory + else getTxHistoryByTime + hs = depositsPaginateM params - (byTime <$> getMockHistory) + history 100 newScrolling $ scrollableDeposits diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/TxIds.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/TxIds.hs index e244caaf513..0637b01b650 100644 --- a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/TxIds.hs +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Deposits/TxIds.hs @@ -21,7 +21,7 @@ import Cardano.Wallet.Deposit.Pure.API.TxHistory ) import Cardano.Wallet.Deposit.REST ( WalletResource - , WalletResourceM + , WalletResourceM, getTxHistoryByTime ) import Cardano.Wallet.Read ( TxId @@ -100,10 +100,13 @@ depositsCustomersTxIdsTable -> Customer -> WalletResourceM (Scrolling WalletResourceM TxId) depositsCustomersTxIdsTable params time customer = do - let hs = + let history = if depositsFakeData params + then byTime <$> getMockHistory + else getTxHistoryByTime + hs = depositCustomersTxIdsPaginateM params - (byTime <$> getMockHistory) + history time customer 100 diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Payments/Page.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Payments/Page.hs new file mode 100644 index 00000000000..0b98184a703 --- /dev/null +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Server/Payments/Page.hs @@ -0,0 +1,96 @@ +{-# LANGUAGE NamedFieldPuns #-} + +module Cardano.Wallet.UI.Deposit.Server.Payments.Page + ( servePaymentsPage + , servePaymentsNewReceiver + , servePaymentsDeleteReceiver + ) +where + +import Prelude + +import Cardano.Wallet.Deposit.REST + ( WalletResource + ) +import Cardano.Wallet.UI.Common.Handlers.Session + ( withSessionLayer + ) +import Cardano.Wallet.UI.Common.Html.Html + ( RawHtml (..) + , renderHtml + ) +import Cardano.Wallet.UI.Common.Html.Pages.Lib + ( alertH + ) +import Cardano.Wallet.UI.Common.Layer + ( UILayer (..) + ) +import Cardano.Wallet.UI.Cookies + ( CookieResponse + , RequestCookies + ) +import Cardano.Wallet.UI.Deposit.API.Payments + ( NewReceiver (..) + , WithReceivers (..) + ) +import Cardano.Wallet.UI.Deposit.Handlers.Lib + ( walletPresence + ) +import Cardano.Wallet.UI.Deposit.Handlers.Payments.Transaction + ( createPaymentHandler + ) +import Cardano.Wallet.UI.Deposit.Html.Pages.Payments.Page + ( paymentsElementH + , updateReceiversH + ) +import Cardano.Wallet.UI.Deposit.Server.Lib + ( renderSmoothHtml + ) +import Servant + ( Handler + ) + +servePaymentsPage + :: UILayer WalletResource + -> Maybe RequestCookies + -> Handler (CookieResponse RawHtml) +servePaymentsPage ul = withSessionLayer ul $ \layer -> do + wp <- walletPresence layer + pure + $ renderSmoothHtml + $ paymentsElementH alertH wp + +servePaymentsNewReceiver + :: UILayer WalletResource + -> NewReceiver + -> Maybe RequestCookies + -> Handler (CookieResponse RawHtml) +servePaymentsNewReceiver + ul + (NewReceiver WithReceivers{receivers, what = newReceiver}) = + withSessionLayer ul $ \layer -> do + _wp <- walletPresence layer + let newReceivers = receivers ++ [newReceiver] + renderHtml + <$> createPaymentHandler layer alertH updateReceiversH newReceivers + +servePaymentsDeleteReceiver + :: UILayer WalletResource + -> WithReceivers () + -> Maybe Int + -> Maybe RequestCookies + -> Handler (CookieResponse RawHtml) +servePaymentsDeleteReceiver + ul + WithReceivers{receivers} + (Just receiverN) = + withSessionLayer ul $ \layer -> do + _wp <- walletPresence layer + let newReceivers = + fmap snd + $ filter ((/= receiverN) . fst) + $ zip [0 ..] receivers + renderHtml + <$> createPaymentHandler layer alertH updateReceiversH newReceivers +servePaymentsDeleteReceiver _ _ _ = + error "servePaymentsDeleteReceiver: receiver-number is required" diff --git a/lib/ui/src/Cardano/Wallet/UI/Deposit/Types/Payments.hs b/lib/ui/src/Cardano/Wallet/UI/Deposit/Types/Payments.hs new file mode 100644 index 00000000000..7d22d2a4527 --- /dev/null +++ b/lib/ui/src/Cardano/Wallet/UI/Deposit/Types/Payments.hs @@ -0,0 +1,62 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +module Cardano.Wallet.UI.Deposit.Types.Payments + ( Receiver (..) + ) +where + +import Prelude + +import Cardano.Wallet.Deposit.Pure.API.Address + ( decodeAddress + , encodeAddress + ) +import Cardano.Wallet.Deposit.Read + ( Address + ) +import Numeric.Natural + ( Natural + ) +import Web.HttpApiData + ( FromHttpApiData (parseUrlPiece) + , ToHttpApiData (toUrlPiece) + ) + +import qualified Data.Text as T + +-- | A receiver of a payment. +data Receiver = Receiver + { address :: Address + -- ^ The address of the receiver. + , amount :: Natural + -- ^ The amount of lovelace to send to the receiver. + } + deriving (Eq, Show) + +instance FromHttpApiData Receiver where + parseUrlPiece t = case T.splitOn "," t of + [addressText, amountText] -> do + amount :: Natural <- case reads (T.unpack amountText) of + [(n, "")] -> pure n + _ -> Left "Amount must be a number" + address <- parseUrlPiece addressText + pure $ Receiver{address, amount} + _ -> Left "Receiver must be in the format 'address,amount'" + +instance ToHttpApiData Receiver where + toUrlPiece Receiver{address, amount} = + T.intercalate "," + [ encodeAddress address + , T.pack $ show amount + ] + +instance FromHttpApiData Address where + parseUrlPiece t = case decodeAddress t of + Left err -> Left $ T.pack $ show err + Right address -> pure address