11module Seabug.Metadata
2- ( FullSeabugMetadata
2+ ( BlockfrostFetchError (..)
3+ , FullSeabugMetadata
34 , Hash
45 , getFullSeabugMetadata
6+ , getFullSeabugMetadataWithBackoff
57 ) where
68
79import Contract.Prelude
810
911import Aeson as Aeson
12+ import Affjax (printError )
1013import Affjax as Affjax
1114import Affjax.RequestHeader as Affjax.RequestHeader
1215import Affjax.ResponseFormat as Affjax.ResponseFormat
1316import Cardano.Types.Value as Cardano.Types.Value
14- import Contract.Monad (Contract )
1517import Contract.Prim.ByteArray (byteArrayToHex )
16- import Contract.Transaction
17- ( ClientError (ClientHttpError, ClientDecodeJsonError)
18- )
1918import Contract.Value
2019 ( CurrencySymbol
2120 , TokenName
@@ -24,42 +23,83 @@ import Contract.Value
2423 , mkCurrencySymbol
2524 )
2625import Control.Alternative (guard )
26+ import Control.Monad.Error.Class (throwError )
2727import Control.Monad.Except.Trans (ExceptT (ExceptT), except , runExceptT )
28+ import Control.Monad.Reader (ReaderT , runReaderT )
2829import Control.Monad.Reader.Trans (asks )
2930import Control.Monad.Trans.Class (lift )
3031import Data.Argonaut as Argonaut
31- import Data.Bifunctor (bimap , lmap )
32+ import Data.Bifunctor (lmap )
3233import Data.Function (on )
3334import Data.HTTP.Method (Method (GET))
3435import Data.Newtype (unwrap )
36+ import Effect.Aff (delay )
37+ import Effect.Random (randomRange )
3538import Seabug.Metadata.Types (SeabugMetadata (SeabugMetadata))
3639import Partial.Unsafe (unsafePartial )
3740
38- import Debug (traceM )
39-
4041type Hash = String
4142
4243type FullSeabugMetadata =
4344 { seabugMetadata :: SeabugMetadata
4445 , ipfsHash :: Hash
4546 }
4647
48+ data BlockfrostFetchError
49+ = BlockfrostRateLimit
50+ | BlockfrostOtherError String
51+
52+ derive instance Generic BlockfrostFetchError _
53+
54+ instance Show BlockfrostFetchError where
55+ show = genericShow
56+
57+ type BlockfrostFetch a = ExceptT BlockfrostFetchError
58+ (ReaderT { projectId :: String } Aff )
59+ a
60+
61+ -- | Tries to get the metadata for the given asset using
62+ -- | Blockfrost. If the rate limit is hit, retries after a random
63+ -- | delay, up to 5 times. Uses a very simple back-off mechanism.
64+ -- | Instead of relying on this, refactor so the rate limit isn't hit.
65+ getFullSeabugMetadataWithBackoff
66+ :: CurrencySymbol /\ TokenName
67+ -> String
68+ -> Aff (Either BlockfrostFetchError FullSeabugMetadata )
69+ getFullSeabugMetadataWithBackoff asset projectId = go 1.0
70+ where
71+ go n = do
72+ r <- getFullSeabugMetadata asset projectId
73+ case r of
74+ Left BlockfrostRateLimit
75+ | n < 5.0 -> do
76+ let n' = n + 1.0
77+ log " Blockfrost rate limit hit, backing off"
78+ -- Wait a random amount of time in the range of [1, 3 *
79+ -- (attempt + 1)) seconds, this is just a heuristic based
80+ -- on my testing
81+ delay <<< wrap <<< (_ * 1000.0 ) =<<
82+ (liftEffect $ randomRange 1.0 (3.0 * n'))
83+ log $ " Retrying, attempt " <> show n'
84+ go n'
85+ _ -> pure r
86+
4787getFullSeabugMetadata
48- :: forall (r :: Row Type )
49- . CurrencySymbol /\ TokenName
50- -> Contract (projectId :: String | r ) (Either ClientError FullSeabugMetadata )
51- getFullSeabugMetadata a@(currSym /\ _) = runExceptT $ do
52- seabugMetadata <- getMintingTxSeabugMetadata currSym =<< getMintingTxHash a
53- log $ show seabugMetadata
54- ipfsHash <- getIpfsHash seabugMetadata
55- pure { seabugMetadata, ipfsHash }
88+ :: CurrencySymbol /\ TokenName
89+ -> String
90+ -> Aff (Either BlockfrostFetchError FullSeabugMetadata )
91+ getFullSeabugMetadata a@(currSym /\ _) projectId =
92+ flip runReaderT { projectId } <<< runExceptT $ do
93+ seabugMetadata <-
94+ getMintingTxSeabugMetadata currSym =<< getMintingTxHash a
95+ ipfsHash <- getIpfsHash seabugMetadata
96+ pure { seabugMetadata, ipfsHash }
5697
5798getIpfsHash
58- :: forall (r :: Row Type )
59- . SeabugMetadata
60- -> ExceptT ClientError (Contract (projectId :: String | r )) Hash
99+ :: SeabugMetadata
100+ -> BlockfrostFetch Hash
61101getIpfsHash (SeabugMetadata { collectionNftCS, collectionNftTN }) = do
62- except <<< (decodeField " image" <=< decodeFieldJson " onchain_metadata" )
102+ except <<< (decodeField " image" <=< decodeField " onchain_metadata" )
63103 =<< mkGetRequest (" assets/" <> mkAsset curr collectionNftTN)
64104 where
65105 curr :: CurrencySymbol
@@ -70,17 +110,17 @@ getMintingTxSeabugMetadata
70110 :: forall (r :: Row Type )
71111 . CurrencySymbol
72112 -> Hash
73- -> ExceptT ClientError ( Contract ( projectId :: String | r )) SeabugMetadata
113+ -> BlockfrostFetch SeabugMetadata
74114getMintingTxSeabugMetadata currSym txHash = do
75115 res <- mkGetRequest $ " txs/" <> txHash <> " /metadata"
76116 ms <- except
77- $ lmap ClientDecodeJsonError
117+ $ lmap ( BlockfrostOtherError <<< show)
78118 $ Aeson .caseAesonArray
79119 (Left (Argonaut.TypeMismatch " Expected array of objects" ))
80120 Right
81- ( Aeson .jsonToAeson res)
121+ res
82122 except
83- $ note (ClientDecodeJsonError ( Argonaut.UnexpectedValue res))
123+ $ note (BlockfrostOtherError ( " Unexpected JSON: " <> show res))
84124 $ findSeabugMetadata ms
85125 where
86126 findSeabugMetadata :: Array Aeson.Aeson -> Maybe SeabugMetadata
@@ -97,9 +137,9 @@ getMintingTxSeabugMetadata currSym txHash = do
97137getMintingTxHash
98138 :: forall (r :: Row Type )
99139 . CurrencySymbol /\ TokenName
100- -> ExceptT ClientError ( Contract ( projectId :: String | r )) Hash
140+ -> BlockfrostFetch Hash
101141getMintingTxHash a =
102- except <<< decodeFieldJson " initial_mint_tx_hash"
142+ except <<< decodeField " initial_mint_tx_hash"
103143 =<< mkGetRequest (" assets/" <> uncurry mkAsset a)
104144
105145mkAsset :: CurrencySymbol -> TokenName -> String
@@ -112,41 +152,41 @@ decodeField
112152 . Aeson.DecodeAeson a
113153 => String
114154 -> Aeson.Aeson
115- -> Either ClientError a
155+ -> Either BlockfrostFetchError a
116156decodeField field = do
117- traceM $ show field
118- lmap ClientDecodeJsonError <<<
157+ lmap (BlockfrostOtherError <<< show) <<<
119158 ( Aeson .decodeAeson
120159 <=< Aeson .caseAesonObject
121160 (Left (Argonaut.TypeMismatch " Expected Object" ))
122161 (flip Aeson .getField field)
123162 )
124163
125- decodeFieldJson
126- :: forall (a :: Type )
127- . Aeson.DecodeAeson a
128- => String
129- -> Argonaut.Json
130- -> Either ClientError a
131- decodeFieldJson field = decodeField field <<< Aeson .jsonToAeson
132-
133164mkGetRequest
134165 :: forall (r :: Row Type )
135166 . String
136- -> ExceptT ClientError ( Contract ( projectId :: String | r )) Argonaut.Json
167+ -> BlockfrostFetch Aeson.Aeson
137168mkGetRequest path = do
138- projectId <- lift $ asks $ _.projectId <<< unwrap
169+ projectId <- lift $ asks $ _.projectId
139170 let
140- req :: Affjax.Request Argonaut.Json
171+ req :: Affjax.Request String
141172 req = Affjax .defaultRequest
142173 { url = mkUrl
143- , responseFormat = Affjax.ResponseFormat .json
174+ , responseFormat = Affjax.ResponseFormat .string
144175 , method = Left GET
145176 , headers =
146177 [ Affjax.RequestHeader.RequestHeader " project_id" projectId
147178 ]
148179 }
149- ExceptT $ liftAff $ Affjax .request req <#> bimap ClientHttpError _.body
180+ res <- ExceptT $ liftAff $ do
181+ r <- Affjax .request req <#> lmap (BlockfrostOtherError <<< printError)
182+ case r of
183+ Left e -> log $ show e
184+ Right _ -> pure unit
185+ pure r
186+ when (unwrap res.status == 429 ) $ throwError $ BlockfrostRateLimit
187+ except $
188+ lmap (BlockfrostOtherError <<< ((" Error parsing JSON: " <> _) <<< show))
189+ (Aeson .parseJsonStringToAeson res.body)
150190 where
151191 mkUrl :: String
152192 mkUrl = " https://cardano-testnet.blockfrost.io/api/v0/" <> path
0 commit comments