Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ explorer {
]
liquidity-minimum = 100 # in USD
liquidity-minimum = ${?EXPLORER_MARKET_LIQUIDITY_MINIMUM}
mobula-uri = "https://api.mobula.io/api/1"
mobula-uri = "https://api.mobula.io/api/2"
mobula-uri = ${?EXPLORER_MARKET_MOBULA_URI}
coingecko-uri = "https://api.coingecko.com/api/v3"
coingecko-uri = ${?EXPLORER_MARKET_COINGECKO_URI}
token-list-uri = "https://raw.githubusercontent.com/alephium/token-list/master/tokens/mainnet.json"
token-list-uri = ${?EXPLORER_MARKET_TOKEN_LIST_URI}
mobula-api-key = ${?EXPLORER_MARKET_MOBULA_API_KEY}
mobula-max-tokens-per-request = 50
mobula-max-tokens-per-request = 500
mobula-max-tokens-per-request = ${?EXPLORER_MARKET_MOBULA_MAX_TOKENS_PER_REQUEST}
market-chart-days = 365
market-chart-days = ${?EXPLORER_MARKET_CHART_DAYS}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,8 @@ object MarketService extends StrictLogging {
}
}

private def tokenListToAddresses(tokens: ArraySeq[TokenList.Entry]): ArraySeq[Address] = {
tokens.map { token =>
Address.contract(ContractId.unsafe(Hash.unsafe(Hex.unsafe(token.id))))
}
private def tokenToAddress(token: TokenList.Entry): Address = {
Address.contract(ContractId.unsafe(Hash.unsafe(Hex.unsafe(token.id))))
}

// This is used to validate the token list freshness. Only used in `getTokenList`
Expand Down Expand Up @@ -276,13 +274,18 @@ object MarketService extends StrictLogging {
val batches = batchTokens(tokens, marketConfig.mobulaMaxTokensPerRequest)

val batchFutures = foldFutures(batches) { batch =>
val assets = tokenListToAddresses(batch)
val assetsStr = assets.map(_.toBase58).mkString(",")
val blockchains = assets.map(_ => "alephium").mkString(",")
requestBatch(
uri"$mobulaBaseUri/market/multi-data?assets=${assetsStr}&blockchains=${blockchains}",
headers = Map(("Authorization", apiKey.value)),
batch
val mobulaPriceRequest = MobulaPriceRequest(
items = ArraySeq.from(
batch.map { token =>
MobulaPriceRequestAsset(tokenToAddress(token).toBase58, "Alephium")
}
)
)

requestPost(
uri"$mobulaBaseUri/token/price",
writeJs(mobulaPriceRequest),
headers = Map(("Authorization", apiKey.value))
)(response => handleMobulaPricesRateResponse(response, batch, retried))
}

Expand All @@ -297,17 +300,6 @@ object MarketService extends StrictLogging {
}
}

private def requestBatch[A](
uri: Uri,
headers: Map[String, String],
batch: ArraySeq[TokenList.Entry]
)(
f: Response[Either[String, String]] => Future[Either[String, A]]
): Future[Either[String, A]] = {
logger.debug(s"Fetching Mobula data for batch: ${batch.map(_.symbol).mkString(", ")}")
request(uri, headers)(f)
}

private def combineBatchResults(
batchFutures: Future[Seq[Either[String, ArraySeq[MobulaPrice]]]]
): Future[Either[String, ArraySeq[MobulaPrice]]] = {
Expand Down Expand Up @@ -342,7 +334,7 @@ object MarketService extends StrictLogging {
handleResponseAndRetryWithCondition(
"mobula/price",
response,
_.code != StatusCode.Ok,
!_.code.isSuccess,
retried,
convertJsonToMobulaPrices(assets),
getMobulaPricesRemote,
Expand All @@ -357,7 +349,7 @@ object MarketService extends StrictLogging {
handleResponseAndRetryWithCondition(
"coingecko/price",
response,
_.code != StatusCode.Ok,
!_.code.isSuccess,
retried,
convertJsonToCoingeckoPrices,
getCoingeckoPricesRemote,
Expand Down Expand Up @@ -398,7 +390,7 @@ object MarketService extends StrictLogging {
handleResponseAndRetryWithCondition(
tokenListUri,
response,
_.code != StatusCode.Ok,
!_.code.isSuccess,
retried,
ujson =>
Try(read[TokenList](ujson)) match {
Expand Down Expand Up @@ -472,6 +464,30 @@ object MarketService extends StrictLogging {
}
}

def requestPost[A](
uri: Uri,
payload: ujson.Value,
headers: Map[String, String]
)(
f: Response[Either[String, String]] => Future[Either[String, A]]
): Future[Either[String, A]] = {
basicRequest
.post(uri)
.headers(headers)
.body(write(payload))
.contentType("application/json")
.send(backend)
.flatMap(f)
.recover { case e: Throwable =>
// If the service is stopped, we don't want to throw an exception
if (isRunning.get()) {
throw e
} else {
Left(e.getMessage)
}
}
}

def handleChartResponse(
id: String,
response: Response[Either[String, String]],
Expand Down Expand Up @@ -551,26 +567,29 @@ object MarketService extends StrictLogging {
)(json: ujson.Value): Either[String, ArraySeq[MobulaPrice]] = {
json match {
case obj: ujson.Obj =>
obj.value.get("data") match {
case Some(data: ujson.Obj) =>
Try {
ArraySeq.from(assets.flatMap { asset =>
val address = tokenListToAddresses(ArraySeq(asset)).head
data.value.get(address.toBase58).flatMap { value =>
obj.value.get("payload") match {
case Some(payload: ujson.Arr) =>
if (payload.arr.length != assets.length) {
Left(
s"Mobula response length ${payload.arr.length} doesn't match request length ${assets.length}"
)
} else {
Try {
assets.zip(payload.arr).flatMap { case (asset, value) =>
for {
price <- value("price").numOpt
liquidity <- value("liquidity").numOpt
price <- value("priceUSD").numOpt
liquidity <- value("liquidityUSD").numOpt
result <- validateMobulaData(asset, price, liquidity)
} yield {
result
}
}
})
}.toEither.left.map { error =>
error.getMessage
}.toEither.left.map { error =>
error.getMessage
}
}
case _ =>
Left(s"JSON isn't an object: $obj")
Left(s"JSON isn't an array: $obj")
}
case other =>
Left(s"JSON isn't an object: $other")
Expand Down Expand Up @@ -684,5 +703,22 @@ object MarketService extends StrictLogging {
val symbol: String = asset.symbol
}

final private[market] case class MobulaPriceRequestAsset(
address: String,
blockchain: String
)

object MobulaPriceRequestAsset {
implicit val readWriter: ReadWriter[MobulaPriceRequestAsset] = macroRW
}

final private[market] case class MobulaPriceRequest(
items: ArraySeq[MobulaPriceRequestAsset]
)

object MobulaPriceRequest {
implicit val readWriter: ReadWriter[MobulaPriceRequest] = macroRW
}

final private case class CoingeckoPrice(symbol: String, price: Double) extends Price
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import io.vertx.ext.web._
import org.scalatest.concurrent.ScalaFutures
import sttp.tapir._
import sttp.tapir.CodecFormat.TextPlain
import sttp.tapir.generic.auto._
import sttp.tapir.server.vertx.VertxFutureServerInterpreter._

import org.alephium.api.{alphJsonBody => jsonBody}
import org.alephium.explorer.AlephiumFutureSpec
import org.alephium.explorer.GenCoreApi.apiKeyGen
import org.alephium.explorer.api.BaseEndpoint
import org.alephium.explorer.config.ExplorerConfig
import org.alephium.explorer.service.market.MarketService.MobulaPriceRequest
import org.alephium.explorer.web.Server
import org.alephium.json.Json._

Expand Down Expand Up @@ -275,13 +277,12 @@ object MarketServiceSpec {
val routes: ArraySeq[Router => Route] =
ArraySeq(
route(
baseEndpoint.get
.in("market")
.in("multi-data")
.in(query[List[String]]("assets"))
.in(query[List[String]]("blockchains"))
baseEndpoint.post
.in("token")
.in("price")
.in(jsonBody[MobulaPriceRequest])
.out(jsonBody[ujson.Value])
.serverLogicSuccess[Future] { case (_, _) =>
.serverLogicSuccess[Future] { _ =>
Future.successful(ujson.read(mobulaPrices))
}
)
Expand Down Expand Up @@ -333,32 +334,36 @@ object MarketServiceSpec {
}
}"""

def mobulaPrices: String = s"""{"data": {
"vT49PY8ksoUL6NcXiZ1t2wAmC7tTPRfFfER8n3UCLvXy": {
"price": 4.213296507259207,
"liquidity": 1000
def mobulaPrices: String = s"""{"payload": [
{
"priceUSD": $alphPrice,
"liquidityUSD": 1000
},
{
"priceUSD": $usdcPrice,
"liquidityUSD": 99
},
"xoDuoek5V2T1dL2HWwvbHT1JEHjMjtJfJoUS2xKsjFg3": {
"price": 1.0001333774731636,
"liquidity": 1000
{
"priceUSD": 1.0001333774731636,
"liquidityUSD": 1000
},
"zSRgc7goAYUgYsEBYdAzogyyeKv3ne3uvWb3VDtxnaEK": {
"price": $usdtPrice,
"liquidity": 1000
{
"priceUSD": null,
"liquidityUSD": null
},
"22Nb9JajRpAh9A2fWNgoKt867PA6zNyi541rtoraDfKXV": {
"price": 0.999953840448559,
"liquidity": 99
{
"priceUSD": $usdtPrice,
"liquidityUSD": 1000
},
"vP6XSUyjmgWCB2B9tD5Rqun56WJqDdExWnfwZVEqzhQb": {
"price": 2609.03101054154,
"liquidity": 10
{
"priceUSD": 2609.03101054154,
"liquidityUSD": 10
},
"xUTp3RXGJ1fJpCGqsAY6GgyfRQ3WQ1MdcYR1SiwndAbR": {
"price": 67214.51967683395,
"liquidity": 1000
{
"priceUSD": 67214.51967683395,
"liquidityUSD": 1000
}
}
]
}"""

val exchangeRates: String = """
Expand Down