Skip to content

Commit a841af3

Browse files
committed
feat: fetch ada price from coin-gecko (PLT-4295)
1 parent fe0908e commit a841af3

File tree

8 files changed

+119
-16
lines changed

8 files changed

+119
-16
lines changed

nix/materialized/aarch64-darwin/.plan.nix/plutus-certification.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"Plutus/Certification/TransactionBroadcaster"
8282
"Plutus/Certification/Server/Internal"
8383
"Plutus/Certification/Server/Instance"
84+
"Plutus/Certification/CoinGeckoClient"
8485
"Plutus/Certification/API"
8586
"Plutus/Certification/Cache"
8687
"Plutus/Certification/Cicero"

nix/materialized/x86_64-darwin/.plan.nix/plutus-certification.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"Plutus/Certification/TransactionBroadcaster"
8282
"Plutus/Certification/Server/Internal"
8383
"Plutus/Certification/Server/Instance"
84+
"Plutus/Certification/CoinGeckoClient"
8485
"Plutus/Certification/API"
8586
"Plutus/Certification/Cache"
8687
"Plutus/Certification/Cicero"

nix/materialized/x86_64-linux/.plan.nix/plutus-certification.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"Plutus/Certification/TransactionBroadcaster"
8282
"Plutus/Certification/Server/Internal"
8383
"Plutus/Certification/Server/Instance"
84+
"Plutus/Certification/CoinGeckoClient"
8485
"Plutus/Certification/API"
8586
"Plutus/Certification/Cache"
8687
"Plutus/Certification/Cicero"

plutus-certification.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ library
7878
Plutus.Certification.TransactionBroadcaster
7979
Plutus.Certification.Server.Internal
8080
Plutus.Certification.Server.Instance
81+
Plutus.Certification.CoinGeckoClient
8182
autogen-modules: Paths_plutus_certification
8283
default-language: Haskell2010
8384

server/Main.hs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import Observe.Event.Render.IO.JSON
3939
import Observe.Event.Wai hiding (OnException)
4040
import Observe.Event.Servant.Client
4141
import System.IO
42+
import Data.IORef
4243
import Control.Concurrent.MVar
4344
import Control.Concurrent.Async
4445
import Network.Wai
@@ -81,7 +82,6 @@ data Args = Args
8182
, useWhitelist :: !Bool
8283
, github :: !GitHubArgs
8384
, bypassSubscriptionValidation :: !Bool
84-
, adaUsdPrice :: !DB.AdaUsdPrice
8585
}
8686

8787
data GitHubArgs = GitHubArgs
@@ -149,11 +149,6 @@ argsParser = Args
149149
( long "unsafe-bypass-subscription-validation"
150150
<> help "Bypass subscription validation"
151151
)
152-
<*> option auto
153-
( long "ada-usd-price"
154-
<> metavar "ADA_USD_PRICE"
155-
<> help "price of ADA in USD"
156-
)
157152

158153
data AuthMode = JWTAuth JWTArgs | PlainAddressAuth
159154

@@ -399,15 +394,20 @@ main = do
399394
-- if useWhitelist is set to false the whitelist is ignored
400395
whitelist <- if not args.useWhitelist then pure Nothing else Just <$> whitelisted
401396
_ <- initDb
402-
_ <- forkIO $ startTransactionsMonitor (narrowEventBackend InjectSynchronizer eb) (args.wallet) 10
397+
adaPriceRef <- startSynchronizer eb args
403398
-- TODO: this has to be refactored
404399
runSettings settings . application (narrowEventBackend InjectServeRequest eb) $
405400
cors (const $ Just corsPolicy) .
406401
serveWithContext (Proxy @APIWithSwagger) (genAuthServerContext whitelist args.auth) .
407-
(\r -> swaggerSchemaUIServer (documentation args.auth) :<|> server (serverArgs args caps r eb whitelist))
402+
(\r -> swaggerSchemaUIServer (documentation args.auth)
403+
:<|> server (serverArgs args caps r eb whitelist adaPriceRef))
408404
exitFailure
409405
where
410-
serverArgs args caps r eb whitelist = ServerArgs
406+
startSynchronizer eb args = do
407+
ref <- newIORef Nothing
408+
_ <- forkIO $ startTransactionsMonitor (narrowEventBackend InjectSynchronizer eb) (args.wallet) ref 10
409+
pure ref
410+
serverArgs args caps r eb whitelist ref = ServerArgs
411411
{ serverCaps = caps
412412
, serverWalletArgs = args.wallet
413413
, githubToken = args.github.accessToken
@@ -416,8 +416,8 @@ main = do
416416
, serverSigningTimeout = args.signatureTimeout
417417
, serverWhitelist = whitelist :: Maybe Whitelist
418418
, validateSubscriptions = not args.bypassSubscriptionValidation
419-
, adaUsdPrice = args.adaUsdPrice
420419
, serverGitHubCredentials = args.github.credentials
420+
, adaUsdPrice = liftIO $ readIORef ref
421421
}
422422
jwtArgs PlainAddressAuth = Nothing
423423
jwtArgs (JWTAuth args) = Just args
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE LambdaCase #-}
3+
{-# LANGUAGE BlockArguments #-}
4+
{-# LANGUAGE TypeOperators #-}
5+
{-# LANGUAGE OverloadedStrings #-}
6+
{-# LANGUAGE GADTs #-}
7+
8+
module Plutus.Certification.CoinGeckoClient where
9+
10+
import Servant.API
11+
import Servant.Client
12+
import Data.Functor
13+
import Data.Fixed
14+
import Data.Aeson
15+
16+
import Observe.Event.Render.JSON
17+
import Observe.Event.Backend
18+
import Control.Monad.IO.Class ( MonadIO(..) )
19+
import Control.Monad.Catch (MonadMask)
20+
import Observe.Event
21+
import Network.HTTP.Client hiding (Proxy)
22+
23+
import Network.HTTP.Client.TLS
24+
import Data.Proxy
25+
26+
newtype CoinGeckoResponse = CoinGeckoResponse { usdPrice :: Micro } deriving (Show)
27+
28+
type API = "coins"
29+
:> Capture "id" String
30+
:> QueryParam "tickers" Bool
31+
:> QueryParam "market_data" Bool
32+
:> QueryParam "community_data" Bool
33+
:> QueryParam "developer_data" Bool
34+
:> QueryParam "sparkline" Bool
35+
:> Get '[JSON] CoinGeckoResponse
36+
37+
-- this is how we access the usd price => .market_data.current_price.usd
38+
instance FromJSON CoinGeckoResponse where
39+
parseJSON = withObject "CoinGeckoResponse" \o ->
40+
o .: "market_data" >>=
41+
(.: "current_price") >>=
42+
(.: "usd") <&>
43+
CoinGeckoResponse
44+
45+
data CoinGeckoClientSelector f where
46+
FetchAdaPrice :: CoinGeckoClientSelector (Either ClientError Micro)
47+
48+
renderCoinGeckoClientSelector :: RenderSelectorJSON CoinGeckoClientSelector
49+
renderCoinGeckoClientSelector FetchAdaPrice = ("coin-gecko-fetch-price",\case
50+
Left err -> ("http-error",toJSON (show err))
51+
Right price -> ("ada-usd-price",toJSON price)
52+
)
53+
54+
getAdaPrice :: (MonadIO m,MonadMask m)
55+
=> EventBackend m r CoinGeckoClientSelector
56+
-> m (Either ClientError Micro)
57+
getAdaPrice eb = withEvent eb FetchAdaPrice $ \ev -> do
58+
manager' <- liftIO $ newManager tlsManagerSettings
59+
let clientEnv = mkClientEnv manager'
60+
(BaseUrl Https "api.coingecko.com" 443 "api/v3")
61+
let clientM = client (Proxy :: Proxy API)
62+
"cardano"
63+
(Just False) -- tickers
64+
(Just True) -- market_data
65+
(Just False) -- community_data
66+
(Just False) -- developer_data
67+
(Just False) -- sparkline
68+
69+
resp <- liftIO (runClientM clientM clientEnv) -- extract the usd price
70+
<&> fmap usdPrice
71+
addField ev resp
72+
pure resp

src/Plutus/Certification/Server/Instance.hs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ data ServerArgs m r = ServerArgs
7878
, serverWhitelist :: !(Maybe Whitelist)
7979
, serverGitHubCredentials :: !(Maybe GitHubCredentials)
8080
, validateSubscriptions :: Bool
81-
, adaUsdPrice :: DB.AdaUsdPrice
81+
, adaUsdPrice :: m (Maybe DB.AdaUsdPrice)
8282
}
8383

8484
type Whitelist = HashSet Text
@@ -108,7 +108,7 @@ server ServerArgs{..} = NamedAPI
108108
, versionHead = withEvent eb Version . const $ pure NoContent
109109
, walletAddress = withEvent eb WalletAddress . const $ pure serverWalletArgs.walletAddress
110110
, createRun = \(profileId,_) commitOrBranch -> withEvent eb CreateRun \ev -> do
111-
-- ensure the profile has an active feauture for L1Run
111+
-- ensure the profile has an active feature for L1Run
112112
validateFeature L1Run profileId
113113
(fref,profileAccessToken) <- getFlakeRefAndAccessToken profileId commitOrBranch
114114
let githubToken' = profileAccessToken <|> githubToken
@@ -276,7 +276,8 @@ server ServerArgs{..} = NamedAPI
276276
addField ev $ SubscribeFieldProfileId profileId
277277
addField ev $ SubscribeFieldTierId tierId
278278
now <- getNow
279-
ret <- DB.withDb (DB.createSubscription now profileId tierId adaUsdPrice)
279+
adaUsdPrice' <- getAdaUsdPrice'
280+
ret <- DB.withDb (DB.createSubscription now profileId tierId adaUsdPrice')
280281
forM_ ret $ \dto -> addField ev $ SubscribeFieldSubscriptionId (dto.subscriptionDtoId)
281282
maybeToServerError err404 "Tier not found" ret
282283

@@ -295,10 +296,13 @@ server ServerArgs{..} = NamedAPI
295296
addField ev $ GetActiveFeaturesFieldFeatures featureTypes
296297
pure featureTypes
297298
, getAdaUsdPrice = withEvent eb GetAdaUsdPrice \ev -> do
298-
addField ev adaUsdPrice
299-
pure adaUsdPrice
299+
adaUsdPrice' <- getAdaUsdPrice'
300+
addField ev adaUsdPrice'
301+
pure adaUsdPrice'
300302
}
301303
where
304+
getAdaUsdPrice' =
305+
adaUsdPrice >>= maybeToServerError err500 "Can't get ada usd price"
302306
validateFeature featureType profileId = do
303307
-- ensure the profile has an active feauture for L1Run
304308
when validateSubscriptions $ do

src/Plutus/Certification/Synchronizer.hs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import Control.Monad.IO.Class ( MonadIO(..) )
2424
import Control.Monad.Catch (MonadMask)
2525
import Data.List (groupBy)
2626
import Plutus.Certification.API.Routes (RunIDV1(..))
27+
import Plutus.Certification.CoinGeckoClient
2728
import Data.Aeson
2829
import Plutus.Certification.TransactionBroadcaster
2930
import Observe.Event.Render.JSON
@@ -37,15 +38,20 @@ import Control.Monad.Except (MonadError)
3738
import Data.Maybe (fromMaybe)
3839
import Observe.Event.Backend
3940
import Observe.Event
41+
import Data.IORef
42+
import Data.Void
43+
4044
data InitializingField
4145
= WalletArgsField WalletArgs
4246
| DelayField Int
4347

4448
data SynchronizerSelector f where
4549
InitializingSynchronizer :: SynchronizerSelector InitializingField
4650
InjectTxBroadcaster :: forall f . !(TxBroadcasterSelector f) -> SynchronizerSelector f
51+
InjectCoinGeckoClient :: forall f . !(CoinGeckoClientSelector f) -> SynchronizerSelector f
4752
MonitorTransactions :: SynchronizerSelector TransactionsCount
4853
ActivateSubscriptions :: SynchronizerSelector [DB.SubscriptionId]
54+
UpdateAdaPrice :: SynchronizerSelector Void
4955

5056
newtype TransactionsCount = TransactionsCount Int
5157

@@ -62,8 +68,10 @@ renderSynchronizerSelector InitializingSynchronizer =
6268
DelayField delay -> ("delay", toJSON delay)
6369
)
6470
renderSynchronizerSelector (InjectTxBroadcaster selector) = renderTxBroadcasterSelector selector
71+
renderSynchronizerSelector (InjectCoinGeckoClient selector) = renderCoinGeckoClientSelector selector
6572
renderSynchronizerSelector MonitorTransactions = ("monitor-transactions", renderTransactionsCount)
6673
renderSynchronizerSelector ActivateSubscriptions = ("activate-subscriptions", renderSubscriptions)
74+
renderSynchronizerSelector UpdateAdaPrice = ("refresh-ada-price", absurd)
6775

6876
renderSubscriptions :: RenderFieldJSON [DB.SubscriptionId]
6977
renderSubscriptions subscriptions = ("subscriptions", toJSON subscriptions)
@@ -233,16 +241,31 @@ certifyProfileRuns certificationProcess runs =
233241
startTransactionsMonitor :: (MonadIO m,MonadMask m,MonadError IOException m)
234242
=> EventBackend m r SynchronizerSelector
235243
-> WalletArgs
244+
-> IORef (Maybe DB.AdaUsdPrice)
236245
-> Int
237246
-> m b
238-
startTransactionsMonitor eb args delayInSeconds =
247+
startTransactionsMonitor eb args adaPriceRef delayInSeconds =
239248
withEvent eb InitializingSynchronizer $ \ev -> do
240249
addField ev $ WalletArgsField args
241250
addField ev $ DelayField delayInSeconds
242251
-- TODO maybe a forkIO here will be better than into the calling function
243252
-- hence, now, the parent instrumentation event will never terminate
244253
forever $ do
254+
updateAdaPrice (subEventBackend ev) adaPriceRef
245255
monitorWalletTransactions (subEventBackend ev) args
246256
liftIO $ threadDelay delayInMicroseconds
247257
where
248258
delayInMicroseconds = delayInSeconds * 1000000
259+
260+
updateAdaPrice :: (MonadIO m,MonadMask m)
261+
=> EventBackend m r SynchronizerSelector
262+
-> IORef (Maybe DB.AdaUsdPrice)
263+
-> m ()
264+
updateAdaPrice eb ref = withEvent eb UpdateAdaPrice $ \_ -> do
265+
-- fetch the ada price from the wallet
266+
adaPrice <- getAdaPrice ( narrowEventBackend InjectCoinGeckoClient eb )
267+
liftIO $ writeIORef ref $
268+
case adaPrice of
269+
Left _ -> Nothing
270+
Right p -> Just p
271+

0 commit comments

Comments
 (0)