diff --git a/README.md b/README.md index ac62917c..9d5b41c5 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ For details please see the following section: maestroToken: YOUR_MAESTRO_TOKEN # API key to protect server endpoints with. It's value must be provided under `api-key` header of request. serverApiKey: YOUR_SECRET_KEY + # TapTools API key, to access historical prices using TapTools. + tapToolsApiKey: YOUR_TAP_TOOLS_KEY # Optionally, wallet key details if one wants server to be able to sign transactions using this key. wallet: tag: mnemonicWallet diff --git a/geniusyield-dex-api/CHANGELOG.md b/geniusyield-dex-api/CHANGELOG.md index 8bca76a1..bd583a48 100644 --- a/geniusyield-dex-api/CHANGELOG.md +++ b/geniusyield-dex-api/CHANGELOG.md @@ -1,5 +1,10 @@ # Revision history for geniusyield-dex-api +## 0.2.1.0 -- 2024-05-07 + +* Adds `placePartialOrder''`, `placePartialOrderWithVersion''` to also return for order's NFT token. +* Exports `placePartialOrder''`, `placePartialOrderWithVersion`, `placePartialOrderWithVersion'` and `placePartialOrderWithVersion''`. + ## 0.2.0.0 -- 2024-04-17 * Adds support for v1.1 family of scripts. diff --git a/geniusyield-dex-api/geniusyield-dex-api.cabal b/geniusyield-dex-api/geniusyield-dex-api.cabal index 9cb137ad..4ccea27f 100644 --- a/geniusyield-dex-api/geniusyield-dex-api.cabal +++ b/geniusyield-dex-api/geniusyield-dex-api.cabal @@ -4,7 +4,7 @@ name: geniusyield-dex-api -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change -version: 0.2.0.0 +version: 0.2.1.0 synopsis: API code to interact with GeniusYield DEX. description: API code to interact with GeniusYield DEX. Learn more about GeniusYield by visiting https://www.geniusyield.co/. @@ -72,10 +72,8 @@ library build-depends: , aeson , base ^>=4.16.4.0 - , bytestring , containers , data-default - , file-embed , http-types , lens , mtl diff --git a/geniusyield-dex-api/src/GeniusYield/Api/Dex/PartialOrder.hs b/geniusyield-dex-api/src/GeniusYield/Api/Dex/PartialOrder.hs index 15d45a8f..6f855a0d 100644 --- a/geniusyield-dex-api/src/GeniusYield/Api/Dex/PartialOrder.hs +++ b/geniusyield-dex-api/src/GeniusYield/Api/Dex/PartialOrder.hs @@ -40,6 +40,10 @@ module GeniusYield.Api.Dex.PartialOrder ( -- * Tx constructors placePartialOrder, placePartialOrder', + placePartialOrder'', + placePartialOrderWithVersion, + placePartialOrderWithVersion', + placePartialOrderWithVersion'', completelyFillPartialOrder, partiallyFillPartialOrder, fillPartialOrder, @@ -603,7 +607,33 @@ placePartialOrder' → GYTxOutRef → PartialOrderConfigInfoF GYAddress → m (GYTxSkeleton 'PlutusV2) -placePartialOrder' pors = placePartialOrderWithVersion' pors defaultPOCVersion +placePartialOrder' pors addr (offerAmt, offerAC) priceAC price start end addLov addOff stakeCred cfgRef pocd = snd <$> placePartialOrder'' pors addr (offerAmt, offerAC) priceAC price start end addLov addOff stakeCred cfgRef pocd + +placePartialOrder'' + ∷ (GYDexApiMonad m a, HasCallStack) + ⇒ PORefs + → GYAddress + -- ^ Order owner + → (Natural, GYAssetClass) + -- ^ Amount and asset to offer. + → GYAssetClass + -- ^ The asset being asked for as payment. + → GYRational + -- ^ The price for one unit of the offered asset. + → Maybe GYTime + -- ^ The earliest time when the order can be filled (optional). + → Maybe GYTime + -- ^ The latest time when the order can be filled (optional). + → Natural + -- ^ Additional lovelace fee. + → Natural + -- ^ Additional fee in offered tokens. + → Maybe GYStakeCredential + -- ^ Stake credential of user. We do not support pointer reference. + → GYTxOutRef + → PartialOrderConfigInfoF GYAddress + → m (GYAssetClass, GYTxSkeleton 'PlutusV2) +placePartialOrder'' pors = placePartialOrderWithVersion'' pors defaultPOCVersion placePartialOrderWithVersion' ∷ (GYDexApiMonad m a, HasCallStack) @@ -630,7 +660,34 @@ placePartialOrderWithVersion' → GYTxOutRef → PartialOrderConfigInfoF GYAddress → m (GYTxSkeleton 'PlutusV2) -placePartialOrderWithVersion' pors pocVersion addr (offerAmt, offerAC) priceAC price start end addLov addOff stakeCred cfgRef pocd = do +placePartialOrderWithVersion' pors pocVersion addr (offerAmt, offerAC) priceAC price start end addLov addOff stakeCred cfgRef pocd = snd <$> placePartialOrderWithVersion'' pors pocVersion addr (offerAmt, offerAC) priceAC price start end addLov addOff stakeCred cfgRef pocd + +placePartialOrderWithVersion'' + ∷ (GYDexApiMonad m a, HasCallStack) + ⇒ PORefs + → POCVersion + → GYAddress + -- ^ Order owner + → (Natural, GYAssetClass) + -- ^ Amount and asset to offer. + → GYAssetClass + -- ^ The asset being asked for as payment. + → GYRational + -- ^ The price for one unit of the offered asset. + → Maybe GYTime + -- ^ The earliest time when the order can be filled (optional). + → Maybe GYTime + -- ^ The latest time when the order can be filled (optional). + → Natural + -- ^ Additional lovelace fee. + → Natural + -- ^ Additional fee in offered tokens. + → Maybe GYStakeCredential + -- ^ Stake credential of user. We do not support pointer reference. + → GYTxOutRef + → PartialOrderConfigInfoF GYAddress + → m (GYAssetClass, GYTxSkeleton 'PlutusV2) +placePartialOrderWithVersion'' pors pocVersion addr (offerAmt, offerAC) priceAC price start end addLov addOff stakeCred cfgRef pocd = do when (offerAmt == 0) $ throwAppError $ PodNonPositiveAmount $ toInteger offerAmt when (price <= 0) $ throwAppError $ PodNonPositivePrice price when (offerAC == priceAC) $ throwAppError $ PodNonDifferentAssets offerAC @@ -696,11 +753,12 @@ placePartialOrderWithVersion' pors pocVersion addr (offerAmt, offerAC) priceAC p o = mkGYTxOut outAddr' offerV (datumFromPlutusData od) return $ - mustHaveInput nftInput - <> mustHaveOutput o - <> mustMint (GYMintReference porMintRef $ mintingPolicyToScript policy) nftRedeemer nftName 1 - <> mustHaveRefInput cfgRef - <> mustHaveTxMetadata stampPlaced + (nft,) $ + mustHaveInput nftInput + <> mustHaveOutput o + <> mustMint (GYMintReference porMintRef $ mintingPolicyToScript policy) nftRedeemer nftName 1 + <> mustHaveRefInput cfgRef + <> mustHaveTxMetadata stampPlaced -- | Fills an order. If the provided amount of offered tokens to buy is equal to the offered amount, then we completely fill the order. Otherwise, it gets partially filled. fillPartialOrder diff --git a/geniusyield-server-lib/CHANGELOG.md b/geniusyield-server-lib/CHANGELOG.md index 355b584c..39c620be 100644 --- a/geniusyield-server-lib/CHANGELOG.md +++ b/geniusyield-server-lib/CHANGELOG.md @@ -1,5 +1,13 @@ # Revision history for geniusyield-server-lib +## 0.3.0 -- 2024-05-07 + +* Adds TapTools OHLCV endpoint. +* Adds NFT token in response of place order family of endpoints. +* Adds GET variant for getting details of an order from it's NFT token identifier. +* Clarifies which endpoints require `maestroToken` field to be set. +* Clarifies which endpoints require signing key to be configured in the server to derive for wallet's address, likewise it is clarified that which endpoints use fields such as `collateral`, etc. from server's configuration. + ## 0.2.0 -- 2024-04-22 * Uses latest version of `geniusyield-dex-api` which adds support of v1.1 script. diff --git a/geniusyield-server-lib/geniusyield-server-lib.cabal b/geniusyield-server-lib/geniusyield-server-lib.cabal index 919440bd..4be9547d 100644 --- a/geniusyield-server-lib/geniusyield-server-lib.cabal +++ b/geniusyield-server-lib/geniusyield-server-lib.cabal @@ -1,6 +1,6 @@ cabal-version: 3.6 name: geniusyield-server-lib -version: 0.2.0 +version: 0.3.0 synopsis: GeniusYield server library description: Library for GeniusYield server. license: Apache-2.0 @@ -58,10 +58,13 @@ library GeniusYield.Server.Constants GeniusYield.Server.Ctx GeniusYield.Server.Dex.HistoricalPrices.Maestro + GeniusYield.Server.Dex.HistoricalPrices.TapTools + GeniusYield.Server.Dex.HistoricalPrices.TapTools.Client GeniusYield.Server.Dex.Markets GeniusYield.Server.Dex.PartialOrder GeniusYield.Server.ErrorMiddleware GeniusYield.Server.Options + GeniusYield.Server.Orphans GeniusYield.Server.RequestLoggerMiddleware GeniusYield.Server.Run GeniusYield.Server.Tx @@ -78,7 +81,7 @@ library build-depends: , aeson , atlas-cardano - , base ^>=4.16.4.0 + , base ^>=4.16.4.0 , binary , bytestring , cardano-api @@ -89,6 +92,8 @@ library , geniusyield-dex-api , geniusyield-orderbot-lib , githash + , http-client + , http-client-tls , http-types , insert-ordered-containers , lens @@ -96,6 +101,10 @@ library , plutus-ledger-api , ply-core , rio + , servant + , servant-checked-exceptions + , servant-client + , servant-client-core , servant-foreign , servant-server , servant-swagger diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Api.hs b/geniusyield-server-lib/src/GeniusYield/Server/Api.hs index c37e131a..51c4573a 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Api.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Api.hs @@ -34,8 +34,10 @@ import GeniusYield.Server.Auth (APIKeyAuthProtect, V0) import GeniusYield.Server.Constants (gitHash) import GeniusYield.Server.Ctx import GeniusYield.Server.Dex.HistoricalPrices.Maestro +import GeniusYield.Server.Dex.HistoricalPrices.TapTools (TapToolsPriceHistoryAPI, handleTapToolsPriceHistoryApi) import GeniusYield.Server.Dex.Markets (MarketsAPI, handleMarketsApi) import GeniusYield.Server.Dex.PartialOrder (OrderInfo (..), OrdersAPI, handleOrdersApi, poiToOrderInfo) +import GeniusYield.Server.Orphans () import GeniusYield.Server.Tx (TxAPI, handleTxApi) import GeniusYield.Server.Utils import GeniusYield.TxBuilder (GYTxQueryMonad (utxosAtAddress)) @@ -186,6 +188,10 @@ type OrderBookAPI = Summary "Order book" :> Description "Get order book for a sp type BalancesAPI = Summary "Balances" :> Description "Get token balances of an address." :> Capture "address" GYAddressBech32 :> Get '[JSON] GYBalance +type HistoricalPricesAPI = + "maestro" :> MaestroPriceHistoryAPI + :<|> "tap-tools" :> TapToolsPriceHistoryAPI + type V0API = "settings" :> SettingsAPI :<|> "orders" :> OrdersAPI @@ -194,7 +200,7 @@ type V0API = :<|> "trading-fees" :> TradingFeesAPI :<|> "assets" :> AssetsAPI :<|> "order-books" :> OrderBookAPI - :<|> "historical-prices" :> "maestro" :> MaestroPriceHistoryAPI + :<|> "historical-prices" :> HistoricalPricesAPI :<|> "balances" :> BalancesAPI type GeniusYieldAPI = APIKeyAuthProtect :> V0 :> V0API @@ -239,7 +245,7 @@ geniusYieldAPISwagger = & applyTagsFor (subOperations (Proxy ∷ Proxy ("trading-fees" +> TradingFeesAPI)) (Proxy ∷ Proxy GeniusYieldAPI)) ["Trading Fees" & description ?~ "Endpoint to get trading fees of DEX."] & applyTagsFor (subOperations (Proxy ∷ Proxy ("assets" +> AssetsAPI)) (Proxy ∷ Proxy GeniusYieldAPI)) ["Assets" & description ?~ "Endpoint to fetch asset details."] & applyTagsFor (subOperations (Proxy ∷ Proxy ("order-books" +> OrderBookAPI)) (Proxy ∷ Proxy GeniusYieldAPI)) ["Order Book" & description ?~ "Endpoint to fetch order book."] - & applyTagsFor (subOperations (Proxy ∷ Proxy ("historical-prices" +> "maestro" :> MaestroPriceHistoryAPI)) (Proxy ∷ Proxy GeniusYieldAPI)) ["Historical Prices" & description ?~ "Endpoints to fetch historical prices."] + & applyTagsFor (subOperations (Proxy ∷ Proxy ("historical-prices" +> HistoricalPricesAPI)) (Proxy ∷ Proxy GeniusYieldAPI)) ["Historical Prices" & description ?~ "Endpoints to fetch historical prices."] & applyTagsFor (subOperations (Proxy ∷ Proxy ("balances" +> BalancesAPI)) (Proxy ∷ Proxy GeniusYieldAPI)) ["Balances" & description ?~ "Endpoint to fetch token balances."] geniusYieldServer ∷ Ctx → ServerT GeniusYieldAPI IO @@ -252,11 +258,16 @@ geniusYieldServer ctx = :<|> handleTradingFeesApi ctx :<|> handleAssetsApi ctx :<|> handleOrderBookApi ctx - :<|> handleMaestroPriceHistoryApi ctx + :<|> handleHistoricalPricesApi ctx :<|> handleBalancesApi ctx where ignoredAuthResult f _authResult = f +handleHistoricalPricesApi ∷ Ctx → ServerT HistoricalPricesAPI IO +handleHistoricalPricesApi ctx = + handleMaestroPriceHistoryApi ctx + :<|> handleTapToolsPriceHistoryApi ctx + type MainAPI = GeniusYieldAPI diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Assets.hs b/geniusyield-server-lib/src/GeniusYield/Server/Assets.hs index a3891332..19a0bebd 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Assets.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Assets.hs @@ -4,6 +4,7 @@ module GeniusYield.Server.Assets ( ) where import Fmt +import GHC.TypeLits (AppendSymbol) import GeniusYield.OrderBot.Domain.Assets import GeniusYield.Server.Ctx import GeniusYield.Server.Utils @@ -11,7 +12,7 @@ import GeniusYield.Types import RIO hiding (logDebug, logInfo) import Servant -type AssetsAPI = Summary "Get assets information" :> Description "Get information for a specific asset." :> Capture "asset" GYAssetClass :> Get '[JSON] AssetDetails +type AssetsAPI = Summary "Get assets information" :> Description ("Get information for a specific asset. " `AppendSymbol` CommonMaestroKeyRequirementText) :> Capture "asset" GYAssetClass :> Get '[JSON] AssetDetails handleAssetsApi ∷ Ctx → GYAssetClass → IO AssetDetails handleAssetsApi ctx@Ctx {..} ac = do diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Auth.hs b/geniusyield-server-lib/src/GeniusYield/Server/Auth.hs index 6cba7563..4718973a 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Auth.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Auth.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-orphans #-} - module GeniusYield.Server.Auth ( V0, ApiKey, @@ -10,17 +8,12 @@ module GeniusYield.Server.Auth ( APIKeyAuthProtect, ) where -import Control.Lens (at, (?~)) -import Data.HashMap.Strict.InsOrd qualified as IOHM -import Data.Swagger import GHC.TypeLits (Symbol, symbolVal) import Network.Wai (Request (requestHeaders)) import RIO import RIO.Text qualified as T import Servant -import Servant.Foreign import Servant.Server.Experimental.Auth (AuthHandler, AuthServerData, mkAuthHandler) -import Servant.Swagger type V0 ∷ Symbol type V0 = "v0" @@ -50,68 +43,3 @@ apiKeyAuthHandler (ApiKey key) = mkAuthHandler handler type APIKeyAuthProtect = AuthProtect ApiKeyHeader type instance AuthServerData APIKeyAuthProtect = () - -instance HasSwagger api ⇒ HasSwagger (APIKeyAuthProtect :> api) where - toSwagger _ = - toSwagger (Proxy ∷ Proxy api) - & securityDefinitions - .~ SecurityDefinitions (IOHM.fromList [(apiKeyHeaderText, apiKeySecurityScheme)]) - -- & paths - -- . at signingKeyReqEndpoint - -- . _Just - -- . post - -- . _Just - -- . responses - -- %~ add500SigningKeyFailureResponse - -- & paths - -- . at signingKeyReqEndpoint - -- . _Just - -- . delete - -- . _Just - -- . responses - -- %~ add500SigningKeyFailureResponse - & allOperations - . security - .~ [SecurityRequirement (IOHM.singleton apiKeyHeaderText [])] - & allOperations - . responses - %~ addCommonResponses - where - apiKeySecurityScheme ∷ SecurityScheme - apiKeySecurityScheme = - SecurityScheme - { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams apiKeyHeaderText ApiKeyHeader), - _securitySchemeDescription = Just "API key for accessing the server's API." - } - addCommonResponses ∷ Responses → Responses - addCommonResponses resps = resps & at 401 ?~ Inline response401 & at 403 ?~ Inline response403 & at 500 ?~ Inline response500 - - -- add500SigningKeyFailureResponse ∷ Responses → Responses - -- add500SigningKeyFailureResponse resps = resps & at 500 ?~ Inline response500SigningKeyFailure - - response401 ∷ Response - response401 = mempty & description .~ "Unauthorized access - API key missing" - - response403 ∷ Response - response403 = mempty & description .~ "Forbidden - The API key does not have permission to perform the request" - - response500 ∷ Response - response500 = mempty & description .~ "Internal server error" - --- response500SigningKeyFailure ∷ Response --- response500SigningKeyFailure = mempty & description .~ "Internal server error - Corresponding signing key is not configured" - --- signingKeyReqEndpoint = "/" <> symbolVal (Proxy ∷ Proxy V0) <> "/orders" - --- `HasForeign` instance for `APIKeyAuthProtect :> api` is required to generate client code using libraries such as `servant-py`. --- This is written with help from https://github.com/haskell-servant/servant-auth/issues/8#issue-185541839. -instance ∀ lang ftype api. (HasForeign lang ftype api, HasForeignType lang ftype Text) ⇒ HasForeign lang ftype (APIKeyAuthProtect :> api) where - type Foreign ftype (APIKeyAuthProtect :> api) = Foreign ftype api - foreignFor lang Proxy Proxy subR = foreignFor lang Proxy (Proxy ∷ Proxy api) subR' - where - subR' = subR {_reqHeaders = HeaderArg arg : _reqHeaders subR} - arg = - Arg - { _argName = "api-key", - _argType = typeFor lang (Proxy ∷ Proxy ftype) (Proxy ∷ Proxy Text) - } diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Config.hs b/geniusyield-server-lib/src/GeniusYield/Server/Config.hs index e04b15df..293c3b50 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Config.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Config.hs @@ -56,6 +56,7 @@ data ServerConfig = ServerConfig scPort ∷ !Port, scWallet ∷ !(Maybe UserWallet), scServerApiKey ∷ !(Confidential Text), + scTapToolsApiKey ∷ !(Maybe (Confidential Text)), scCollateral ∷ !(Maybe GYTxOutRef), scStakeAddress ∷ !(Maybe GYStakeAddressBech32) } diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Ctx.hs b/geniusyield-server-lib/src/GeniusYield/Server/Ctx.hs index 59ac32a7..9e2775b8 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Ctx.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Ctx.hs @@ -2,6 +2,8 @@ module GeniusYield.Server.Ctx ( DEXInfo (..), dexInfoDefaultMainnet, dexInfoDefaultPreprod, + TapToolsApiKey, + TapToolsEnv (..), Ctx (..), runSkeletonI, runSkeletonWithStrategyI, @@ -19,6 +21,14 @@ import GeniusYield.Transaction import GeniusYield.TxBuilder import GeniusYield.Types import RIO +import Servant.Client (ClientEnv) + +type TapToolsApiKey = Text + +data TapToolsEnv = TapToolsEnv + { tteClientEnv ∷ !ClientEnv, + tteApiKey ∷ !TapToolsApiKey + } -- | Server context: configuration & shared state. data Ctx = Ctx @@ -26,6 +36,7 @@ data Ctx = Ctx ctxProviders ∷ !GYProviders, ctxDexInfo ∷ !DEXInfo, ctxMaestroProvider ∷ !MaestroProvider, + ctxTapToolsProvider ∷ !(Maybe TapToolsEnv), ctxSigningKey ∷ !(Maybe (Pair GYSomePaymentSigningKey GYAddress)), ctxCollateral ∷ !(Maybe GYTxOutRef), ctxStakeAddress ∷ !(Maybe GYStakeAddressBech32) diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/Maestro.hs b/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/Maestro.hs index 6958e79c..d47e7a7f 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/Maestro.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/Maestro.hs @@ -8,13 +8,11 @@ module GeniusYield.Server.Dex.HistoricalPrices.Maestro ( import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) -import Data.Kind (Type) import Data.Swagger qualified as Swagger -import Data.Swagger.Internal qualified as Swagger import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import Deriving.Aeson import Fmt -import GHC.TypeLits (Symbol) +import GHC.TypeLits (AppendSymbol, Symbol) import GeniusYield.OrderBot.Adapter.Maestro (MaestroProvider (..), handleMaestroError) import GeniusYield.OrderBot.Domain.Assets (AssetDetails (adAssetTicker), AssetTicker (..)) import GeniusYield.OrderBot.Domain.Markets (OrderAssetPair (..)) @@ -93,9 +91,6 @@ newtype MaestroOrder = MaestroOrder {unMaestroOrder ∷ Order} deriving stock (Show) deriving newtype (ToHttpApiData, FromHttpApiData, Enum, Bounded, ToJSON) -commonEnumParamSchemaRecipe ∷ ∀ a (t ∷ Swagger.SwaggerKind Type). (Bounded a, Enum a, ToJSON a) ⇒ Proxy a → Swagger.ParamSchema t -commonEnumParamSchemaRecipe _ = mempty & Swagger.type_ ?~ Swagger.SwaggerString & Swagger.enum_ ?~ fmap toJSON [(minBound ∷ a) .. maxBound] - instance Swagger.ToParamSchema MaestroOrder where toParamSchema = commonEnumParamSchemaRecipe @@ -115,7 +110,7 @@ instance Swagger.ToParamSchema MaestroDex where type MaestroPriceHistoryAPI = Summary "Get price history using Maestro." - :> Description "This endpoint internally calls Maestro's \"DEX And Pair OHLC\" endpoint." + :> Description ("This endpoint internally calls Maestro's \"DEX And Pair OHLC\" endpoint. " `AppendSymbol` CommonMaestroKeyRequirementText) :> Capture "market-id" OrderAssetPair :> Capture "dex" MaestroDex :> QueryParam "resolution" MaestroResolution diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/TapTools.hs b/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/TapTools.hs new file mode 100644 index 00000000..10653a3f --- /dev/null +++ b/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/TapTools.hs @@ -0,0 +1,46 @@ +module GeniusYield.Server.Dex.HistoricalPrices.TapTools ( + TapToolsPriceHistoryAPI, + handleTapToolsPriceHistoryApi, +) where + +import Control.Lens ((?~)) +import Data.Swagger qualified as Swagger +import Data.Swagger.Internal.Schema qualified as Swagger +import Fmt +import GeniusYield.Server.Ctx +import GeniusYield.Server.Dex.HistoricalPrices.TapTools.Client (TapToolsInterval, TapToolsOHLCV, TapToolsUnit (TapToolsUnit), handleTapToolsError, tapToolsOHLCV) +import GeniusYield.Server.Utils +import GeniusYield.Types +import RIO hiding (logDebug, logInfo) +import Servant + +newtype TapToolsNumIntervals = TapToolsNumIntervals {unTapToolsNumIntervals ∷ Natural} + deriving stock (Eq, Ord, Show) + deriving newtype (FromHttpApiData, Swagger.ToParamSchema) + +-- Since this is a query parameter, our schema description wouldn't be registered for in swagger specification :(. Following OpenAPI 3.0 would allow for it. +instance Swagger.ToSchema TapToolsNumIntervals where + declareNamedSchema p = + pure $ + Swagger.named "TapToolsNumIntervals" $ + Swagger.paramSchemaToSchema p + & Swagger.description + ?~ "The number of intervals to return, e.g. if you want 180 days of data in 1d intervals, then pass 180 here." + +type TapToolsPriceHistoryAPI = + Summary "Get price history using TapTools." + :> Description "This endpoint internally calls TapTools's \"Token price OHLCV\" endpoint. Note that only the liquidity pools involving ADA and the given asset class is considered to get for aggregated price information. Price returned is in ADA." + :> Capture "asset" GYAssetClass + :> QueryParam' '[Required, Strict] "interval" TapToolsInterval + :> QueryParam "numIntervals" TapToolsNumIntervals + :> Get '[JSON] [TapToolsOHLCV] + +throwNoTapToolsKeyError ∷ IO a +throwNoTapToolsKeyError = throwIO $ err500 {errBody = "No API key configured for TapTools."} + +handleTapToolsPriceHistoryApi ∷ Ctx → GYAssetClass → TapToolsInterval → Maybe TapToolsNumIntervals → IO [TapToolsOHLCV] +handleTapToolsPriceHistoryApi ctx token tti (fmap unTapToolsNumIntervals → mttni) = do + logInfo ctx $ "Fetching price history. Token: " +|| token ||+ ", interval: " +|| tti ||+ "" + case ctxTapToolsProvider ctx of + Nothing → throwNoTapToolsKeyError + Just te → try (tapToolsOHLCV te (Just (TapToolsUnit token)) tti mttni) >>= handleTapToolsError "handleTapToolsPriceHistory" diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/TapTools/Client.hs b/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/TapTools/Client.hs new file mode 100644 index 00000000..ba6fcd49 --- /dev/null +++ b/geniusyield-server-lib/src/GeniusYield/Server/Dex/HistoricalPrices/TapTools/Client.hs @@ -0,0 +1,143 @@ +module GeniusYield.Server.Dex.HistoricalPrices.TapTools.Client ( + TapToolsUnit (..), + TapToolsInterval (..), + TapToolsOHLCV (..), + TapToolsAPI, + TapToolsOHLCVAPI, + tapToolsClientEnv, + tapToolsOHLCV, + TapToolsException, + handleTapToolsError, +) where + +import Data.Aeson (ToJSON (..)) +import Data.Swagger qualified as Swagger +import Data.Time.Clock.POSIX +import Deriving.Aeson +import GHC.TypeLits (Symbol, symbolVal) +import GeniusYield.Server.Ctx (TapToolsApiKey, TapToolsEnv (tteApiKey, tteClientEnv)) +import GeniusYield.Server.Utils (commonEnumParamSchemaRecipe, hideServantClientErrorHeader) +import GeniusYield.Swagger.Utils +import GeniusYield.Types (GYAssetClass) +import Maestro.Types.Common (LowerFirst) +import Network.HTTP.Client (newManager) +import Network.HTTP.Client.TLS (tlsManagerSettings) +import RIO +import RIO.Text qualified as Text +import Servant.API +import Servant.Client + +{- $setup + +>>> :set -XOverloadedStrings -XTypeApplications +>>> import GeniusYield.Types +-} + +newtype TapToolsUnit = TapToolsUnit {unTapToolsUnit ∷ GYAssetClass} + deriving stock (Eq, Ord, Show) + +{- | + +>>> toUrlPiece $ TapToolsUnit "dda5fdb1002f7389b33e036b6afee82a8189becb6cba852e8b79b4fb.0014df1047454e53" +"dda5fdb1002f7389b33e036b6afee82a8189becb6cba852e8b79b4fb0014df1047454e53" +-} +instance ToHttpApiData TapToolsUnit where + toUrlPiece (TapToolsUnit ac) = removeDot $ toUrlPiece ac + where + removeDot = Text.filter (/= '.') + +data TapToolsInterval = TTI3m | TTI5m | TTI15m | TTI30m | TTI1h | TTI2h | TTI4h | TTI12h | TTI1d | TTI3d | TTI1w | TTI1M + deriving stock (Eq, Ord, Enum, Bounded, Data, Typeable, Generic) + deriving (FromJSON, ToJSON) via CustomJSON '[ConstructorTagModifier '[StripPrefix "TTI"]] TapToolsInterval + +-- >>> show TTI1M +-- "1M" +instance Show TapToolsInterval where + show = toConstr >>> show >>> drop 3 + +instance ToHttpApiData TapToolsInterval where + toQueryParam = Text.pack . show + +instance FromHttpApiData TapToolsInterval where + parseQueryParam = \case + "3m" → Right TTI3m + "5m" → Right TTI5m + "15m" → Right TTI15m + "30m" → Right TTI30m + "1h" → Right TTI1h + "2h" → Right TTI2h + "4h" → Right TTI4h + "12h" → Right TTI12h + "1d" → Right TTI1d + "3d" → Right TTI3d + "1w" → Right TTI1w + "1M" → Right TTI1M + x → Left $ "Invalid TapToolsInterval: " <> x + +instance Swagger.ToParamSchema TapToolsInterval where + toParamSchema = commonEnumParamSchemaRecipe + +type TapToolsOHLCVPrefix ∷ Symbol +type TapToolsOHLCVPrefix = "tapToolsOHLCV" + +data TapToolsOHLCV = TapToolsOHLCV + { tapToolsOHLCVTime ∷ !POSIXTime, + tapToolsOHLCVOpen ∷ !Double, + tapToolsOHLCVHigh ∷ !Double, + tapToolsOHLCVLow ∷ !Double, + tapToolsOHLCVClose ∷ !Double, + tapToolsOHLCVVolume ∷ !Double + } + deriving stock (Eq, Ord, Show, Generic) + deriving + (FromJSON, ToJSON) + via CustomJSON '[FieldLabelModifier '[StripPrefix TapToolsOHLCVPrefix, LowerFirst]] TapToolsOHLCV + +instance Swagger.ToSchema TapToolsOHLCV where + declareNamedSchema = + let open = 0.15800583264941748 + in Swagger.genericDeclareNamedSchema Swagger.defaultSchemaOptions {Swagger.fieldLabelModifier = dropSymbolAndCamelToSnake @TapToolsOHLCVPrefix} + & addSwaggerDescription "Get a specific token's trended (open, high, low, close, volume) price data." + & addSwaggerExample (toJSON $ TapToolsOHLCV {tapToolsOHLCVTime = 1_715_007_300, tapToolsOHLCVOpen = open, tapToolsOHLCVHigh = open, tapToolsOHLCVLow = open, tapToolsOHLCVClose = open, tapToolsOHLCVVolume = 120}) + +type TapToolsApiKeyHeaderName ∷ Symbol +type TapToolsApiKeyHeaderName = "x-api-key" + +type TapToolsAPI = + Header' '[Required] TapToolsApiKeyHeaderName TapToolsApiKey :> TapToolsOHLCVAPI + +type TapToolsOHLCVAPI = + "token" + :> "ohlcv" + :> QueryParam "unit" TapToolsUnit + :> QueryParam' '[Required, Strict] "interval" TapToolsInterval + :> QueryParam "numIntervals" Natural + :> Get '[JSON] [TapToolsOHLCV] + +_tapToolsOHLCV ∷ TapToolsApiKey → Maybe TapToolsUnit → TapToolsInterval → Maybe Natural → ClientM [TapToolsOHLCV] +_tapToolsOHLCV = client (Proxy @TapToolsAPI) + +tapToolsBaseUrl ∷ String +tapToolsBaseUrl = "https://openapi.taptools.io/api/v1" + +tapToolsClientEnv ∷ IO ClientEnv +tapToolsClientEnv = do + baseUrl ← parseBaseUrl tapToolsBaseUrl + manager ← newManager tlsManagerSettings + pure $ mkClientEnv manager baseUrl + +runTapToolsClient ∷ TapToolsEnv → ClientM a → IO (Either ClientError a) +runTapToolsClient (tteClientEnv → ce) c = runClientM c ce + +-- | Exceptions. +data TapToolsException + = -- | Error from the TapTools API. + TapToolsApiError !Text !ClientError + deriving stock (Eq, Show) + deriving anyclass (Exception) + +handleTapToolsError ∷ Text → Either ClientError a → IO a +handleTapToolsError locationInfo = either (throwIO . TapToolsApiError locationInfo . hideServantClientErrorHeader (fromString $ symbolVal (Proxy @TapToolsApiKeyHeaderName))) pure + +tapToolsOHLCV ∷ TapToolsEnv → Maybe TapToolsUnit → TapToolsInterval → Maybe Natural → IO [TapToolsOHLCV] +tapToolsOHLCV env@(tteApiKey → apiKey) ttu tti mttni = _tapToolsOHLCV apiKey ttu tti mttni & runTapToolsClient env >>= handleTapToolsError "tapToolsOHLCV" diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Dex/Markets.hs b/geniusyield-server-lib/src/GeniusYield/Server/Dex/Markets.hs index fc972d0e..6fee88ce 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Dex/Markets.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Dex/Markets.hs @@ -6,9 +6,10 @@ module GeniusYield.Server.Dex.Markets ( import Data.Aeson (camelTo2) import Data.Swagger qualified as Swagger import Deriving.Aeson +import GHC.TypeLits (AppendSymbol) import GeniusYield.OrderBot.Domain.Markets (HasMarkets (getMarkets), OrderAssetPair (commodityAsset, currencyAsset)) import GeniusYield.Server.Ctx -import GeniusYield.Server.Utils (addSwaggerDescription, logInfo) +import GeniusYield.Server.Utils (CommonMaestroKeyRequirementText, addSwaggerDescription, logInfo) import GeniusYield.Types import RIO hiding (logDebug, logInfo) import Servant @@ -43,7 +44,7 @@ instance Swagger.ToSchema Market where type MarketsAPI = Summary "Get markets information for the DEX." - :> Description "Returns the list of markets information supported by GeniusYield DEX." + :> Description ("Returns the list of markets information supported by GeniusYield DEX. " `AppendSymbol` CommonMaestroKeyRequirementText) :> Get '[JSON] [Market] handleMarketsApi ∷ Ctx → ServerT MarketsAPI IO diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Dex/PartialOrder.hs b/geniusyield-server-lib/src/GeniusYield/Server/Dex/PartialOrder.hs index a8e1185e..ddf9972c 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Dex/PartialOrder.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Dex/PartialOrder.hs @@ -4,6 +4,8 @@ module GeniusYield.Server.Dex.PartialOrder ( OrderInfo (..), poiToOrderInfo, PodServerException (..), + PodOrderNotFound (..), + ErrDescription (..), ) where import Data.Aeson (ToJSON (..)) @@ -15,7 +17,7 @@ import Data.Swagger.Internal.Schema qualified as Swagger import Deriving.Aeson import Fmt import GHC.TypeLits (AppendSymbol, Symbol) -import GeniusYield.Api.Dex.PartialOrder (PartialOrderInfo (..), cancelMultiplePartialOrders', fillMultiplePartialOrders', getPartialOrdersInfos, getPartialOrdersInfos', getVersionsInOrders, orderByNft, partialOrderPrice', placePartialOrder', preferentiallySelectLatestPocd, preferentiallySelectLatestVersion, roundFunctionForPOCVersion) +import GeniusYield.Api.Dex.PartialOrder (PartialOrderInfo (..), cancelMultiplePartialOrders', fillMultiplePartialOrders', getPartialOrdersInfos, getPartialOrdersInfos', getVersionsInOrders, orderByNft, partialOrderPrice', placePartialOrder'', preferentiallySelectLatestPocd, preferentiallySelectLatestVersion, roundFunctionForPOCVersion) import GeniusYield.Api.Dex.PartialOrderConfig (RefPocd (..), SomeRefPocd (SomeRefPocd), fetchPartialOrderConfig, fetchPartialOrderConfigs) import GeniusYield.HTTP.Errors import GeniusYield.OrderBot.Domain.Markets (OrderAssetPair (..)) @@ -31,6 +33,7 @@ import RIO.Map qualified as Map import RIO.NonEmpty qualified as NonEmpty import RIO.Text qualified as T import Servant +import Servant.Checked.Exceptions -- | Number of orders that we at most allow to be filled in a single transaction. maxFillOrders ∷ GYNatural @@ -44,6 +47,20 @@ data PodServerException deriving stock (Show) deriving anyclass (Exception) +-- | When order whose details is queried for is not found. +data PodOrderNotFound = PodOrderNotFound + deriving (Eq, Show, Generic) + deriving anyclass (Exception, ToJSON) + +instance ErrStatus PodOrderNotFound where + toErrStatus _ = status404 + +class ErrDescription e where + toErrDescription ∷ e → Text + +instance ErrDescription PodOrderNotFound where + toErrDescription _ = "Order not found" + instance IsGYApiError PodServerException where toApiError PodMultiFillMoreThanAllowed = GYApiError @@ -58,6 +75,14 @@ instance IsGYApiError PodServerException where gaeMsg = "Given orders are not having same payment token" } +instance IsGYApiError PodOrderNotFound where + toApiError PodOrderNotFound = + GYApiError + { gaeErrorCode = "ORDER_NOT_FOUND", + gaeHttpStatus = status404, + gaeMsg = toErrDescription PodOrderNotFound + } + type OrderInfoPrefix ∷ Symbol type OrderInfoPrefix = "oi" @@ -211,7 +236,8 @@ data PlaceOrderTransactionDetails = PlaceOrderTransactionDetails potdMakerOfferedPercentFee ∷ !GYRational, potdMakerOfferedPercentFeeAmount ∷ !GYNatural, potdLovelaceDeposit ∷ !GYNatural, - potdOrderRef ∷ !GYTxOutRef + potdOrderRef ∷ !GYTxOutRef, + potdNFTToken ∷ !GYAssetClass } deriving stock (Generic) deriving @@ -317,6 +343,9 @@ instance Swagger.ToSchema FillOrderTransactionDetails where type CommonCollateralText ∷ Symbol type CommonCollateralText = "Note that if \"collateral\" field is not provided, then framework would try to pick collateral UTxO on it's own and in that case would also be free to spend it (i.e., would be made available to coin balancer)." +type CommonSignText ∷ Symbol +type CommonSignText = "It uses the signing key from configuration to compute for wallet address. If collateral is specified in the configuration, then it would be used for." + type OrdersAPI = Summary "Build transaction to create order" :> Description ("Build a transaction to create an order. In case \"stakeAddress\" field is provided then order is placed at a mangled address having the given staking credential. " `AppendSymbol` CommonCollateralText) @@ -325,7 +354,7 @@ type OrdersAPI = :> ReqBody '[JSON] PlaceOrderParameters :> Post '[JSON] PlaceOrderTransactionDetails :<|> Summary "Create an order" - :> Description "Create an order. This endpoint would also sign & submit the built transaction." + :> Description ("Create an order. This endpoint would also sign & submit the built transaction. " `AppendSymbol` CommonSignText `AppendSymbol` " \"stakeAddress\" field from configuration, if provided, is used to place order at a mangled address.") :> ReqBody '[JSON] BotPlaceOrderParameters :> Post '[JSON] PlaceOrderTransactionDetails :<|> Summary "Build transaction to cancel order(s)" @@ -335,7 +364,7 @@ type OrdersAPI = :> ReqBody '[JSON] CancelOrderParameters :> Post '[JSON] CancelOrderTransactionDetails :<|> Summary "Cancel order(s)" - :> Description "Cancel order(s). This endpoint would also sign & submit the built transaction." + :> Description ("Cancel order(s). This endpoint would also sign & submit the built transaction. " `AppendSymbol` CommonSignText) :> ReqBody '[JSON] BotCancelOrderParameters :> Delete '[JSON] CancelOrderTransactionDetails :<|> Summary "Get order(s) details" @@ -343,6 +372,12 @@ type OrdersAPI = :> "details" :> ReqBody '[JSON] [GYAssetClass] :> Post '[JSON] [OrderInfoDetailed] + :<|> Summary "Get order details" + :> Description "Get details of an order using it's unique NFT token. Note that each order is identified uniquely by an associated NFT token which can then later be used to retrieve it's details across partial fills." + :> "details" + :> Capture "nft-token" GYAssetClass + :> Throws PodOrderNotFound + :> Get '[JSON] OrderInfoDetailed :<|> Summary "Build transaction to fill order(s)" :> Description ("Build a transaction to fill order(s). " `AppendSymbol` CommonCollateralText) :> "tx" @@ -357,6 +392,7 @@ handleOrdersApi ctx = :<|> handleCancelOrders ctx :<|> handleCancelOrdersAndSignSubmit ctx :<|> handleOrdersDetails ctx + :<|> handleOrderDetails ctx :<|> handleFillOrders ctx handlePlaceOrder ∷ Ctx → PlaceOrderParameters → IO PlaceOrderTransactionDetails @@ -370,9 +406,9 @@ handlePlaceOrder ctx@Ctx {..} pops@PlaceOrderParameters {..} = do let unitPrice = rationalFromGHC $ toInteger popPriceAmount % toInteger popOfferAmount - txBody ← - runSkeletonI ctx (NonEmpty.toList popAddresses') changeAddr popCollateral $ - placePartialOrder' + (nftAC, txBody) ← + runSkeletonF ctx (NonEmpty.toList popAddresses') changeAddr popCollateral $ + placePartialOrder'' porefs changeAddr (naturalToGHC popOfferAmount, popOfferToken) @@ -395,7 +431,8 @@ handlePlaceOrder ctx@Ctx {..} pops@PlaceOrderParameters {..} = do potdMakerOfferedPercentFee = 100 * pociMakerFeeRatio pocd, potdMakerOfferedPercentFeeAmount = roundFunctionForPOCVersion pocVersion $ toRational popOfferAmount * rationalToGHC (pociMakerFeeRatio pocd), potdLovelaceDeposit = fromIntegral $ pociMinDeposit pocd, - potdOrderRef = txOutRefFromTuple (txId, 0) + potdOrderRef = txOutRefFromTuple (txId, 0), + potdNFTToken = nftAC } resolveCtxSigningKeyInfo ∷ Ctx → IO (Strict.Pair GYSomePaymentSigningKey GYAddress) @@ -440,6 +477,15 @@ handleCancelOrdersAndSignSubmit ctx BotCancelOrderParameters {..} = do -- Though transaction id would be same, but we are returning it again, just in case... pure $ details {cotdTransactionId = txId, cotdTransaction = signedTx} +handleOrderDetails ∷ Ctx → GYAssetClass → IO (Envelope '[PodOrderNotFound] OrderInfoDetailed) +handleOrderDetails ctx@Ctx {..} ac = do + logInfo ctx $ "Getting order details for NFT token: " +|| ac ||+ "" + let porefs = dexPORefs ctxDexInfo + os ← runQuery ctx $ fmap poiToOrderInfoDetailed <$> orderByNft porefs ac + case os of + Nothing → throwIO PodOrderNotFound + Just o → pureSuccEnvelope o + handleOrdersDetails ∷ Ctx → [GYAssetClass] → IO [OrderInfoDetailed] handleOrdersDetails ctx@Ctx {..} acs = do logInfo ctx $ "Getting orders details for NFT tokens: " +|| acs ||+ "" diff --git a/geniusyield-server-lib/src/GeniusYield/Server/ErrorMiddleware.hs b/geniusyield-server-lib/src/GeniusYield/Server/ErrorMiddleware.hs index 84ea0e2e..545f7be3 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/ErrorMiddleware.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/ErrorMiddleware.hs @@ -15,7 +15,7 @@ import Data.ByteString.Builder (toLazyByteString) import GeniusYield.HTTP.Errors import GeniusYield.Imports (lazyDecodeUtf8Lenient) import GeniusYield.Providers.Common (SubmitTxException (SubmitTxException)) -import GeniusYield.Server.Dex.PartialOrder (PodServerException) +import GeniusYield.Server.Dex.PartialOrder (PodOrderNotFound, PodServerException) import GeniusYield.Transaction (BuildTxException (..)) import GeniusYield.Transaction.Common (BalancingError (..)) import GeniusYield.TxBuilder @@ -151,7 +151,8 @@ exceptionHandler = GYQueryDatumException qdErr → someBackendError $ tShow qdErr GYDatumMismatch actualDatum scriptWitness → someBackendError $ "Actual datum in UTxO is: " <> tShow actualDatum <> ", but witness has wrong corresponding datum information: " <> tShow scriptWitness GYApplicationException e → toApiError e, - WH $ \(e ∷ PodServerException) → toApiError e + WH $ \(e ∷ PodServerException) → toApiError e, + WH $ \(e ∷ PodOrderNotFound) → toApiError e ] sinkStreamingBody ∷ ((Wai.StreamingBody → IO ()) → IO ()) → IO LBS.ByteString diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Orphans.hs b/geniusyield-server-lib/src/GeniusYield/Server/Orphans.hs new file mode 100644 index 00000000..ea978c81 --- /dev/null +++ b/geniusyield-server-lib/src/GeniusYield/Server/Orphans.hs @@ -0,0 +1,70 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module GeniusYield.Server.Orphans () where + +import Control.Lens (at, (?~)) +import Data.HashMap.Strict.InsOrd qualified as IOHM +import Data.Swagger +import GeniusYield.Server.Auth (APIKeyAuthProtect, apiKeyHeaderText) +import GeniusYield.Server.Dex.PartialOrder (ErrDescription (..)) +import RIO +import Servant +import Servant.Checked.Exceptions +import Servant.Foreign +import Servant.Swagger + +type IsErr err = (ErrDescription err, ErrStatus err) + +instance (IsErr err, HasSwagger sub) ⇒ HasSwagger (Throws err :> sub) where + toSwagger _ = + toSwagger (Proxy ∷ Proxy sub) + & setResponseWith + (\old _ → addDescription old) + (fromEnum $ toErrStatus (undefined ∷ err)) + (return $ mempty & description .~ errDescription) + where + addDescription = description %~ ((errDescription <> " OR ") <>) + errDescription = toErrDescription (undefined ∷ err) + +instance HasSwagger api ⇒ HasSwagger (APIKeyAuthProtect :> api) where + toSwagger _ = + toSwagger (Proxy ∷ Proxy api) + & securityDefinitions + .~ SecurityDefinitions (IOHM.fromList [(apiKeyHeaderText, apiKeySecurityScheme)]) + & allOperations + . security + .~ [SecurityRequirement (IOHM.singleton apiKeyHeaderText [])] + & allOperations + . responses + %~ addCommonResponses + where + apiKeySecurityScheme ∷ SecurityScheme + apiKeySecurityScheme = + SecurityScheme + { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams apiKeyHeaderText ApiKeyHeader), + _securitySchemeDescription = Just "API key for accessing the server's API." + } + addCommonResponses ∷ Responses → Responses + addCommonResponses resps = resps & at 401 ?~ Inline response401 & at 403 ?~ Inline response403 & at 500 ?~ Inline response500 + + response401 ∷ Response + response401 = mempty & description .~ "Unauthorized access - API key missing" + + response403 ∷ Response + response403 = mempty & description .~ "Forbidden - The API key does not have permission to perform the request" + + response500 ∷ Response + response500 = mempty & description .~ "Internal server error" + +-- `HasForeign` instance for `APIKeyAuthProtect :> api` is required to generate client code using libraries such as `servant-py`. +-- This is written with help from https://github.com/haskell-servant/servant-auth/issues/8#issue-185541839. +instance ∀ lang ftype api. (HasForeign lang ftype api, HasForeignType lang ftype Text) ⇒ HasForeign lang ftype (APIKeyAuthProtect :> api) where + type Foreign ftype (APIKeyAuthProtect :> api) = Foreign ftype api + foreignFor lang Proxy Proxy subR = foreignFor lang Proxy (Proxy ∷ Proxy api) subR' + where + subR' = subR {_reqHeaders = HeaderArg arg : _reqHeaders subR} + arg = + Arg + { _argName = "api-key", + _argType = typeFor lang (Proxy ∷ Proxy ftype) (Proxy ∷ Proxy Text) + } diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Run.hs b/geniusyield-server-lib/src/GeniusYield/Server/Run.hs index 99712005..4b0df25b 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Run.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Run.hs @@ -18,6 +18,11 @@ import GeniusYield.Server.Auth import GeniusYield.Server.Config (ServerConfig (..), coreConfigFromServerConfig, optionalSigningKeyFromServerConfig, serverConfigOptionalFPIO) import GeniusYield.Server.Constants (gitHash) import GeniusYield.Server.Ctx +-- import RIO.ByteString.Lazy qualified as BL + +-- import Servant.PY (requests, writePythonForAPI) + +import GeniusYield.Server.Dex.HistoricalPrices.TapTools.Client (tapToolsClientEnv) import GeniusYield.Server.ErrorMiddleware import GeniusYield.Server.RequestLoggerMiddleware (gcpReqLogger) import GeniusYield.Server.Utils @@ -27,10 +32,8 @@ import Network.Wai.Handler.Warp qualified as Warp import PackageInfo_geniusyield_server_lib qualified as PackageInfo import RIO hiding (Handler, logDebug, logErrorS, logInfo, logInfoS, onException) import RIO.ByteString qualified as B --- import RIO.ByteString.Lazy qualified as BL import RIO.Text.Lazy qualified as LT import Servant --- import Servant.PY (requests, writePythonForAPI) import Servant.Server.Experimental.Auth (AuthHandler) import Servant.Server.Internal.ServerError (responseServerError) import System.TimeManager (TimeoutThread (..)) @@ -39,6 +42,12 @@ runServer ∷ Maybe FilePath → IO () runServer mfp = do serverConfig ← serverConfigOptionalFPIO mfp menv ← networkIdToMaestroEnv (case scMaestroToken serverConfig of Confidential t → t) (scNetworkId serverConfig) + mtenv ← + case scTapToolsApiKey serverConfig of + Nothing → pure Nothing + Just (Confidential apiKey) → do + tce ← tapToolsClientEnv + pure $ Just $ TapToolsEnv {tteClientEnv = tce, tteApiKey = apiKey} optionalSigningKey ← optionalSigningKeyFromServerConfig serverConfig let nid = scNetworkId serverConfig coreCfg = coreConfigFromServerConfig serverConfig @@ -82,6 +91,7 @@ runServer mfp = do | nid == GYTestnetPreprod → dexInfoDefaultPreprod | otherwise → error "Only mainnet & preprod network are supported", ctxMaestroProvider = MaestroProvider menv, + ctxTapToolsProvider = mtenv, ctxSigningKey = optionalSigningKey, ctxCollateral = scCollateral serverConfig, ctxStakeAddress = scStakeAddress serverConfig diff --git a/geniusyield-server-lib/src/GeniusYield/Server/Utils.hs b/geniusyield-server-lib/src/GeniusYield/Server/Utils.hs index 7b25693e..f70f9351 100644 --- a/geniusyield-server-lib/src/GeniusYield/Server/Utils.hs +++ b/geniusyield-server-lib/src/GeniusYield/Server/Utils.hs @@ -9,14 +9,25 @@ module GeniusYield.Server.Utils ( addSwaggerDescription, addSwaggerExample, bytestringToString, + hideServantClientErrorHeader, + commonEnumParamSchemaRecipe, + CommonMaestroKeyRequirementText, ) where +import Control.Lens ((?~)) +import Data.Swagger qualified as Swagger +import Data.Swagger.Internal qualified as Swagger +import GHC.TypeLits (Symbol) import GeniusYield.Imports import GeniusYield.Server.Ctx import GeniusYield.Swagger.Utils (addSwaggerDescription, addSwaggerExample, dropSymbolAndCamelToSnake) import GeniusYield.Types +import Network.HTTP.Client qualified as Http +import Network.HTTP.Types qualified as Http import RIO hiding (logDebug, logInfo) import RIO.Text qualified as Text +import Servant.Client qualified as Servant +import Servant.Client.Core qualified as Servant logDebug ∷ HasCallStack ⇒ Ctx → String → IO () logDebug ctx = gyLogDebug (ctxProviders ctx) mempty @@ -40,3 +51,21 @@ isMatchedException (etype :>> etypes) se = isJust (f etype) || isMatchedExceptio bytestringToString ∷ ByteString → String bytestringToString = RIO.decodeUtf8Lenient >>> Text.unpack + +hideServantClientErrorHeader ∷ Http.HeaderName → Servant.ClientError → Servant.ClientError +hideServantClientErrorHeader headerName clientError = case clientError of + Servant.FailureResponse reqF res → Servant.FailureResponse reqF {Servant.requestHeaders = renameHeader <$> Servant.requestHeaders reqF} res + Servant.ConnectionError se → case fromException @Http.HttpException se of + Just he → case he of + Http.HttpExceptionRequest req content → Servant.ConnectionError $ SomeException $ Http.HttpExceptionRequest req {Http.requestHeaders = renameHeader <$> Http.requestHeaders req} content + _anyOther → clientError + Nothing → clientError + _anyOther → clientError + where + renameHeader (h, v) = if h == headerName then (h, "hidden") else (h, v) + +commonEnumParamSchemaRecipe ∷ ∀ a (t ∷ Swagger.SwaggerKind Type). (Bounded a, Enum a, ToJSON a) ⇒ Proxy a → Swagger.ParamSchema t +commonEnumParamSchemaRecipe _ = mempty & Swagger.type_ ?~ Swagger.SwaggerString & Swagger.enum_ ?~ fmap toJSON [(minBound ∷ a) .. maxBound] + +type CommonMaestroKeyRequirementText ∷ Symbol +type CommonMaestroKeyRequirementText = "\"maestroToken\" field in the configuration is required for this operation." diff --git a/web/swagger/api.yaml b/web/swagger/api.yaml index 39bfb11b..ef6df7bd 100644 --- a/web/swagger/api.yaml +++ b/web/swagger/api.yaml @@ -457,6 +457,8 @@ definitions: type: string maker_offered_percent_fee_amount: $ref: '#/definitions/GYNatural' + nft_token: + $ref: '#/definitions/GYAssetClass' order_ref: $ref: '#/definitions/GYTxOutRef' transaction: @@ -474,6 +476,7 @@ definitions: - maker_offered_percent_fee_amount - lovelace_deposit - order_ref + - nft_token type: object Settings: description: Genius Yield Server settings. @@ -498,6 +501,43 @@ definitions: - revision - backend type: object + TapToolsOHLCV: + description: Get a specific token's trended (open, high, low, close, volume) price + data. + example: + close: 0.15800583264941748 + high: 0.15800583264941748 + low: 0.15800583264941748 + open: 0.15800583264941748 + time: 1715007300 + volume: 120 + properties: + close: + format: double + type: number + high: + format: double + type: number + low: + format: double + type: number + open: + format: double + type: number + time: + multipleOf: 1.0e-12 + type: number + volume: + format: double + type: number + required: + - time + - open + - high + - low + - close + - volume + type: object TradingFees: description: Trading fees of DEX. properties: @@ -533,7 +573,8 @@ info: paths: /v0/assets/{asset}: get: - description: Get information for a specific asset. + description: Get information for a specific asset. "maestroToken" field in the + configuration is required for this operation. parameters: - in: path name: asset @@ -593,6 +634,7 @@ paths: /v0/historical-prices/maestro/{market-id}/{dex}: get: description: This endpoint internally calls Maestro's "DEX And Pair OHLC" endpoint. + "maestroToken" field in the configuration is required for this operation. parameters: - in: path name: market-id @@ -666,10 +708,67 @@ paths: summary: Get price history using Maestro. tags: - Historical Prices + /v0/historical-prices/tap-tools/{asset}: + get: + description: This endpoint internally calls TapTools's "Token price OHLCV" endpoint. + Note that only the liquidity pools involving ADA and the given asset class + is considered to get for aggregated price information. Price returned is in + ADA. + parameters: + - in: path + name: asset + required: true + type: string + - enum: + - 3m + - 5m + - 15m + - 30m + - 1h + - 2h + - 4h + - 12h + - 1d + - 3d + - 1w + - 1M + in: query + name: interval + required: true + type: string + - exclusiveMinimum: false + in: query + minimum: 0 + name: numIntervals + required: false + type: integer + produces: + - application/json;charset=utf-8 + responses: + '200': + description: '' + schema: + items: + $ref: '#/definitions/TapToolsOHLCV' + type: array + '400': + description: Invalid `numIntervals` or `interval` or `asset` + '401': + description: Unauthorized access - API key missing + '403': + description: Forbidden - The API key does not have permission to perform + the request + '500': + description: Internal server error + security: + - api-key: [] + summary: Get price history using TapTools. + tags: + - Historical Prices /v0/markets: get: description: Returns the list of markets information supported by GeniusYield - DEX. + DEX. "maestroToken" field in the configuration is required for this operation. produces: - application/json;charset=utf-8 responses: @@ -730,7 +829,9 @@ paths: consumes: - application/json;charset=utf-8 description: Cancel order(s). This endpoint would also sign & submit the built - transaction. + transaction. It uses the signing key from configuration to compute for wallet + address. If collateral is specified in the configuration, then it would be + used for. parameters: - in: body name: body @@ -762,7 +863,10 @@ paths: consumes: - application/json;charset=utf-8 description: Create an order. This endpoint would also sign & submit the built - transaction. + transaction. It uses the signing key from configuration to compute for wallet + address. If collateral is specified in the configuration, then it would be + used for. "stakeAddress" field from configuration, if provided, is used to + place order at a mangled address. parameters: - in: body name: body @@ -828,6 +932,39 @@ paths: summary: Get order(s) details tags: - Orders + /v0/orders/details/{nft-token}: + get: + description: Get details of an order using it's unique NFT token. Note that + each order is identified uniquely by an associated NFT token which can then + later be used to retrieve it's details across partial fills. + parameters: + - in: path + name: nft-token + required: true + type: string + produces: + - application/json;charset=utf-8 + responses: + '200': + description: '' + schema: + $ref: '#/definitions/OrderInfoDetailed' + '400': + description: Invalid `nft-token` + '401': + description: Unauthorized access - API key missing + '403': + description: Forbidden - The API key does not have permission to perform + the request + '404': + description: Order not found + '500': + description: Internal server error + security: + - api-key: [] + summary: Get order details + tags: + - Orders /v0/orders/tx/build-cancel: post: consumes: