Skip to content

Commit 705feb1

Browse files
Merge pull request #12 from mlabs-haskell/calum/optimize-list-nft
Improve NFT listing performance
2 parents 3de16d6 + a6e5a40 commit 705feb1

File tree

3 files changed

+98
-50
lines changed

3 files changed

+98
-50
lines changed

spago.dhall

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ You can edit this file as you like.
2828
, "mote"
2929
, "newtype"
3030
, "ordered-collections"
31+
, "parallel"
3132
, "partial"
3233
, "prelude"
34+
, "random"
3335
, "spec"
3436
, "strings"
3537
, "text-encoding"

src/Seabug/Contract/MarketPlaceListNft.purs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Contract.Prelude
88

99
import Contract.Address (getNetworkId, typedValidatorEnterpriseAddress)
1010
import Contract.Monad (Contract, liftContractE, liftedM)
11-
import Contract.PlutusData (fromData, getDatumByHash)
11+
import Contract.PlutusData (fromData, getDatumsByHashes)
1212
import Contract.Transaction
1313
( TransactionInput
1414
, TransactionOutput(TransactionOutput)
@@ -17,10 +17,12 @@ import Contract.Utxos (utxosAt)
1717
import Contract.Value (valueOf)
1818
import Control.Alternative (guard)
1919
import Control.Monad.Maybe.Trans (MaybeT(MaybeT), runMaybeT)
20-
import Data.Array (catMaybes)
20+
import Control.Monad.Reader (asks)
21+
import Control.Parallel (parTraverse)
22+
import Data.Array (catMaybes, mapMaybe)
2123
import Data.Map as Map
2224
import Seabug.MarketPlace (marketplaceValidator)
23-
import Seabug.Metadata (FullSeabugMetadata, getFullSeabugMetadata)
25+
import Seabug.Metadata (FullSeabugMetadata, getFullSeabugMetadataWithBackoff)
2426
import Seabug.Types (MarketplaceDatum(MarketplaceDatum))
2527

2628
type ListNftResult =
@@ -38,6 +40,7 @@ marketPlaceListNft
3840
marketPlaceListNft = do
3941
marketplaceValidator' <- unwrap <$> liftContractE marketplaceValidator
4042
networkId <- getNetworkId
43+
projectId <- asks $ unwrap >>> _.projectId
4144
scriptAddr <-
4245
liftedM "marketPlaceListNft: Cannot convert validator hash to address"
4346
$ pure
@@ -46,14 +49,17 @@ marketPlaceListNft = do
4649
scriptUtxos <- Map.toUnfoldable <<< unwrap <$>
4750
liftedM "marketPlaceListNft: Cannot get script Utxos"
4851
(utxosAt scriptAddr)
49-
withMetadata <- for scriptUtxos $
52+
datums <- getDatumsByHashes
53+
$ mapMaybe (snd >>> unwrap >>> _.dataHash) scriptUtxos
54+
withMetadata <- liftAff $ (flip parTraverse) scriptUtxos $
5055
\(input /\ output@(TransactionOutput out)) ->
5156
runMaybeT $ do
52-
datumHash <- MaybeT $ pure $ out.dataHash
53-
plutusData <- MaybeT $ getDatumByHash datumHash
5457
MarketplaceDatum { getMarketplaceDatum: curr /\ name } <-
55-
MaybeT $ pure $ fromData $ unwrap plutusData
58+
MaybeT $ pure $ (fromData <<< unwrap)
59+
=<< (_ `Map.lookup` datums)
60+
=<< out.dataHash
5661
guard $ valueOf out.amount curr name == one
57-
metadata <- MaybeT $ map hush $ getFullSeabugMetadata $ curr /\ name
62+
metadata <- MaybeT $ map hush $
63+
getFullSeabugMetadataWithBackoff (curr /\ name) projectId
5864
pure { input, output, metadata }
5965
pure $ catMaybes withMetadata

src/Seabug/Metadata.purs

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
module Seabug.Metadata
2-
( FullSeabugMetadata
2+
( BlockfrostFetchError(..)
3+
, FullSeabugMetadata
34
, Hash
45
, getFullSeabugMetadata
6+
, getFullSeabugMetadataWithBackoff
57
) where
68

79
import Contract.Prelude
810

911
import Aeson as Aeson
12+
import Affjax (printError)
1013
import Affjax as Affjax
1114
import Affjax.RequestHeader as Affjax.RequestHeader
1215
import Affjax.ResponseFormat as Affjax.ResponseFormat
1316
import Cardano.Types.Value as Cardano.Types.Value
14-
import Contract.Monad (Contract)
1517
import Contract.Prim.ByteArray (byteArrayToHex)
16-
import Contract.Transaction
17-
( ClientError(ClientHttpError, ClientDecodeJsonError)
18-
)
1918
import Contract.Value
2019
( CurrencySymbol
2120
, TokenName
@@ -24,42 +23,83 @@ import Contract.Value
2423
, mkCurrencySymbol
2524
)
2625
import Control.Alternative (guard)
26+
import Control.Monad.Error.Class (throwError)
2727
import Control.Monad.Except.Trans (ExceptT(ExceptT), except, runExceptT)
28+
import Control.Monad.Reader (ReaderT, runReaderT)
2829
import Control.Monad.Reader.Trans (asks)
2930
import Control.Monad.Trans.Class (lift)
3031
import Data.Argonaut as Argonaut
31-
import Data.Bifunctor (bimap, lmap)
32+
import Data.Bifunctor (lmap)
3233
import Data.Function (on)
3334
import Data.HTTP.Method (Method(GET))
3435
import Data.Newtype (unwrap)
36+
import Effect.Aff (delay)
37+
import Effect.Random (randomRange)
3538
import Seabug.Metadata.Types (SeabugMetadata(SeabugMetadata))
3639
import Partial.Unsafe (unsafePartial)
3740

38-
import Debug (traceM)
39-
4041
type Hash = String
4142

4243
type 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+
4787
getFullSeabugMetadata
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

5798
getIpfsHash
58-
:: forall (r :: Row Type)
59-
. SeabugMetadata
60-
-> ExceptT ClientError (Contract (projectId :: String | r)) Hash
99+
:: SeabugMetadata
100+
-> BlockfrostFetch Hash
61101
getIpfsHash (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
74114
getMintingTxSeabugMetadata 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
97137
getMintingTxHash
98138
:: forall (r :: Row Type)
99139
. CurrencySymbol /\ TokenName
100-
-> ExceptT ClientError (Contract (projectId :: String | r)) Hash
140+
-> BlockfrostFetch Hash
101141
getMintingTxHash a =
102-
except <<< decodeFieldJson "initial_mint_tx_hash"
142+
except <<< decodeField "initial_mint_tx_hash"
103143
=<< mkGetRequest ("assets/" <> uncurry mkAsset a)
104144

105145
mkAsset :: 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
116156
decodeField 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-
133164
mkGetRequest
134165
:: forall (r :: Row Type)
135166
. String
136-
-> ExceptT ClientError (Contract (projectId :: String | r)) Argonaut.Json
167+
-> BlockfrostFetch Aeson.Aeson
137168
mkGetRequest 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

Comments
 (0)