diff --git a/src/main/kotlin/com/example/dto/yield/YieldPoolFields.kt b/src/main/kotlin/com/example/dto/yield/YieldPoolFields.kt index 3a9ae1b..dc7d3f2 100644 --- a/src/main/kotlin/com/example/dto/yield/YieldPoolFields.kt +++ b/src/main/kotlin/com/example/dto/yield/YieldPoolFields.kt @@ -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 = YieldPoolFieldsStormTrade::class, name = "stormtrade"), ) interface YieldPoolFields @@ -15,4 +16,13 @@ data class YieldPoolFieldsDex( val firstAsset: String, val secondAsset: String, // extra data, like fees, pool type, and so on. +) : YieldPoolFields + +@JsonIgnoreProperties(ignoreUnknown = true) +data class YieldPoolFieldsStormTrade( + val asset: String, + val assetName: String, + val decimals: Int, + val lpJettonMaster: String, + val quoteAssetId: String ) : YieldPoolFields \ No newline at end of file diff --git a/src/main/kotlin/com/example/dto/yield/YieldProtocols.kt b/src/main/kotlin/com/example/dto/yield/YieldProtocols.kt index 667ba88..e99f19c 100644 --- a/src/main/kotlin/com/example/dto/yield/YieldProtocols.kt +++ b/src/main/kotlin/com/example/dto/yield/YieldProtocols.kt @@ -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"), + STORMTRADE("stormtrade"); companion object { fun resolve(it: String): YieldProtocols { @@ -21,6 +22,7 @@ enum class YieldProtocols(val value: String) { fun YieldProtocols.mapToApi(): ApiSupportedYields { when (this) { YieldProtocols.STONFI_V1 -> return ApiSupportedYields.STONFI + YieldProtocols.STORMTRADE -> return ApiSupportedYields.STORMTRADE else -> throw IllegalArgumentException("Unsupported Yield Protocol: $this") } } @@ -28,6 +30,7 @@ fun YieldProtocols.mapToApi(): ApiSupportedYields { fun ApiSupportedYields.mapToYieldProtocols(): YieldProtocols { when (this) { ApiSupportedYields.STONFI -> return YieldProtocols.STONFI_V1 + ApiSupportedYields.STORMTRADE -> return YieldProtocols.STORMTRADE else -> throw IllegalArgumentException("Unsupported Yield Protocol: $this") } } \ No newline at end of file diff --git a/src/main/kotlin/com/example/protocols/stormtrade/StormTradeLoaderService.kt b/src/main/kotlin/com/example/protocols/stormtrade/StormTradeLoaderService.kt new file mode 100644 index 0000000..52cef1b --- /dev/null +++ b/src/main/kotlin/com/example/protocols/stormtrade/StormTradeLoaderService.kt @@ -0,0 +1,206 @@ +package com.example.protocols.stormtrade + +import com.example.dto.db.LiquidityPool +import com.example.dto.yield.YieldPoolFieldsStormTrade +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.dateToLong +import com.example.utils.getLogger +import com.example.utils.http.AsyncHttpClient +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import ru.tinkoff.kora.common.Component +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit + +@Component +class StormTradeLoaderService( + private val yieldBoostsService: YieldBoostsService, + private val yieldTradingStatisticsService: YieldTradingStatisticsService, + private val httpClient: AsyncHttpClient, + private val repository: PoolsRepository, + private val conversionService: ConversionServiceStub +) : LoaderService { + + companion object { + private const val STORMTRADE = "stormtrade" + private const val STORMTRADE_BASE_URL = "https://api5.storm.tg/api" + } + + private val logger = getLogger() + + override val workerName: String = "stormtrade" + override val workFrequencyMillis: Long = TimeUnit.MINUTES.toMillis(10) + + override suspend fun doWork() { + try { + logger.info("Starting StormTrade data loading...") + + val knownPools = repository.selectLiquidityPoolsByProtocol(STORMTRADE) + .associateBy { it.poolAddress } + + // Fetch liquidity sources configuration + val config = fetchConfig() + logger.info("Fetched ${config.liquiditySources.size} liquidity sources from StormTrade") + + // Fetch current vault data + val vaults = fetchVaults() + logger.info("Fetched ${vaults.size} vaults from StormTrade") + + // Create pools that don't exist yet + val newPoolsCreated = createNewPools(config, knownPools) + if (newPoolsCreated > 0) { + logger.info("Created $newPoolsCreated new StormTrade pools") + } + + // Calculate statistics for each vault + val poolStatistics = calculateVaultStatistics(vaults) + logger.info("Calculated statistics for ${poolStatistics.size} vaults") + + // Save data + yieldTradingStatisticsService.saveData(poolStatistics) + yieldBoostsService.saveData(emptyList()) // StormTrade doesn't have traditional boosts + + logger.info("StormTrade data loading completed successfully") + + } catch (e: Exception) { + logger.error("Error loading StormTrade data: ${e.message}", e) + // Don't rethrow - we want the loader to continue running + } + } + + private suspend fun fetchConfig(): StormTradeConfigResponse { + val response = httpClient.get("$STORMTRADE_BASE_URL/config").body() + return ObjectMappers.DEFAULT.readValue(response, StormTradeConfigResponse::class.java) + } + + private suspend fun fetchVaults(): List { + val response = httpClient.get("$STORMTRADE_BASE_URL/vaults").body() + return ObjectMappers.DEFAULT.readValue(response, Array::class.java).toList() + } + + private suspend fun createNewPools( + config: StormTradeConfigResponse, + knownPools: Map + ): Int { + var created = 0 + for (liquiditySource in config.liquiditySources) { + val vaultAddress = liquiditySource.vaultAddress + if (!knownPools.containsKey(vaultAddress)) { + val poolData = YieldPoolFieldsStormTrade( + asset = liquiditySource.asset.assetId, + assetName = liquiditySource.asset.name, + decimals = liquiditySource.asset.decimals, + lpJettonMaster = liquiditySource.lpJettonMaster, + quoteAssetId = liquiditySource.quoteAssetId + ) + + val newPool = LiquidityPool( + YieldProtocols.STORMTRADE, + vaultAddress, + 0, + Long.MAX_VALUE, + ObjectMappers.DEFAULT.writeValueAsString(poolData) + ) + + repository.insertLiquidityPool(newPool) + created++ + logger.debug("Created new pool for vault: $vaultAddress (${liquiditySource.asset.name})") + } + } + return created + } + + private fun calculateVaultStatistics(vaults: List): List> { + return vaults.map { vault -> + try { + // Calculate TVL from vault balances + val lockedBalance = vault.lockedBalance.toBigIntegerOrNull()?.toDouble() ?: 0.0 + val freeBalance = vault.freeBalance.toBigIntegerOrNull()?.toDouble() ?: 0.0 + val totalBalance = lockedBalance + freeBalance + + // Convert to USD using the asset ID + val assetId = vault.config.asset.assetId + val tvlUsd = conversionService.getUsdAmount(assetId, totalBalance) + + // Parse APR from vault data + val yearlyApr = parseApr(vault.apr.year) + + val statistics = YieldTradingStatistics( + apr = yearlyApr, + boostApr = 0.0, // StormTrade doesn't have traditional boosts + lpApr = yearlyApr, + volumeUsd = 0.0, // Volume tracking would require blockchain parsing + feesUsd = 0.0, // Fee tracking would require blockchain parsing + tvlUsd = tvlUsd + ) + + LoadedData(vault.address, statistics, true) + + } catch (e: Exception) { + logger.error("Error calculating statistics for vault ${vault.address}: ${e.message}", e) + // Return empty statistics for this vault + LoadedData(vault.address, YieldTradingStatistics.EMPTY, false) + } + } + } + + private fun parseApr(aprString: String): Double { + return try { + // Handle potential "NaN" values from API + if (aprString.equals("NaN", ignoreCase = true)) { + 0.0 + } else { + aprString.toDoubleOrNull() ?: 0.0 + } + } catch (e: Exception) { + logger.warn("Failed to parse APR value: $aprString") + 0.0 + } + } + + // Data classes for StormTrade API responses (using camelCase to match API) + @JsonIgnoreProperties(ignoreUnknown = true) + data class StormTradeConfigResponse( + val liquiditySources: List + ) + + @JsonIgnoreProperties(ignoreUnknown = true) + data class StormTradeLiquiditySource( + val asset: StormTradeAsset, + val vaultAddress: String, + val quoteAssetId: String, + val lpJettonMaster: String + ) + + @JsonIgnoreProperties(ignoreUnknown = true) + data class StormTradeAsset( + val name: String, + val decimals: Int, + val assetId: String + ) + + @JsonIgnoreProperties(ignoreUnknown = true) + data class StormTradeVault( + val address: String, + val rate: String, + val lockedBalance: String, + val freeBalance: String, + val apr: StormTradeApr, + val predictedRoi: String, + val config: StormTradeLiquiditySource + ) + + @JsonIgnoreProperties(ignoreUnknown = true) + data class StormTradeApr( + val week: String, + val month: String, + val year: String + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/protocols/stormtrade/StormTradeService.kt b/src/main/kotlin/com/example/protocols/stormtrade/StormTradeService.kt new file mode 100644 index 0000000..ec528f8 --- /dev/null +++ b/src/main/kotlin/com/example/protocols/stormtrade/StormTradeService.kt @@ -0,0 +1,361 @@ +package com.example.protocols.stormtrade + +import com.example.api.model.ApiTransactionBoc +import com.example.api.model.ApiTransactionResponse +import com.example.service.ConversionServiceStub +import com.example.service.statuses.StatusHandlerOnReceive +import com.example.service.statuses.StatusObserverServiceStub +import com.example.service.statuses.StatusServiceStub +import com.example.utils.ObjectMappers +import com.example.utils.getLogger +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.TvmStackEntrySlice +import org.ton.java.utils.Utils +import java.math.BigInteger +import java.util.ArrayDeque + +class StormTradeService( + private val httpClient: AsyncHttpClient, + private val tonlib: Tonlib, + private val conversionService: ConversionServiceStub, + private val statusServiceStub: StatusServiceStub, + private val statusObserverServiceStub: StatusObserverServiceStub, +) { + + private val logger = getLogger() + + companion object { + private const val STORMTRADE_BASE_URL = "https://api5.storm.tg/api" + + // StormTrade vault opcodes (from TypeScript VaultOpcodes enum) + private const val VAULT_PROVIDE_LIQUIDITY_OPCODE = 0xc89a3ee4L + private const val VAULT_WITHDRAW_LIQUIDITY_OPCODE = 0x595f07bcL // jetton burn + + // Standard jetton transfer opcode + private const val JETTON_TRANSFER_OPCODE = 0xf8a7ea5L + + // StormTrade fees (from TypeScript samples) + private val PROVIDE_LIQUIDITY_MSG_VALUE = Utils.toNano("0.35") + private val PROVIDE_LIQUIDITY_FORWARD_VALUE = Utils.toNano("0.305") + private val WITHDRAW_LIQUIDITY_MSG_VALUE = Utils.toNano("0.3") + } + + /** + * Gets the liquidity pools configuration from StormTrade API + */ + suspend fun getLiquiditySources(): List { + return try { + val response = httpClient.get("$STORMTRADE_BASE_URL/config").body() + val config = ObjectMappers.DEFAULT.readValue(response, StormTradeLoaderService.StormTradeConfigResponse::class.java) + config.liquiditySources + } catch (e: Exception) { + throw RuntimeException("Failed to fetch StormTrade liquidity sources", e) + } + } + + // Cache for vaults to avoid repeated API calls + private var vaultsCache: List? = null + private var vaultsCacheTime: Long = 0 + private val cacheValidityMs = 60_000L // 1 minute cache + + /** + * Gets vault information for all vaults (with caching) + */ + suspend fun getVaults(): List { + val now = System.currentTimeMillis() + if (vaultsCache != null && (now - vaultsCacheTime) < cacheValidityMs) { + return vaultsCache!! + } + + return try { + val response = httpClient.get("$STORMTRADE_BASE_URL/vaults").body() + val vaults = ObjectMappers.DEFAULT.readValue(response, Array::class.java).toList() + vaultsCache = vaults + vaultsCacheTime = now + vaults + } catch (e: Exception) { + throw RuntimeException("Failed to fetch StormTrade vaults", e) + } + } + + /** + * Gets vault information for a specific vault address + */ + suspend fun getVault(vaultAddress: String): StormTradeLoaderService.StormTradeVault? { + require(vaultAddress.isNotBlank()) { "Vault address cannot be blank" } + return getVaults().find { it.address == vaultAddress } + } + + /** + * Gets user's position in a specific vault + */ + suspend fun getUserPosition(vaultAddress: String, userAddress: String): StormTradeUserPosition? { + require(vaultAddress.isNotBlank()) { "Vault address cannot be blank" } + require(userAddress.isNotBlank()) { "User address cannot be blank" } + + return try { + val encodedVaultAddress = vaultAddress.replace(":", "%3A") + val encodedUserAddress = userAddress.replace(":", "%3A") + val response = httpClient.get( + "$STORMTRADE_BASE_URL/liquidity-providers/$encodedVaultAddress/$encodedUserAddress" + ).body() + ObjectMappers.DEFAULT.readValue(response, StormTradeUserPosition::class.java) + } catch (e: Exception) { + // User has no position in this vault or API error + null + } + } + + /** + * Gets all user positions across all vaults + */ + suspend fun getUserPositions(userAddress: String): List> { + val vaults = getVaults() + val positions = mutableListOf>() + + for (vault in vaults) { + val position = getUserPosition(vault.address, userAddress) + if (position != null && position.supplied.toBigIntegerOrNull()?.let { it > BigInteger.ZERO } == true) { + positions.add(vault.address to position) + } + } + + return positions + } + + /** + * Gets the total supply of LP tokens for a vault + */ + suspend fun getTotalSupply(vaultAddress: String): String { + require(vaultAddress.isNotBlank()) { "Vault address cannot be blank" } + + // For StormTrade, we can calculate total supply from vault data + val vault = getVault(vaultAddress) ?: return "0" + + val lockedBalance = vault.lockedBalance.toBigIntegerOrNull() ?: BigInteger.ZERO + val freeBalance = vault.freeBalance.toBigIntegerOrNull() ?: BigInteger.ZERO + val totalBalance = lockedBalance + freeBalance + + return totalBalance.toString() + } + + /** + * Supply TON to a vault (native TON deposits) + * Based on packNativeProvideLiquidity from TypeScript samples + */ + suspend fun supplyTon( + vaultAddress: String, + userAddress: String, + amount: BigInteger + ): ApiTransactionResponse { + require(vaultAddress.isNotBlank()) { "Vault address cannot be blank" } + require(userAddress.isNotBlank()) { "User address cannot be blank" } + require(amount > BigInteger.ZERO) { "Amount must be positive" } + + val queryId = statusServiceStub.initializeUserDirectQuery() + + // Create native payload (equivalent to packInNativePayload) + val body = CellBuilder.beginCell() + .storeUint(VAULT_PROVIDE_LIQUIDITY_OPCODE, 32) + .storeCoins(amount) + .endCell() + + val totalValue = PROVIDE_LIQUIDITY_MSG_VALUE.add(amount) + + return ApiTransactionResponse( + queryId.toString(), + ApiTransactionBoc( + body.toBase64(), + vaultAddress, + totalValue.toString() + ) + ).also { + statusObserverServiceStub.registerToObserve( + vaultAddress, + queryId, + listOf(StatusHandlerOnReceive(true)) + ) + } + } + + /** + * Supply jetton to a vault + * Based on packJettonProvideLiquidity from TypeScript samples + */ + suspend fun supplyJetton( + vaultAddress: String, + userAddress: String, + jettonAmount: BigInteger, + jettonMasterAddress: String + ): ApiTransactionResponse { + require(vaultAddress.isNotBlank()) { "Vault address cannot be blank" } + require(userAddress.isNotBlank()) { "User address cannot be blank" } + require(jettonMasterAddress.isNotBlank()) { "Jetton master address cannot be blank" } + require(jettonAmount > BigInteger.ZERO) { "Jetton amount must be positive" } + + val queryId = statusServiceStub.initializeUserDirectQuery() + + // Get user's jetton wallet address + val userJettonWallet = getUserJettonWalletAddress(jettonMasterAddress, userAddress) + + // Create provide liquidity payload (equivalent to packProvideLiquidity()) + val provideLiquidityPayload = CellBuilder.beginCell() + .storeUint(VAULT_PROVIDE_LIQUIDITY_OPCODE, 32) + .endCell() + + // Create jetton transfer payload (equivalent to packInJettonPayload) + val body = CellBuilder.beginCell() + .storeUint(JETTON_TRANSFER_OPCODE, 32) // jetton transfer opcode + .storeUint(0, 64) // queryId (StormTrade uses 0) + .storeCoins(jettonAmount) + .storeAddress(Address.of(vaultAddress)) // destination (vault) + .storeAddress(Address.of(userAddress)) // response_destination (user's address, not jetton wallet) + .storeRefMaybe(provideLiquidityPayload) // custom_payload (provide liquidity command) + .storeCoins(PROVIDE_LIQUIDITY_FORWARD_VALUE) // forward_ton_amount + .storeRefMaybe(null) // forward_payload (none needed) + .endCell() + + return ApiTransactionResponse( + queryId.toString(), + ApiTransactionBoc( + body.toBase64(), + userJettonWallet, + PROVIDE_LIQUIDITY_MSG_VALUE.toString() + ) + ).also { + statusObserverServiceStub.registerToObserve( + vaultAddress, + queryId, + listOf(StatusHandlerOnReceive(true)) + ) + } + } + + /** + * Withdraw liquidity from a vault + * Based on createWithdrawLiquidityTx from TypeScript samples + */ + suspend fun withdrawLiquidity( + vaultAddress: String, + userAddress: String, + lpAmount: BigInteger + ): ApiTransactionResponse { + require(vaultAddress.isNotBlank()) { "Vault address cannot be blank" } + require(userAddress.isNotBlank()) { "User address cannot be blank" } + require(lpAmount > BigInteger.ZERO) { "LP amount must be positive" } + + val queryId = statusServiceStub.initializeUserDirectQuery() + + // Get vault config to find LP jetton master + val vault = getVault(vaultAddress) ?: throw IllegalArgumentException("Vault not found: $vaultAddress") + val lpJettonMaster = vault.config.lpJettonMaster + + // Get user's LP wallet address + val userLpWallet = getUserJettonWalletAddress(lpJettonMaster, userAddress) + + // Create withdraw liquidity payload (equivalent to packWithdrawLiquidity) + val body = CellBuilder.beginCell() + .storeUint(VAULT_WITHDRAW_LIQUIDITY_OPCODE, 32) + .storeUint(0, 64) // queryId (StormTrade uses 0) + .storeCoins(lpAmount) + .storeAddress(Address.of(userAddress)) + .endCell() + + return ApiTransactionResponse( + queryId.toString(), + ApiTransactionBoc( + body.toBase64(), + userLpWallet, // Send to user's LP wallet, not vault directly + WITHDRAW_LIQUIDITY_MSG_VALUE.toString() + ) + ).also { + statusObserverServiceStub.registerToObserve( + vaultAddress, + queryId, + listOf(StatusHandlerOnReceive(true)) + ) + } + } + + /** + * Gets user's jetton wallet address for a given jetton master + * Based on StonfiV1Service implementation + */ + suspend fun getUserJettonWalletAddress(jettonMasterAddress: String, userAddress: String): String { + require(jettonMasterAddress.isNotBlank()) { "Jetton master address cannot be blank" } + require(userAddress.isNotBlank()) { "User address cannot be blank" } + + return try { + val res = tonlib.runMethod( + Address.of(jettonMasterAddress), + "get_wallet_address", + ArrayDeque().apply { + add( + "[[slice, ${ + Utils.bytesToHex( + CellBuilder.beginCell().storeAddress(Address.of(userAddress)).endCell().toBoc() + ) + }]]" + ) + } + ) + require(res.exit_code == 0L) { "TON node returned error: ${res.exit_code}" } + val cs = CellSlice.beginParse(Cell.fromBocBase64((res.stack[0] as TvmStackEntrySlice).slice.bytes)) + cs.loadAddress().toString() + } catch (e: Exception) { + throw RuntimeException("Failed to get jetton wallet address for $jettonMasterAddress", e) + } + } + + /** + * Clears the vaults cache + */ + fun clearCache() { + vaultsCache = null + vaultsCacheTime = 0 + logger.debug("StormTrade vaults cache cleared") + } + + /** + * Determines if a vault is for native TON or jetton deposits + */ + private suspend fun isNativeVault(vaultAddress: String): Boolean { + val vault = getVault(vaultAddress) ?: return false + return vault.config.asset.name.equals("TON", ignoreCase = true) + } + + /** + * Provides liquidity to a vault (automatically detects TON vs jetton) + */ + suspend fun provideLiquidity( + vaultAddress: String, + userAddress: String, + amount: BigInteger + ): ApiTransactionResponse { + require(vaultAddress.isNotBlank()) { "Vault address cannot be blank" } + require(userAddress.isNotBlank()) { "User address cannot be blank" } + require(amount > BigInteger.ZERO) { "Amount must be positive" } + + return if (isNativeVault(vaultAddress)) { + supplyTon(vaultAddress, userAddress, amount) + } else { + val vault = getVault(vaultAddress) ?: throw IllegalArgumentException("Vault not found: $vaultAddress") + supplyJetton(vaultAddress, userAddress, amount, vault.config.asset.assetId) + } + } + + // Data class for user position API response + @JsonIgnoreProperties(ignoreUnknown = true) + data class StormTradeUserPosition( + val vault: String, + val trader: String, + val supplied: String, + val averageRate: String + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/service/YieldService.kt b/src/main/kotlin/com/example/service/YieldService.kt index 3a2401d..f08fac6 100644 --- a/src/main/kotlin/com/example/service/YieldService.kt +++ b/src/main/kotlin/com/example/service/YieldService.kt @@ -3,7 +3,9 @@ package com.example.service import com.example.api.model.* import com.example.dto.yield.* import com.example.protocols.stonfi.StonfiV1Service +import com.example.protocols.stormtrade.StormTradeService import com.example.repository.PoolsRepository +import com.example.service.ConversionServiceStub import com.example.utils.ObjectMappers import com.example.utils.getLogger import kotlinx.coroutines.runBlocking @@ -19,7 +21,9 @@ class YieldService( private val yieldBoostsService: YieldBoostsService, private val yieldTradingStatisticsService: YieldTradingStatisticsService, private val tokenService: TokenService, - private val stonfiV1Service: StonfiV1Service + private val stonfiV1Service: StonfiV1Service, + private val stormTradeService: StormTradeService, + private val conversionService: ConversionServiceStub // TODO: add you service here, like stonfiV1Service ) { @@ -95,9 +99,32 @@ class YieldService( ) ApiYieldSearchWrapper(stat, mapper(poolHolder)) } + // StormTrade positions + val stormTradePositions = if (protocols.contains(YieldProtocols.STORMTRADE)) { + stormTradeService.getUserPositions(userAddress).map { (vaultAddress, position) -> + val poolHolder = pools[vaultAddress] ?: return@map null + val suppliedUsd = conversionService.getUsdAmount( + vaultAddress, + position.supplied.toDoubleOrNull() ?: 0.0 + ) + val stat = ApiPoolStatistics( + poolHolder.stat.tvlUsd, + suppliedUsd, + 0.0, + poolHolder.stat.apr, + poolHolder.stat.lpApr, + poolHolder.stat.boostApr + ) + ApiYieldSearchWrapper(stat, mapper(poolHolder)) + }.filterNotNull() + } else { + emptyList() + } + // 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(stormTradePositions) return userPositions } @@ -148,6 +175,19 @@ class YieldService( stonfiV1Service.getTotalSupply(item.poolAddress) ) } + + is YieldPoolFieldsStormTrade -> { + val vault = stormTradeService.getVault(item.poolAddress) + ApiYieldDetailsStormTrade( + vault = mapper(item) as ApiYieldSearchStormTrade, + totalSupply = stormTradeService.getTotalSupply(item.poolAddress), + lockedBalance = vault?.lockedBalance ?: "0", + freeBalance = vault?.freeBalance ?: "0", + aprWeek = vault?.apr?.week, + aprMonth = vault?.apr?.month, + aprYear = vault?.apr?.year + ) + } else -> TODO("When you implement new YieldPoolFields_, add it here, and provide latest data to user") } @@ -160,18 +200,45 @@ class YieldService( val pools = pools ?: throw NullPointerException("Not found pool: $poolAddress") val poolHolder = pools[poolAddress] ?: throw NullPointerException("Not found pool: $poolAddress") - return if (poolHolder.protocol == YieldProtocols.STONFI_V1) { - val userData = stonfiV1Service.getUserPosition(poolAddress, userAddress) - ApiYieldUserDetails( - ApiYieldUserDetailsDex( - userData.first, // user's lp amount - userData.second, // user's jetton wallet - emptyList() // boosts + return when (poolHolder.protocol) { + YieldProtocols.STONFI_V1 -> { + val userData = stonfiV1Service.getUserPosition(poolAddress, userAddress) + ApiYieldUserDetails( + ApiYieldUserDetailsDex( + userData.first, // user's lp amount + userData.second, // 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") + } + + YieldProtocols.STORMTRADE -> { + val position = stormTradeService.getUserPosition(poolAddress, userAddress) + val vault = stormTradeService.getVault(poolAddress) + val userLpWallet = if (position != null && vault != null) { + try { + stormTradeService.getUserJettonWalletAddress(vault.config.lpJettonMaster, userAddress) + } catch (e: Exception) { + poolAddress // fallback to vault address + } + } else { + poolAddress + } + + ApiYieldUserDetails( + ApiYieldUserDetailsStormTrade( + userSuppliedAmount = position?.supplied ?: "0", + vaultAddress = poolAddress, + averageRate = position?.averageRate ?: "0", + userLpWallet = userLpWallet + ) + ) + } + + else -> { + // TODO: call yourProtocolService.getUserPosition, which may return any data, which will be mapped into ApiYieldUserDetails + TODO("Implement me") + } } } @@ -187,31 +254,74 @@ class YieldService( val pools = pools ?: throw NullPointerException("Not found pool: $poolAddress") val poolHolder = pools[poolAddress] ?: throw NullPointerException("Not found pool: $poolAddress") - return if (poolHolder.protocol == YieldProtocols.STONFI_V1) { - when (request) { - is ApiDexPoolLiquidityWithdrawalRequest -> listOf( - stonfiV1Service.closeUserPosition( - poolAddress, - userAddress, - request.lpAmount.toBigInteger() + return when (poolHolder.protocol) { + YieldProtocols.STONFI_V1 -> { + when (request) { + is ApiDexPoolLiquidityWithdrawalRequest -> listOf( + stonfiV1Service.closeUserPosition( + poolAddress, + userAddress, + request.lpAmount.toBigInteger() + ) ) - ) - is ApiDexPoolLiquidityProvisioningRequest -> TODO("Not needed in this example") - is ApiStonfiFarmRequest -> TODO("Not needed in this example") - is ApiYieldInteractionRequestDexStonfiWithdrawFromStaking -> TODO("Not needed in this example") - else -> throw IllegalArgumentException("Unsupported operation") - } - } else { - // TODO if this pool is from your protocol consider to add when(request) like this: - /* - when(request) { - is YourApiPostRequestCommand1 -> yourProtocolService.buildTransactionForCommand1(poolAddress, userAddress, request) - is YourApiPostRequestCommand2 -> yourProtocolService.buildTransactionForCommand2(poolAddress, userAddress, request) + is ApiDexPoolLiquidityProvisioningRequest -> TODO("Not needed in this example") + is ApiStonfiFarmRequest -> TODO("Not needed in this example") + is ApiYieldInteractionRequestDexStonfiWithdrawFromStaking -> TODO("Not needed in this example") else -> throw IllegalArgumentException("Unsupported operation") } - */ - TODO("Implement me") + } + + YieldProtocols.STORMTRADE -> { + when (request) { + is ApiYieldInteractionRequestStormTradeWithdraw -> listOf( + stormTradeService.withdrawLiquidity( + poolAddress, + userAddress, + request.lpAmount.toBigInteger() + ) + ) + + is ApiYieldInteractionRequestStormTradeProvide -> listOf( + stormTradeService.provideLiquidity( + poolAddress, + userAddress, + request.amount.toBigInteger() + ) + ) + + // Keep backward compatibility with DEX requests for migration period + is ApiDexPoolLiquidityWithdrawalRequest -> listOf( + stormTradeService.withdrawLiquidity( + poolAddress, + userAddress, + request.lpAmount.toBigInteger() + ) + ) + + is ApiDexPoolLiquidityProvisioningRequest -> listOf( + stormTradeService.provideLiquidity( + poolAddress, + userAddress, + request.asset1Amount.toBigInteger() + ) + ) + + else -> throw IllegalArgumentException("Unsupported operation for StormTrade: ${request::class.simpleName}") + } + } + + else -> { + // TODO if this pool is from your protocol consider to add when(request) like this: + /* + when(request) { + is YourApiPostRequestCommand1 -> yourProtocolService.buildTransactionForCommand1(poolAddress, userAddress, request) + is YourApiPostRequestCommand2 -> yourProtocolService.buildTransactionForCommand2(poolAddress, userAddress, request) + else -> throw IllegalArgumentException("Unsupported operation") + } + */ + TODO("Implement me") + } } } @@ -244,6 +354,15 @@ class YieldService( null, null ) + + is YieldPoolFieldsStormTrade -> ApiYieldSearchStormTrade( + vaultAddress = item.poolAddress, + asset = tokenService.toApiModel(it.asset), + assetName = it.assetName, + protocol = item.protocol.value, + decimals = it.decimals, + lpJettonMaster = it.lpJettonMaster + ) else -> TODO("Implement your YieldPoolFields, which returns main info about your pool") } diff --git a/src/main/resources/openapi/yield-spec.yaml b/src/main/resources/openapi/yield-spec.yaml index 69153b6..c6036d5 100644 --- a/src/main/resources/openapi/yield-spec.yaml +++ b/src/main/resources/openapi/yield-spec.yaml @@ -546,6 +546,7 @@ components: - coffee - tonco - tonstakers + - stormtrade ApiPoolStatistics: type: object required: @@ -624,6 +625,32 @@ components: # TODO: Add new object, ApiYieldSearch_ here + ApiYieldSearch_StormTrade: + type: object + required: + - vault_address + - asset + - asset_name + - protocol + properties: + vault_address: + type: string + description: StormTrade vault address + asset: + $ref: '#/components/schemas/ApiToken' + asset_name: + type: string + example: TON + protocol: + type: string + example: stormtrade + decimals: + type: integer + format: int32 + lp_jetton_master: + type: string + description: LP jetton master address for this vault + ApiYieldSearchWrapper: type: object required: @@ -636,10 +663,12 @@ components: # TODO: extend this object with ApiYieldSearch_ oneOf: - $ref: '#/components/schemas/ApiPool' + - $ref: '#/components/schemas/ApiYieldSearch_StormTrade' discriminator: propertyName: 'yieldTypeResolver' mapping: dex_pool: '#/components/schemas/ApiPool' + stormtrade_vault: '#/components/schemas/ApiYieldSearch_StormTrade' ApiYieldSearchResponse: type: object required: @@ -671,6 +700,36 @@ components: total_supply: type: string description: Absolute value of total minted LP for this pool + + ApiYieldDetails_StormTrade: + type: object + required: + - vault + - total_supply + - locked_balance + - free_balance + properties: + vault: + $ref: '#/components/schemas/ApiYieldSearch_StormTrade' + total_supply: + type: string + description: Total supply of assets in vault + locked_balance: + type: string + description: Locked balance in vault + free_balance: + type: string + description: Free balance available for trading + apr_week: + type: string + description: Weekly APR + apr_month: + type: string + description: Monthly APR + apr_year: + type: string + description: Yearly APR + ApiYieldDetails: type: object required: @@ -683,10 +742,12 @@ components: # TODO: extend this object with ApiYieldDetails_, don't forget to update discriminator oneOf: - $ref: '#/components/schemas/ApiYieldDetails_Dex' + - $ref: '#/components/schemas/ApiYieldDetails_StormTrade' discriminator: propertyName: 'yieldTypeResolver' mapping: dex_pool: '#/components/schemas/ApiYieldDetails_Dex' + stormtrade_vault: '#/components/schemas/ApiYieldDetails_StormTrade' # TODO: Add new object, ApiYieldUserDetails_ here ApiYieldUserDetails_Dex: @@ -707,6 +768,27 @@ components: type: array items: $ref: '#/components/schemas/ApiClaimingTokenStats' + + ApiYieldUserDetails_StormTrade: + type: object + required: + - user_supplied_amount + - vault_address + - average_rate + properties: + user_supplied_amount: + type: string + description: Amount of assets user has supplied to vault + vault_address: + type: string + description: StormTrade vault address + average_rate: + type: string + description: User's average supply rate + user_lp_wallet: + type: string + description: User's LP jetton wallet address (for withdrawals) + ApiYieldUserDetails: type: object required: @@ -716,10 +798,12 @@ components: # TODO: extend this object with ApiYieldUserDetails_, don't forget to update discriminator oneOf: - $ref: '#/components/schemas/ApiYieldUserDetails_Dex' + - $ref: '#/components/schemas/ApiYieldUserDetails_StormTrade' discriminator: propertyName: 'yieldTypeResolver' mapping: dex_pool: '#/components/schemas/ApiYieldUserDetails_Dex' + stormtrade_vault: '#/components/schemas/ApiYieldUserDetails_StormTrade' # TODO: Add new object, ApiYieldInteractionRequest_ here ApiYieldInteractionRequest_DexStonfiWithdrawFromStaking: @@ -731,6 +815,27 @@ components: type: string description: Stonfi farm position address example: EQDY6PMeMbQSz7dltNN4RRTIxyuBJX4dQESOy8cWR2tUgBxH + + ApiYieldInteractionRequest_StormTradeProvide: + type: object + required: + - amount + properties: + amount: + type: string + description: Amount of asset to provide to vault (absolute value) + example: "1000000000" + + ApiYieldInteractionRequest_StormTradeWithdraw: + type: object + required: + - lp_amount + properties: + lp_amount: + type: string + description: Amount of LP tokens to withdraw (absolute value) + example: "1000000000" + ApiYieldInteractionRequest: type: object required: @@ -743,10 +848,14 @@ components: - $ref: '#/components/schemas/ApiDexPoolLiquidityWithdrawalRequest' - $ref: '#/components/schemas/ApiStonfiFarmRequest' - $ref: '#/components/schemas/ApiYieldInteractionRequest_DexStonfiWithdrawFromStaking' + - $ref: '#/components/schemas/ApiYieldInteractionRequest_StormTradeProvide' + - $ref: '#/components/schemas/ApiYieldInteractionRequest_StormTradeWithdraw' discriminator: propertyName: 'yieldTypeResolver' mapping: dex_provide_liquidity: '#/components/schemas/ApiDexPoolLiquidityProvisioningRequest' dex_withdraw_liquidity: '#/components/schemas/ApiDexPoolLiquidityWithdrawalRequest' dex_stonfi_lock_staking: '#/components/schemas/ApiStonfiFarmRequest' - dex_stonfi_withdraw_staking: '#/components/schemas/ApiYieldInteractionRequest_DexStonfiWithdrawFromStaking' \ No newline at end of file + dex_stonfi_withdraw_staking: '#/components/schemas/ApiYieldInteractionRequest_DexStonfiWithdrawFromStaking' + stormtrade_provide_liquidity: '#/components/schemas/ApiYieldInteractionRequest_StormTradeProvide' + stormtrade_withdraw_liquidity: '#/components/schemas/ApiYieldInteractionRequest_StormTradeWithdraw' \ No newline at end of file