Skip to content
Open
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
9 changes: 8 additions & 1 deletion src/main/kotlin/com/example/dto/yield/YieldPoolFields.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
@JsonSubTypes(
JsonSubTypes.Type(value = YieldPoolFieldsDex::class, name = "dex"),
JsonSubTypes.Type(value = YieldPoolFieldsFarmixLending::class, name = "farmix_lending"),
)
interface YieldPoolFields

Expand All @@ -15,4 +16,10 @@ data class YieldPoolFieldsDex(
val firstAsset: String,
val secondAsset: String,
// extra data, like fees, pool type, and so on.
) : YieldPoolFields
) : YieldPoolFields


@JsonIgnoreProperties(ignoreUnknown = true)
data class YieldPoolFieldsFarmixLending(
val underlyingAsset: String
) : YieldPoolFields
5 changes: 4 additions & 1 deletion src/main/kotlin/com/example/dto/yield/YieldProtocols.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import com.example.api.model.ApiSupportedYields

// TODO: add your protocol here
enum class YieldProtocols(val value: String) {
STONFI_V1("stonfi_v1");
STONFI_V1("stonfi_v1"),
FARMIX_V1_LENDING("farmix_v1_lending");

companion object {
fun resolve(it: String): YieldProtocols {
Expand All @@ -21,13 +22,15 @@ enum class YieldProtocols(val value: String) {
fun YieldProtocols.mapToApi(): ApiSupportedYields {
when (this) {
YieldProtocols.STONFI_V1 -> return ApiSupportedYields.STONFI
YieldProtocols.FARMIX_V1_LENDING -> return ApiSupportedYields.FARMIXLENDING
else -> throw IllegalArgumentException("Unsupported Yield Protocol: $this")
}
}

fun ApiSupportedYields.mapToYieldProtocols(): YieldProtocols {
when (this) {
ApiSupportedYields.STONFI -> return YieldProtocols.STONFI_V1
ApiSupportedYields.FARMIXLENDING -> return YieldProtocols.FARMIX_V1_LENDING
else -> throw IllegalArgumentException("Unsupported Yield Protocol: $this")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import com.example.service.ConversionServiceStub
import com.example.service.statuses.StatusObserverServiceStub
import com.example.service.statuses.StatusServiceStub
import com.example.utils.ObjectMappers
import com.example.utils.http.AsyncHttpClient
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import org.ton.java.address.Address
import org.ton.java.cell.Cell
import org.ton.java.cell.CellBuilder
import org.ton.java.cell.CellSlice
import org.ton.java.tonlib.Tonlib
import org.ton.java.tonlib.types.TvmStackEntryNumber
import org.ton.java.tonlib.types.TvmStackEntrySlice
import org.ton.java.utils.Utils
import java.util.ArrayDeque


class FarmixLendingService(
private val httpClient: AsyncHttpClient,
private val tonlib: Tonlib,
private val conversionService: ConversionServiceStub,
private val statusServiceStub: StatusServiceStub,
private val statusObserverServiceStub: StatusObserverServiceStub,
) {

suspend fun getTotalSupply(pool: String): String {
val response = httpClient.get(
"https://indexer.swap.coffee/v1/jettons/master/$pool"
).body()
val jettonMaster = ObjectMappers.SNAKE_CASE.readValue(response, JettonMasterResponse::class.java)
return jettonMaster.totalSupply
}

suspend fun getUserPositions(userAddress: String): List<Pair<String, Double>> {
val res = httpClient.get(
"https://api.farmix.tg/bff/v1/owner/stakes/all",
args = mapOf(Pair("force", "false"), Pair("addr", userAddress)),
).body()

val parsed = ObjectMappers.SNAKE_CASE.readValue(res, UserPositionsRes::class.java)

return parsed.stakes
.filter { it.stake_usd_amount > 0 }
.map {
it.farmixPool to it.stake_usd_amount
}
}

suspend fun getUserPosition(poolAddress: String, userAddress: String): Pair<String, String> {
var res = tonlib.runMethod(
Address.of(poolAddress),
"get_wallet_address",
ArrayDeque<String?>().apply {
add(
"[[slice, ${
Utils.bytesToHex(
CellBuilder.beginCell().storeAddress(Address.of(userAddress)).endCell().toBoc()
)
}]]"
)
}
)
require(res.exit_code == 0L)
val cs = CellSlice.beginParse(Cell.fromBocBase64((res.stack[0] as TvmStackEntrySlice).slice.bytes))
val jettonWallet = cs.loadAddress()
try {
res = tonlib.runMethod(
jettonWallet,
"get_wallet_data"
)
require(res.exit_code == 0L)
return jettonWallet.toBounceable() to (res.stack[0] as TvmStackEntryNumber).number.toString()
} catch (t: Throwable) {
return "0" to "0"
}
}


@JsonIgnoreProperties(ignoreUnknown = true)
private data class UserPosition(
val stakerAddr: String,
val farmixPool: String,
val jettonMaster: String,
val stake_usd_amount: Double,
)

private data class UserPositionsRes(
val stakes: List<UserPosition>
)

@JsonIgnoreProperties(ignoreUnknown = true)
private data class JettonMasterResponse(
val address: String,
val totalSupply: String
)


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import com.example.dto.db.LiquidityPool
import com.example.dto.yield.YieldPoolFieldsFarmixLending
import com.example.dto.yield.YieldProtocols
import com.example.dto.yield.YieldTradingStatistics
import com.example.loader.LoadedData
import com.example.loader.LoaderService
import com.example.repository.PoolsRepository
import com.example.service.ConversionServiceStub
import com.example.service.YieldBoostsService
import com.example.service.YieldTradingStatisticsService
import com.example.utils.ObjectMappers
import com.example.utils.http.AsyncHttpClient
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import org.ton.java.tonlib.Tonlib
import ru.tinkoff.kora.common.Component
import java.util.concurrent.TimeUnit

@Component
class FarmixLengingLoaderService(
private val yieldBoostsService: YieldBoostsService,
private val yieldTradingStatisticsService: YieldTradingStatisticsService,
private val httpClient: AsyncHttpClient,
private val repository: PoolsRepository,
private val tonlib: Tonlib,
private val conversionService: ConversionServiceStub
) : LoaderService {

override val workerName: String = "farmix_lending"

override val workFrequencyMillis: Long = TimeUnit.MINUTES.toMillis(5)

override suspend fun doWork() {

val poolsRaw = ObjectMappers.DEFAULT.readValue(
httpClient.get("https://api.farmix.tg/bff/v1/pool/staker/all").body(),
LendingPoolsRes::class.java
)
.pools

// We suppose that pool if validTillUtcSeconds is in the past then the pool is considered disabled and will not be shown
val pools = poolsRaw.map { p ->
LiquidityPool(
YieldProtocols.FARMIX_V1_LENDING,
p.addr,
0,
if ((p.inited ?: false) && !(p.disabled ?: true) && !(p.deprecated ?: true) && !(p.paused ?: true)) Long.MAX_VALUE else 0,
ObjectMappers.DEFAULT.writeValueAsString(YieldPoolFieldsFarmixLending(p.underlyingJettonAddr))
)
}

val stats = poolsRaw
.filter { p -> (p.inited ?: false) && !(p.disabled ?: true) && !(p.deprecated ?: true) && !(p.paused ?: true) }
.map {
// TODO(latter fees and volumes will be added)
Pair(it.addr, YieldTradingStatistics(
(it.realAprPr ?: 0.0) * 100.0,
0.0,
0.0,
0.0,
0.0,
(it.tvlUsd ?: 0.0),
))
}
.map {
LoadedData(it.first, it.second, true)
}


// TODO(would be better to use transaction here)
for (p in pools) {
repository.insertLiquidityPool(p)
}

yieldTradingStatisticsService.saveData(stats);
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class LendingPoolsRes(
val pools: List<LendingPool>
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class LendingPool(
val addr: String,
@JsonProperty("tvl_usd")
val tvlUsd: Double?,
val paused: Boolean?,
val disabled: Boolean?,
val deprecated: Boolean?,
val inited: Boolean?,
@JsonProperty("real_apr_pr")
val realAprPr: Double?,
@JsonProperty("target_jetton_master_addr")
val underlyingJettonAddr: String
)
}
57 changes: 54 additions & 3 deletions src/main/kotlin/com/example/service/YieldService.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.service

import FarmixLendingService
import com.example.api.model.*
import com.example.dto.yield.*
import com.example.protocols.stonfi.StonfiV1Service
Expand All @@ -19,7 +20,8 @@ class YieldService(
private val yieldBoostsService: YieldBoostsService,
private val yieldTradingStatisticsService: YieldTradingStatisticsService,
private val tokenService: TokenService,
private val stonfiV1Service: StonfiV1Service
private val stonfiV1Service: StonfiV1Service,
private val farmixLendingService: FarmixLendingService,
// TODO: add you service here, like stonfiV1Service
) {

Expand Down Expand Up @@ -95,9 +97,29 @@ class YieldService(
)
ApiYieldSearchWrapper(stat, mapper(poolHolder))
}

val farmixLendingPositions = if (protocols.contains(YieldProtocols.FARMIX_V1_LENDING)) {
farmixLendingService.getUserPositions(userAddress).associateBy { it.first }
} else {
emptyMap()
}.filter { it.key in pools.keys }
.mapNotNull { (poolAddress, balance) ->
val poolHolder = pools[poolAddress] ?: return@mapNotNull null
val stat = ApiPoolStatistics(
poolHolder.stat.tvlUsd,
balance.second,
0.0,
poolHolder.stat.apr,
poolHolder.stat.lpApr,
poolHolder.stat.boostApr
)
ApiYieldSearchWrapper(stat, mapper(poolHolder))
}

// TODO: implement interaction with your protocol in a same way
// implement `getUserPositions` as you wish, you may pass any arguments here
userPositions.addAll(stonfiPositions)
userPositions.addAll(farmixLendingPositions)

return userPositions
}
Expand Down Expand Up @@ -149,6 +171,15 @@ class YieldService(
)
}

is YieldPoolFieldsFarmixLending -> {
val poolInfo = mapper(item) as ApiPool
ApiYieldDetailsDex(
poolInfo,
emptyList<ApiBoost>(),
farmixLendingService.getTotalSupply(item.poolAddress)
)
}

else -> TODO("When you implement new YieldPoolFields_<Protocol>, add it here, and provide latest data to user")
}
}
Expand All @@ -169,6 +200,15 @@ class YieldService(
emptyList() // boosts
)
)
} else if (poolHolder.protocol == YieldProtocols.FARMIX_V1_LENDING) {
val userData = farmixLendingService.getUserPosition(poolAddress, userAddress)
ApiYieldUserDetails(
ApiYieldUserDetailsDex(
userData.second, // user's lp amount
userData.first, // user's jetton wallet
emptyList() // boosts
)
)
} else {
// TODO: call yourProtocolService.getUserPosition, which may return any data, which will be mapped into ApiYieldUserDetails
TODO("Implement me")
Expand Down Expand Up @@ -245,6 +285,19 @@ class YieldService(
null
)

is YieldPoolFieldsFarmixLending -> ApiPool(
item.protocol.value,
item.poolAddress,
ApiPoolType.PUBLIC,
ApiAmmType.FARMIX_LENDING,
listOf(tokenService.toApiModel(it.underlyingAsset)),
listOf(0.0, 0.0),
ApiPoolFees(0.0),
null,
null,
null
)

else -> TODO("Implement your YieldPoolFields, which returns main info about your pool")
}
}
Expand All @@ -256,6 +309,4 @@ class YieldService(
val boosts: List<YieldBoost>,
val stat: YieldTradingStatistics
)


}
2 changes: 2 additions & 0 deletions src/main/resources/openapi/yield-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ components:
- curve_fi_stable
- concentrated_v3
- tonstakers
- farmix_lending
ApiPoolFees:
type: object
required:
Expand Down Expand Up @@ -546,6 +547,7 @@ components:
- coffee
- tonco
- tonstakers
- farmixlending
ApiPoolStatistics:
type: object
required:
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ redis {

openapi {
management {
file = ["openapi/swap-spec.yaml"]
file = ["openapi/yield-spec.yaml"]
enabled = true
endpoint = "/openapi"
swaggerui {
Expand Down