diff --git a/build.gradle.kts b/build.gradle.kts index d58895bd1..0bafdc40e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.11.24" +version = "1.13.4" repositories { google() @@ -63,8 +63,10 @@ kotlin { jvm { compilations.all { kotlinOptions.jvmTarget = "1.8" + kotlinOptions.freeCompilerArgs += "-Xjdk-release=1.8" kotlinOptions.moduleName = "abacus" } + withJava() testRuns["test"].executionTask.configure { useJUnitPlatform() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/SubaccountCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/SubaccountCalculator.kt index 05e2f828f..b48dc9c6c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/SubaccountCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/SubaccountCalculator.kt @@ -245,7 +245,20 @@ internal class SubaccountCalculator(val parser: ParserProtocol) { ) { for (period in periods) { val quoteBalance = parser.asDouble(value(subaccount, "quoteBalance", period)) - if (quoteBalance != null) { + + var hasPositionCalculated = false + positions?.let { + for ((key, position) in positions) { + val valueTotal = parser.asDouble(value(position, "valueTotal", period)) + if (valueTotal != null) { + hasPositionCalculated = true + break + } + } + } + val positionsReady = positions.isNullOrEmpty() || hasPositionCalculated + + if (quoteBalance != null && positionsReady) { var notionalTotal = Numeric.double.ZERO var valueTotal = Numeric.double.ZERO var initialRiskTotal = Numeric.double.ZERO @@ -464,12 +477,9 @@ internal class SubaccountCalculator(val parser: ParserProtocol) { ) { for (period in periods) { val quoteBalance = parser.asDouble(value(subaccount, "quoteBalance", period)) - if (quoteBalance != null) { - val equity = - parser.asDouble(value(subaccount, "equity", period)) ?: Numeric.double.ZERO - val initialRiskTotal = - parser.asDouble(value(subaccount, "initialRiskTotal", period)) - ?: Numeric.double.ZERO + val equity = parser.asDouble(value(subaccount, "equity", period)) + val initialRiskTotal = parser.asDouble(value(subaccount, "initialRiskTotal", period)) + if (quoteBalance != null && equity != null && initialRiskTotal != null) { val imf = parser.asDouble(configs?.get("initialMarginFraction")) ?: parser.asDouble(0.05)!! diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TransferInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TransferInputCalculator.kt index b2e6b11e5..24c60ce46 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TransferInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TransferInputCalculator.kt @@ -151,9 +151,9 @@ internal class TransferInputCalculator(val parser: ParserProtocol) { val exchangeRate = parser.asDouble(parser.value(transfer, "route.exchangeRate")) summary.safeSet("exchangeRate", exchangeRate) - val estimatedRouteDuration = - parser.asDouble(parser.value(transfer, "route.estimatedRouteDuration")) - summary.safeSet("estimatedRouteDuration", estimatedRouteDuration) + val estimatedRouteDurationSeconds = + parser.asDouble(parser.value(transfer, "route.estimatedRouteDurationSeconds")) + summary.safeSet("estimatedRouteDurationSeconds", estimatedRouteDurationSeconds) if (usdcSize != null) { summary.safeSet("usdcSize", usdcSize) @@ -204,9 +204,9 @@ internal class TransferInputCalculator(val parser: ParserProtocol) { val exchangeRate = parser.asDouble(parser.value(transfer, "route.exchangeRate")) summary.safeSet("exchangeRate", exchangeRate) - val estimatedRouteDuration = - parser.asDouble(parser.value(transfer, "route.estimatedRouteDuration")) - summary.safeSet("estimatedRouteDuration", estimatedRouteDuration) + val estimatedRouteDurationSeconds = + parser.asDouble(parser.value(transfer, "route.estimatedRouteDurationSeconds")) + summary.safeSet("estimatedRouteDurationSeconds", estimatedRouteDurationSeconds) val bridgeFee = parser.asDouble(parser.value(transfer, "route.bridgeFee")) summary.safeSet("bridgeFee", bridgeFee) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt index cceb4b4b3..970230c6b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt @@ -80,13 +80,21 @@ internal class SubaccountCalculatorV2( val calculated = subaccount.calculated[period] ?: InternalSubaccountCalculated() subaccount.calculated[period] = calculated + var hasPositionCalculated = false + for (position in positions?.values ?: emptyList()) { + if (position.calculated[period] != null) { + hasPositionCalculated = true + } + } + val positionsReady = positions.isNullOrEmpty() || hasPositionCalculated + val quoteBalance = calculated.quoteBalance - if (quoteBalance != null) { + if (quoteBalance != null && positionsReady) { var notionalTotal = Numeric.double.ZERO var valueTotal = Numeric.double.ZERO var initialRiskTotal = Numeric.double.ZERO - for ((key, position) in positions ?: emptyMap()) { + for (position in positions?.values ?: emptyList()) { val positionCalculated = position.calculated[period] notionalTotal += positionCalculated?.notionalTotal ?: Numeric.double.ZERO valueTotal += positionCalculated?.valueTotal ?: Numeric.double.ZERO @@ -169,9 +177,9 @@ internal class SubaccountCalculatorV2( for (period in periods) { val calculated = subaccount?.calculated?.get(period) val quoteBalance = calculated?.quoteBalance - if (quoteBalance != null) { - val equity = calculated.equity ?: Numeric.double.ZERO - val initialRiskTotal = calculated.initialRiskTotal ?: Numeric.double.ZERO + val equity = calculated?.equity + val initialRiskTotal = calculated?.initialRiskTotal + if (quoteBalance != null && equity != null && initialRiskTotal != null) { val imf = configs?.initialMarginFraction ?: 0.05 calculated.buyingPower = calculateBuyingPower( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TransferInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TransferInputCalculatorV2.kt index 9fcf4590c..44033d3b1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TransferInputCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TransferInputCalculatorV2.kt @@ -51,7 +51,7 @@ internal class TransferInputCalculatorV2( val slippage = parser.asDouble(parser.value(route, "slippage")) val exchangeRate = parser.asDouble(parser.value(route, "exchangeRate")) - val estimatedRouteDuration = parser.asDouble(parser.value(route, "estimatedRouteDuration")) + val estimatedRouteDurationSeconds = parser.asDouble(parser.value(route, "estimatedRouteDurationSeconds")) val bridgeFee = parser.asDouble(parser.value(route, "bridgeFee")) val gasFee = parser.asDouble(parser.value(route, "gasFee")) val toAmount = parser.asDouble(parser.value(route, "toAmount")) @@ -74,7 +74,7 @@ internal class TransferInputCalculatorV2( fee = fee, slippage = slippage, exchangeRate = exchangeRate, - estimatedRouteDuration = estimatedRouteDuration, + estimatedRouteDurationSeconds = estimatedRouteDurationSeconds, usdcSize = usdcSize, bridgeFee = bridgeFee, gasFee = gasFee, @@ -94,7 +94,7 @@ internal class TransferInputCalculatorV2( val slippage = parser.asDouble(parser.value(route, "slippage")) val exchangeRate = parser.asDouble(parser.value(route, "exchangeRate")) - val estimatedRouteDuration = parser.asDouble(parser.value(route, "estimatedRouteDuration")) + val estimatedRouteDurationSeconds = parser.asDouble(parser.value(route, "estimatedRouteDurationSeconds")) val bridgeFee = parser.asDouble(parser.value(route, "bridgeFee")) val gasFee = parser.asDouble(parser.value(route, "gasFee")) val toAmount = parser.asDouble(parser.value(route, "toAmount")) @@ -109,7 +109,7 @@ internal class TransferInputCalculatorV2( fee = fee, slippage = slippage, exchangeRate = exchangeRate, - estimatedRouteDuration = estimatedRouteDuration, + estimatedRouteDurationSeconds = estimatedRouteDurationSeconds, usdcSize = usdcSize, bridgeFee = bridgeFee, gasFee = gasFee, @@ -131,7 +131,7 @@ internal class TransferInputCalculatorV2( fee = null, slippage = null, exchangeRate = null, - estimatedRouteDuration = null, + estimatedRouteDurationSeconds = null, bridgeFee = null, toAmount = null, toAmountMin = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/Vault.kt b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/Vault.kt index a7eb6d260..185195353 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/Vault.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/Vault.kt @@ -100,7 +100,7 @@ object VaultCalculator { } val vaultOfVaultsPnl = - historical!!.megavaultPnl!!.sortedByDescending { parser.asDouble(it.createdAt) } + historical!!.megavaultPnl!!.sortedByDescending { parser.asDatetime(it.createdAt)?.toEpochMilliseconds() ?: 0 } val history = vaultOfVaultsPnl.mapNotNull { entry -> parser.asDatetime(entry.createdAt)?.toEpochMilliseconds()?.toDouble()?.let { createdAt -> @@ -126,6 +126,11 @@ object VaultCalculator { val thirtyDaysAgoTotalPnl = thirtyDaysAgoEntry.totalPnl ?: 0.0 val pnlDifference = latestTotalPnl - thirtyDaysAgoTotalPnl + val timeDifferenceMs = if (latestEntry.date != null && thirtyDaysAgoEntry.date != null) { + latestEntry.date - thirtyDaysAgoEntry.date + } else { + 0.0 + } val thirtyDaysAgoEquity = thirtyDaysAgoEntry.equity ?: 0.0 val thirtyDayReturnPercent = if (thirtyDaysAgoEquity != 0.0) { (pnlDifference / thirtyDaysAgoEquity) @@ -135,15 +140,40 @@ object VaultCalculator { return VaultDetails( totalValue = totalValue, - thirtyDayReturnPercent = thirtyDayReturnPercent, + thirtyDayReturnPercent = if (timeDifferenceMs > 0) thirtyDayReturnPercent * 365.days.inWholeMilliseconds / timeDifferenceMs else 0.0, history = history.toIList(), ) } + private fun maybeAddUsdcRow(positions: List, vaultTvl: Double?): List { + if (vaultTvl != null) { + val usdcTotal = vaultTvl - positions.sumOf { it.marginUsdc ?: 0.0 } + + // add a usdc row + return positions + VaultPosition( + marketId = "USDC-USD", + marginUsdc = usdcTotal, + equityUsdc = usdcTotal, + currentLeverageMultiple = 1.0, + currentPosition = CurrentPosition( + asset = usdcTotal, + usdc = usdcTotal, + ), + thirtyDayPnl = ThirtyDayPnl( + percent = 0.0, + absolute = 0.0, + sparklinePoints = null, + ), + ) + } + return positions + } + fun calculateVaultPositions( positions: IndexerMegavaultPositionResponse?, histories: IndexerVaultsHistoricalPnlResponse?, - markets: IMap? + markets: IMap?, + vaultTvl: Double?, ): VaultPositions? { if (positions?.positions == null) { return null @@ -151,14 +181,18 @@ object VaultCalculator { val historiesMap = histories?.vaultsPnl?.associateBy { it.ticker } + var processedPositions = positions.positions.mapNotNull { + calculateVaultPosition( + it, + historiesMap?.get(it.ticker), + markets?.get(it.ticker), + ) + } + + processedPositions = maybeAddUsdcRow(processedPositions, vaultTvl) + return VaultPositions( - positions = positions.positions.mapNotNull { - calculateVaultPosition( - it, - historiesMap?.get(it.ticker), - markets?.get(it.ticker), - ) - }.toIList(), + positions = processedPositions.toIList(), ) } @@ -170,7 +204,7 @@ object VaultCalculator { return null } - val positions: List? = vault.positions?.mapNotNull { position -> + var positions: List = vault.positions.mapNotNull { position -> val ticker = position.ticker ?: return@mapNotNull null val history = vault.pnls.get(ticker) val market = markets?.get(ticker) @@ -183,7 +217,10 @@ object VaultCalculator { perpetualMarket = market, ) } - return VaultPositions(positions = positions?.toIList()) + + positions = maybeAddUsdcRow(positions, vault.details?.totalValue) + + return VaultPositions(positions = positions.toIList()) } fun calculateVaultPosition( @@ -265,7 +302,7 @@ object VaultCalculator { return null } - val sortedPnl = historicalPnl.sortedByDescending { it.createdAt } + val sortedPnl = historicalPnl.sortedByDescending { parser.asDatetime(it.createdAt)?.toEpochMilliseconds() ?: 0 } val latestEntry = sortedPnl.first() val latestTime = parser.asDatetime(latestEntry.createdAt)?.toEpochMilliseconds() ?: Clock.System.now().toEpochMilliseconds() val thirtyDaysAgoTime = latestTime - 30.days.inWholeMilliseconds diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultAccount.kt b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultAccount.kt index d71873f34..6b5ff642b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultAccount.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultAccount.kt @@ -8,33 +8,11 @@ import indexer.codegen.IndexerTransferType.DEPOSIT import indexer.codegen.IndexerTransferType.TRANSFER_IN import indexer.codegen.IndexerTransferType.TRANSFER_OUT import indexer.codegen.IndexerTransferType.WITHDRAWAL +import indexer.models.chain.OnChainAccountVaultResponse import kollections.toIList import kotlinx.serialization.Serializable import kotlin.js.JsExport -@JsExport -@Serializable -data class ShareUnlock( - val shares: NumShares?, - val unlockBlockHeight: Double?, -) - -@JsExport -@Serializable -data class NumShares( - val numShares: Double?, -) - -@JsExport -@Serializable -data class AccountVaultResponse( - val address: String? = null, - val shares: NumShares? = null, - val shareUnlocks: Array? = null, - val equity: Double? = null, - val withdrawableEquity: Double? = null, -) - @JsExport @Serializable data class VaultAccount( @@ -45,7 +23,16 @@ data class VaultAccount( val allTimeReturnUsdc: Double?, val vaultTransfers: IList?, val totalVaultTransfersCount: Int?, -) + val vaultShareUnlocks: IList?, +) { + val shareValue: Double? + get() = + if (balanceShares != null && balanceUsdc != null && balanceShares > 0) { + balanceUsdc / balanceShares + } else { + null + } +} @JsExport @Serializable @@ -54,6 +41,14 @@ data class VaultTransfer( val amountUsdc: Double?, val type: VaultTransferType?, val id: String?, + val transactionHash: String?, +) + +@JsExport +@Serializable +data class VaultShareUnlock( + val unlockBlockHeight: Double?, + val amountUsdc: Double?, ) @JsExport @@ -67,8 +62,8 @@ enum class VaultTransferType { object VaultAccountCalculator { private val parser = Parser() - fun getAccountVaultResponse(apiResponse: String): AccountVaultResponse? { - return parser.asTypedObject(apiResponse) + fun getAccountVaultResponse(apiResponse: String): OnChainAccountVaultResponse? { + return parser.asTypedObject(apiResponse) } fun getTransfersBetweenResponse(apiResponse: String): IndexerTransferBetweenResponse? { @@ -76,19 +71,27 @@ object VaultAccountCalculator { } fun calculateUserVaultInfo( - vaultInfo: AccountVaultResponse, - vaultTransfers: IndexerTransferBetweenResponse + vaultInfo: OnChainAccountVaultResponse?, + vaultTransfers: IndexerTransferBetweenResponse, ): VaultAccount { - val presentValue = vaultInfo.equity?.let { it / 1_000_000 } + val presentValue = (vaultInfo?.equity ?: 0.0) / 1_000_000 val netTransfers = parser.asDouble(vaultTransfers.totalNetTransfers) - val withdrawable = vaultInfo.withdrawableEquity?.let { it / 1_000_000 } - val allTimeReturn = - if (presentValue != null && netTransfers != null) (presentValue - netTransfers) else null + val withdrawable = (vaultInfo?.withdrawableEquity ?: 0.0) / 1_000_000 + val allTimeReturn = if (netTransfers != null) (presentValue - netTransfers) else null + + val impliedShareValue: Double = if ( + vaultInfo?.shares?.numShares != null && + vaultInfo.shares.numShares > 0 + ) { + presentValue / vaultInfo.shares.numShares + } else { + 0.0 + } return VaultAccount( balanceUsdc = presentValue, - balanceShares = vaultInfo.shares?.numShares, - lockedShares = vaultInfo.shareUnlocks?.sumOf { el -> el.shares?.numShares ?: 0.0 }, + balanceShares = vaultInfo?.shares?.numShares ?: 0.0, + lockedShares = vaultInfo?.shareUnlocks?.sumOf { el -> el.shares?.numShares ?: 0.0 } ?: 0.0, withdrawableUsdc = withdrawable, allTimeReturnUsdc = allTimeReturn, totalVaultTransfersCount = vaultTransfers.totalResults, @@ -102,8 +105,15 @@ object VaultAccountCalculator { DEPOSIT, WITHDRAWAL, null -> null }, id = el.id, + transactionHash = el.transactionHash, ) }?.toIList(), + vaultShareUnlocks = vaultInfo?.shareUnlocks?.map { el -> + VaultShareUnlock( + unlockBlockHeight = el.unlockBlockHeight, + amountUsdc = el.shares?.numShares?.let { it * impliedShareValue }, + ) + }?.sortedBy { it.unlockBlockHeight }?.toIList(), ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultDepositWithdrawForm.kt b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultDepositWithdrawForm.kt index cb11c8680..d33ef5c88 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultDepositWithdrawForm.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/functional/vault/VaultDepositWithdrawForm.kt @@ -6,14 +6,17 @@ import exchange.dydx.abacus.output.input.ErrorResources import exchange.dydx.abacus.output.input.ErrorString import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.ValidationError +import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.asTypedObject +import exchange.dydx.abacus.protocols.localizeWithParams import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.Parser import exchange.dydx.abacus.utils.format +import indexer.models.chain.OnChainVaultDepositWithdrawSlippageResponse import kollections.toIList import kotlinx.serialization.Serializable import kotlin.js.JsExport -import kotlin.math.floor +import kotlin.math.abs @JsExport @Serializable @@ -21,6 +24,7 @@ data class VaultFormData( val action: VaultFormAction, val amount: Double?, val acknowledgedSlippage: Boolean, + val acknowledgedTerms: Boolean, val inConfirmationStep: Boolean, ) @@ -39,14 +43,9 @@ data class VaultFormAccountData( val canViewAccount: Boolean?, ) -@JsExport -@Serializable -data class VaultDepositWithdrawSlippageResponse( - val sharesToWithdraw: NumShares, - val expectedQuoteQuantums: Double, -) - -object VaultFormValidationErrors { +internal class VaultFormValidationErrors( + private val localizer: LocalizerProtocol? = null, +) { private fun createError( code: String, type: ErrorType, @@ -55,6 +54,12 @@ object VaultFormValidationErrors { textKey: String? = null, textKeyParams: List? = null ): ValidationError { + val paramsMap = mutableMapOf() + for (param in textKeyParams ?: emptyList()) { + if (param.value != null) { + paramsMap[param.key] = param.value + } + } return ValidationError( code = code, type = type, @@ -63,12 +68,18 @@ object VaultFormValidationErrors { link = null, linkText = null, resources = ErrorResources( - title = titleKey?.let { ErrorString(stringKey = it, params = null, localized = null) }, + title = titleKey?.let { + ErrorString( + stringKey = it, + params = null, + localized = localizer?.localize(it), + ) + }, text = textKey?.let { ErrorString( stringKey = it, params = textKeyParams?.toIList(), - localized = null, + localized = localizer?.localizeWithParams(it, paramsMap), ) }, action = null, @@ -105,6 +116,14 @@ object VaultFormValidationErrors { textKey = "APP.VAULTS.DEPOSIT_TOO_HIGH", ) + fun depositTooLow() = createError( + code = "DEPOSIT_TOO_LOW", + type = ErrorType.error, + fields = listOf("amount"), + titleKey = "APP.TRADE.MODIFY_SIZE_FIELD", + textKey = "APP.VAULTS.DEPOSIT_TOO_LOW", + ) + fun withdrawTooHigh() = createError( code = "WITHDRAW_TOO_HIGH", type = ErrorType.error, @@ -113,6 +132,14 @@ object VaultFormValidationErrors { textKey = "APP.VAULTS.WITHDRAW_TOO_HIGH", ) + fun withdrawTooLow() = createError( + code = "WITHDRAW_TOO_LOW", + type = ErrorType.error, + fields = listOf("amount"), + titleKey = "APP.TRADE.MODIFY_SIZE_FIELD", + textKey = "APP.VAULTS.WITHDRAW_TOO_LOW", + ) + fun withdrawingLockedBalance() = createError( code = "WITHDRAWING_LOCKED_BALANCE", type = ErrorType.error, @@ -138,6 +165,13 @@ object VaultFormValidationErrors { titleKey = "APP.VAULTS.ACKNOWLEDGE_HIGH_SLIPPAGE", ) + fun mustAckTerms() = createError( + code = "MUST_ACK_TERMS", + type = ErrorType.error, + fields = listOf("acknowledgeTerms"), + titleKey = "APP.VAULTS.ACKNOWLEDGE_MEGAVAULT_TERMS", + ) + fun vaultAccountMissing() = createError( code = "VAULT_ACCOUNT_MISSING", type = ErrorType.error, @@ -180,9 +214,11 @@ data class VaultWithdrawData( @Serializable data class VaultFormSummaryData( val needSlippageAck: Boolean?, + val needTermsAck: Boolean?, val marginUsage: Double?, val freeCollateral: Double?, val vaultBalance: Double?, + val withdrawableVaultBalance: Double?, val estimatedSlippage: Double?, val estimatedAmountReceived: Double? ) @@ -202,33 +238,63 @@ object VaultDepositWithdrawFormValidator { private const val SLIPPAGE_PERCENT_WARN = 0.01 private const val SLIPPAGE_PERCENT_ACK = 0.04 private const val SLIPPAGE_TOLERANCE = 0.01 + private const val EPSILON_FOR_ERRORS = 0.0001 + + private const val MIN_DEPOSIT_FE_THRESHOLD = 20.0 - fun getVaultDepositWithdrawSlippageResponse(apiResponse: String): VaultDepositWithdrawSlippageResponse? { - return parser.asTypedObject(apiResponse) + fun getVaultDepositWithdrawSlippageResponse(apiResponse: String): OnChainVaultDepositWithdrawSlippageResponse? { + return parser.asTypedObject(apiResponse) + } + + fun calculateSharesToWithdraw( + vaultAccount: VaultAccount?, + amount: Double + ): Double { + val shareValue = vaultAccount?.shareValue ?: 0.0 + if (shareValue == 0.0) { + return 0.0 + } + + val amountToUse = if (vaultAccount?.withdrawableUsdc != null && + vaultAccount.withdrawableUsdc - amount >= -EPSILON_FOR_ERRORS && + vaultAccount.withdrawableUsdc - amount <= 0.01 + ) { + vaultAccount.withdrawableUsdc + } else { + amount + } + + return (amountToUse / shareValue).toLong().toDouble() } fun validateVaultForm( formData: VaultFormData, accountData: VaultFormAccountData?, vaultAccount: VaultAccount?, - slippageResponse: VaultDepositWithdrawSlippageResponse? + slippageResponse: OnChainVaultDepositWithdrawSlippageResponse?, + localizer: LocalizerProtocol? = null, ): VaultFormValidationResult { + val vaultFormValidationErrors = VaultFormValidationErrors(localizer) val errors = mutableListOf() var submissionData: VaultDepositWithdrawSubmissionData? = null - // Calculate post-operation values and slippage - val amount = formData.amount ?: 0.0 - - val shareValue = if (vaultAccount?.balanceUsdc != null && vaultAccount.balanceShares != null && vaultAccount.balanceShares > 0) { - vaultAccount.balanceUsdc / vaultAccount.balanceShares + val sharesToAttemptWithdraw = if (formData.action == VaultFormAction.WITHDRAW && + vaultAccount != null && + (vaultAccount.shareValue ?: 0.0) > 0.0 && + formData.amount != null + ) { + calculateSharesToWithdraw(vaultAccount, formData.amount) } else { null } - val sharesToAttemptWithdraw = if (amount > 0 && shareValue != null && shareValue > 0) { - // shares must be whole numbers - floor(amount / shareValue) - } else { - null + + val amount = when (formData.action) { + VaultFormAction.DEPOSIT -> formData.amount ?: 0.0 + VaultFormAction.WITHDRAW -> if (sharesToAttemptWithdraw != null) { + sharesToAttemptWithdraw * (vaultAccount?.shareValue ?: 0.0) + } else { + formData.amount ?: 0.0 + } } val withdrawnAmountIncludingSlippage = slippageResponse?.expectedQuoteQuantums?.let { it / 1_000_000.0 } @@ -236,6 +302,10 @@ object VaultDepositWithdrawFormValidator { VaultFormAction.DEPOSIT -> (vaultAccount?.balanceUsdc ?: 0.0) + amount VaultFormAction.WITHDRAW -> (vaultAccount?.balanceUsdc ?: 0.0) - amount } + val postOpWithdrawableVaultBalance = when (formData.action) { + VaultFormAction.DEPOSIT -> (vaultAccount?.withdrawableUsdc ?: 0.0) + amount + VaultFormAction.WITHDRAW -> (vaultAccount?.withdrawableUsdc ?: 0.0) - amount + } val (postOpFreeCollateral, postOpMarginUsage) = if (accountData?.freeCollateral != null && accountData.marginUsage != null) { val equity = accountData.freeCollateral / (1 - accountData.marginUsage) @@ -267,56 +337,76 @@ object VaultDepositWithdrawFormValidator { } else { 0.0 } - val needSlippageAck = slippagePercent >= SLIPPAGE_PERCENT_WARN + val needSlippageAck = slippagePercent >= SLIPPAGE_PERCENT_ACK && formData.inConfirmationStep + val needTermsAck = formData.action == VaultFormAction.DEPOSIT && formData.inConfirmationStep // Perform validation checks and populate errors list if (accountData == null) { - errors.add(VaultFormValidationErrors.accountDataMissing(accountData?.canViewAccount)) + errors.add(vaultFormValidationErrors.accountDataMissing(accountData?.canViewAccount)) } if (amount == 0.0) { - errors.add(VaultFormValidationErrors.amountEmpty(formData.action)) + errors.add(vaultFormValidationErrors.amountEmpty(formData.action)) } // can't actually submit if we are missing key validation information if (formData.inConfirmationStep && formData.action === VaultFormAction.WITHDRAW) { if (vaultAccount == null) { - errors.add(VaultFormValidationErrors.vaultAccountMissing()) + errors.add(vaultFormValidationErrors.vaultAccountMissing()) } if (slippageResponse == null || sharesToAttemptWithdraw == null) { - errors.add(VaultFormValidationErrors.slippageResponseMissing()) + errors.add(vaultFormValidationErrors.slippageResponseMissing()) } } if (formData.inConfirmationStep && formData.action === VaultFormAction.DEPOSIT) { if (accountData?.marginUsage == null || accountData.freeCollateral == null) { - errors.add(VaultFormValidationErrors.accountDataMissing(accountData?.canViewAccount)) + errors.add(vaultFormValidationErrors.accountDataMissing(accountData?.canViewAccount)) } } + if (needTermsAck && !formData.acknowledgedTerms) { + errors.add(vaultFormValidationErrors.mustAckTerms()) + } + when (formData.action) { VaultFormAction.DEPOSIT -> { - if (postOpFreeCollateral != null && postOpFreeCollateral < 0) { - errors.add(VaultFormValidationErrors.depositTooHigh()) + if (postOpFreeCollateral != null && postOpFreeCollateral < -EPSILON_FOR_ERRORS) { + errors.add(vaultFormValidationErrors.depositTooHigh()) + } + if (amount > 0 && amount < MIN_DEPOSIT_FE_THRESHOLD) { + errors.add(vaultFormValidationErrors.depositTooLow()) } } VaultFormAction.WITHDRAW -> { - if (postOpVaultBalance != null && postOpVaultBalance < 0) { - errors.add(VaultFormValidationErrors.withdrawTooHigh()) + if (postOpVaultBalance < -EPSILON_FOR_ERRORS) { + errors.add(vaultFormValidationErrors.withdrawTooHigh()) + } + if (amount > 0 && amount < MIN_DEPOSIT_FE_THRESHOLD) { + // only allowed if withdrawing entire balance + if (!( + vaultAccount?.withdrawableUsdc != null && + abs(vaultAccount.withdrawableUsdc - amount) <= 0.01 + ) + ) { + errors.add(vaultFormValidationErrors.withdrawTooLow()) + } } - if (postOpVaultBalance != null && postOpVaultBalance >= 0 && amount > 0 && vaultAccount?.withdrawableUsdc != null && amount > vaultAccount.withdrawableUsdc) { - errors.add(VaultFormValidationErrors.withdrawingLockedBalance()) + if (postOpVaultBalance >= -EPSILON_FOR_ERRORS && amount > 0 && + vaultAccount?.withdrawableUsdc != null && vaultAccount.withdrawableUsdc - amount < -EPSILON_FOR_ERRORS + ) { + errors.add(vaultFormValidationErrors.withdrawingLockedBalance()) } if (sharesToAttemptWithdraw != null && slippageResponse != null && sharesToAttemptWithdraw != slippageResponse.sharesToWithdraw.numShares) { errors.add( - VaultFormValidationErrors.slippageResponseWrongShares(), + vaultFormValidationErrors.slippageResponseWrongShares(), ) } - if (needSlippageAck) { - errors.add(VaultFormValidationErrors.slippageTooHigh(slippagePercent)) - if (slippagePercent >= SLIPPAGE_PERCENT_ACK && !formData.acknowledgedSlippage && formData.inConfirmationStep) { - errors.add(VaultFormValidationErrors.mustAckSlippage()) - } + if (slippagePercent >= SLIPPAGE_PERCENT_WARN) { + errors.add(vaultFormValidationErrors.slippageTooHigh(slippagePercent)) + } + if (needSlippageAck && !formData.acknowledgedSlippage && formData.inConfirmationStep) { + errors.add(vaultFormValidationErrors.mustAckSlippage()) } } } @@ -349,9 +439,11 @@ object VaultDepositWithdrawFormValidator { // Prepare summary data val summaryData = VaultFormSummaryData( needSlippageAck = needSlippageAck, + needTermsAck = needTermsAck, marginUsage = postOpMarginUsage, freeCollateral = postOpFreeCollateral, vaultBalance = postOpVaultBalance, + withdrawableVaultBalance = postOpWithdrawableVaultBalance, estimatedSlippage = slippagePercent, estimatedAmountReceived = if (formData.action === VaultFormAction.WITHDRAW && withdrawnAmountIncludingSlippage != null) withdrawnAmountIncludingSlippage else null, ) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/Asset.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/Asset.kt index 589b39d53..55c6b1e4e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/Asset.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/Asset.kt @@ -112,3 +112,16 @@ data class Asset( } } } + +@JsExport +@Serializable +enum class AssetTags(val rawValue: String) { + MEMES("memes"), + AI("ai-big-data"), + GAMING("gaming"), + RWA("real-world-assets"), + DEPIN("depin"), + LAYER1("layer-1"), + LAYER2("layer-2"), + DEFI("defi"), +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/Vault.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/Vault.kt index d609602a5..cde6bb0a9 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/Vault.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/Vault.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.output +import exchange.dydx.abacus.functional.vault.VaultAccount import exchange.dydx.abacus.functional.vault.VaultDetails import exchange.dydx.abacus.functional.vault.VaultPositions import kotlinx.serialization.Serializable @@ -9,5 +10,6 @@ import kotlin.js.JsExport @Serializable data class Vault( val details: VaultDetails? = null, - val positions: VaultPositions? = null + val positions: VaultPositions? = null, + val account: VaultAccount? = null ) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt index dbad270dc..2faf9496e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -767,6 +767,14 @@ enum class OrderType(val rawValue: String) { operator fun invoke(rawValue: String?) = entries.firstOrNull { it.rawValue == rawValue } } + + val isSlTp: Boolean + get() = listOf( + StopMarket, + TakeProfitMarket, + StopLimit, + TakeProfitLimit, + ).contains(this) } @JsExport @@ -775,6 +783,11 @@ enum class OrderSide(val rawValue: String) { Buy("BUY"), Sell("SELL"); + fun opposite() = when (this) { + Buy -> Sell + Sell -> Buy + } + companion object { operator fun invoke(rawValue: String?) = OrderSide.values().firstOrNull { it.rawValue == rawValue } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TransferInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TransferInput.kt index ee01c3af2..9364961b6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TransferInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TransferInput.kt @@ -354,7 +354,7 @@ data class TransferInputSummary( val filled: Boolean, val slippage: Double?, val exchangeRate: Double?, - val estimatedRouteDuration: Double?, + val estimatedRouteDurationSeconds: Double?, val bridgeFee: Double?, val gasFee: Double?, val toAmount: Double?, @@ -377,7 +377,7 @@ data class TransferInputSummary( val filled = parser.asBool(data["filled"]) ?: false val slippage = parser.asDouble(data["slippage"]) val exchangeRate = parser.asDouble(data["exchangeRate"]) - val estimatedRouteDuration = parser.asDouble(data["estimatedRouteDuration"]) + val estimatedRouteDurationSeconds = parser.asDouble(data["estimatedRouteDurationSeconds"]) val bridgeFee = parser.asDouble(data["bridgeFee"]) val gasFee = parser.asDouble(data["gasFee"]) val toAmount = parser.asDouble(data["toAmount"]) @@ -391,7 +391,7 @@ data class TransferInputSummary( existing?.filled != filled || existing.slippage != slippage || existing.exchangeRate != exchangeRate || - existing.estimatedRouteDuration != estimatedRouteDuration || + existing.estimatedRouteDurationSeconds != estimatedRouteDurationSeconds || existing.bridgeFee != bridgeFee || existing.gasFee != gasFee || existing.toAmount != toAmount || @@ -406,7 +406,7 @@ data class TransferInputSummary( filled, slippage, exchangeRate, - estimatedRouteDuration, + estimatedRouteDurationSeconds, bridgeFee, gasFee, toAmount, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetProcessor.kt index 602b5dccc..c0da0fade 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetProcessor.kt @@ -7,6 +7,7 @@ import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet +import indexer.models.configs.ConfigsAssetMetadata import indexer.models.configs.ConfigsMarketAsset internal interface AssetProcessorProtocol { @@ -97,3 +98,89 @@ internal class AssetProcessor( return received } } +internal interface AssetMetadataProcessorProtocol { + fun process( + assetId: String, + payload: ConfigsAssetMetadata, + ): Asset +} + +internal class AssetMetadataProcessor( + parser: ParserProtocol, + private val localizer: LocalizerProtocol? +) : BaseProcessor(parser), AssetMetadataProcessorProtocol { + private val assetConfigurationsResourcesKeyMap = mapOf( + "string" to mapOf( + "website" to "websiteLink", + "technical_doc" to "whitepaperLink", + "cmc" to "coinMarketCapsLink", + ), + ) + + private val assetConfigurationsKeyMap = mapOf( + "string" to mapOf( + "name" to "name", + ), + "strings" to mapOf( + "sector_tags" to "tags", + ), + ) + + override fun process( + assetId: String, + payload: ConfigsAssetMetadata, + ): Asset { + val imageUrl = payload.logo + val primaryDescriptionKey = "__ASSETS.$assetId.PRIMARY" + val secondaryDescriptionKey = "__ASSETS.$assetId.SECONDARY" + val primaryDescription = localizer?.localize(primaryDescriptionKey) + val secondaryDescription = localizer?.localize(secondaryDescriptionKey) + + return Asset( + id = assetId, + name = payload.name, + tags = payload.sector_tags, + resources = AssetResources( + websiteLink = payload.urls["website"], + whitepaperLink = payload.urls["technical_doc"], + coinMarketCapsLink = payload.urls["cmc"], + imageUrl = imageUrl, + primaryDescriptionKey = primaryDescriptionKey, + secondaryDescriptionKey = secondaryDescriptionKey, + primaryDescription = primaryDescription, + secondaryDescription = secondaryDescription, + ), + ) + } + + override fun received( + existing: Map?, + payload: Map + ): Map? { + return existing + } + + internal fun receivedConfigurations( + assetId: String, + asset: Map?, + payload: Map, + ): Map { + val received = transform(asset, payload, assetConfigurationsKeyMap) + val urls = payload["urls"] as Map + val resources = transform( + parser.asNativeMap(asset?.get("resources")), + urls, + assetConfigurationsResourcesKeyMap, + ).mutable() + val imageUrl = payload["logo"] + val primaryDescriptionKey = "__ASSETS.$assetId.PRIMARY" + val secondaryDescriptionKey = "__ASSETS.$assetId.SECONDARY" + resources.safeSet("imageUrl", imageUrl) + resources.safeSet("primaryDescriptionKey", primaryDescriptionKey) + resources.safeSet("secondaryDescriptionKey", secondaryDescriptionKey) + received["id"] = assetId + received["resources"] = resources + + return received + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetsProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetsProcessor.kt index 70c0f0349..4b7205793 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetsProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetsProcessor.kt @@ -6,16 +6,36 @@ import exchange.dydx.abacus.processor.utils.MarketId import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.utils.mutable +import indexer.models.configs.ConfigsAssetMetadata import indexer.models.configs.ConfigsMarketAsset internal class AssetsProcessor( parser: ParserProtocol, - localizer: LocalizerProtocol? + localizer: LocalizerProtocol?, + val metadataService: Boolean = false, ) : BaseProcessor(parser) { private val assetProcessor = AssetProcessor(parser = parser, localizer = localizer) + private val assetMetadataProcessor = AssetMetadataProcessor(parser = parser, localizer = localizer) override fun environmentChanged() { assetProcessor.environment = environment + assetMetadataProcessor.environment = environment + } + + internal fun processMetadataConfigurations( + existing: MutableMap, + payload: Map, + ): MutableMap { + for ((assetId, data) in payload) { + val asset = assetMetadataProcessor.process( + assetId = assetId, + payload = data, + ) + + existing[assetId] = asset + } + + return existing } internal fun processConfigurations( @@ -50,12 +70,20 @@ internal class AssetsProcessor( if (assetId != null) { val marketPayload = parser.asNativeMap(data) if (marketPayload != null) { - val receivedAsset = assetProcessor.receivedConfigurations( - assetId, - parser.asNativeMap(existing?.get(assetId)), - marketPayload, - deploymentUri, - ) + val receivedAsset = if (metadataService) { + assetMetadataProcessor.receivedConfigurations( + assetId, + parser.asNativeMap(existing?.get(assetId)), + marketPayload, + ) + } else { + assetProcessor.receivedConfigurations( + assetId, + parser.asNativeMap(existing?.get(assetId)), + marketPayload, + deploymentUri, + ) + } assets[assetId] = receivedAsset } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessor.kt index 4d3c23ece..38fd0c3a2 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessor.kt @@ -17,7 +17,7 @@ internal class SkipRouteProcessor(internal val parser: ParserProtocol) { "route.estimated_amount_out" to "toAmount", "route.swap_price_impact_percent" to "aggregatePriceImpact", "route.warning" to "warning", - + "route.estimated_route_duration_seconds" to "estimatedRouteDurationSeconds", // SQUID PARAMS THAT ARE NOW DEPRECATED: // "route.estimate.gasCosts.0.amountUSD" to "gasFee", // "route.estimate.exchangeRate" to "exchangeRate", @@ -72,6 +72,8 @@ internal class SkipRouteProcessor(internal val parser: ParserProtocol) { ): Map { val modified = BaseProcessor(parser).transform(existing, payload, keyMap) + modified.safeSet("estimatedRouteDurationSeconds", parser.value(payload, "route.estimated_route_duration_seconds")) + var bridgeFees = findFee(payload, "BRIDGE") ?: 0.0 // TODO: update web UI to show smart relay fees // For now we're just bundling it with the bridge fees diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/utils/MarketId.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/utils/MarketId.kt index ea61bcaf7..b9a99fea5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/utils/MarketId.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/utils/MarketId.kt @@ -25,13 +25,10 @@ internal object MarketId { internal fun getAssetId( marketId: String, ): String? { - val displayId = getDisplayId(marketId) - val elements = displayId.split("-") - val baseAssetLongForm = elements.first() - val baseAssetElements = baseAssetLongForm.split(",") + val elements = marketId.split("-") - return if (baseAssetElements.isNotEmpty()) { - baseAssetElements.first() + return if (elements.isNotEmpty()) { + elements.first() } else { null } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/vault/VaultProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/vault/VaultProcessor.kt index ec36d5011..d9ef98982 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/vault/VaultProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/vault/VaultProcessor.kt @@ -11,7 +11,9 @@ import exchange.dydx.abacus.state.internalstate.InternalVaultPositionState import exchange.dydx.abacus.state.internalstate.InternalVaultState import indexer.codegen.IndexerMegavaultHistoricalPnlResponse import indexer.codegen.IndexerMegavaultPositionResponse +import indexer.codegen.IndexerTransferBetweenResponse import indexer.codegen.IndexerVaultsHistoricalPnlResponse +import indexer.models.chain.OnChainAccountVaultResponse internal class VaultProcessor( parser: ParserProtocol, @@ -85,4 +87,34 @@ internal class VaultProcessor( existing } } + + fun processTransferBetween( + existing: InternalVaultState?, + payload: IndexerTransferBetweenResponse?, + ): InternalVaultState? { + if (payload == null) { + return existing + } + + return if (payload != existing?.transfers) { + existing?.copy(transfers = payload) ?: InternalVaultState(transfers = payload) + } else { + existing + } + } + + fun processAccountOwnerShares( + existing: InternalVaultState?, + payload: OnChainAccountVaultResponse?, + ): InternalVaultState? { + if (payload == null) { + return existing + } + + return if (payload != existing?.account) { + existing?.copy(account = payload) ?: InternalVaultState(account = payload) + } else { + existing + } + } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/protocols/LocalizerProtocol.kt b/src/commonMain/kotlin/exchange.dydx.abacus/protocols/LocalizerProtocol.kt index 9ed51abe8..d77f340bb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/protocols/LocalizerProtocol.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/protocols/LocalizerProtocol.kt @@ -10,8 +10,8 @@ interface LocalizerProtocol { fun localize(path: String, paramsAsJson: String? = null): String } -fun LocalizerProtocol.localizeWithParams(path: String, params: Map): String? = - localize(path = path, paramsAsJson = params.toJsonPrettyPrint()) +fun LocalizerProtocol.localizeWithParams(path: String, params: Map?): String = + if (params == null) localize(path = path) else localize(path = path, paramsAsJson = params.toJsonPrettyPrint()) interface AbacusLocalizerProtocol : LocalizerProtocol { val languages: List diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/protocols/PublicProtocols.kt b/src/commonMain/kotlin/exchange.dydx.abacus/protocols/PublicProtocols.kt index 8ac8b256a..aa268a4d3 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/protocols/PublicProtocols.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/protocols/PublicProtocols.kt @@ -159,7 +159,8 @@ enum class TransactionType(val rawValue: String) { CctpMultiMsgWithdraw("cctpMultiMsgWithdraw"), SignCompliancePayload("signCompliancePayload"), SetSelectedGasDenom("setSelectedGasDenom"), - SignPushNotificationTokenRegistrationPayload("signPushNotificationTokenRegistrationPayload"); + SignPushNotificationTokenRegistrationPayload("signPushNotificationTokenRegistrationPayload"), + GetMegavaultOwnerShares("getMegavaultOwnerShares"); companion object { operator fun invoke(rawValue: String) = diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/app/adaptors/V4TransactionErrors.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/app/adaptors/V4TransactionErrors.kt index ba63696ae..bcfc3fa4c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/app/adaptors/V4TransactionErrors.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/app/adaptors/V4TransactionErrors.kt @@ -6,6 +6,7 @@ import exchange.dydx.abacus.responses.ParsingErrorType class V4TransactionErrors { companion object { private const val QUERY_RESULT_ERROR_PREFIX = "Query failed" + private const val OUT_OF_GAS_ERROR_RAW_LOG_PREFIX = "out of gas" private val FAILED_SUBACCOUNT_UPDATE_RESULT_PATTERN = Regex("""Subaccount with id \{[^}]+\} failed with UpdateResult:\s*([A-Za-z]+):""") fun error(code: Int?, message: String?, codespace: String? = null): ParsingError? { @@ -43,6 +44,17 @@ class V4TransactionErrors { ) } } + + fun parseErrorFromRawLog(rawLog: String): ParsingError? { + return if (rawLog.startsWith(OUT_OF_GAS_ERROR_RAW_LOG_PREFIX)) { + return ParsingError( + ParsingErrorType.BackendError, + "Out of gas: inaccurate gas estimation for transaction", + ) + } else { + null + } + } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 7ded95727..eb9100df0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -48,6 +48,8 @@ import indexer.codegen.IndexerHistoricalBlockTradingReward import indexer.codegen.IndexerHistoricalTradingRewardAggregation import indexer.codegen.IndexerPerpetualPositionStatus import indexer.codegen.IndexerPositionSide +import indexer.codegen.IndexerTransferBetweenResponse +import indexer.models.chain.OnChainAccountVaultResponse import kotlinx.datetime.Instant internal data class InternalState( @@ -240,6 +242,8 @@ internal data class InternalVaultState( val details: VaultDetails? = null, val positions: List? = null, val pnls: MutableMap = mutableMapOf(), + val transfers: IndexerTransferBetweenResponse? = null, + val account: OnChainAccountVaultResponse? = null, ) internal data class InternalVaultPositionState( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalTransferInputState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalTransferInputState.kt index 2e6910d94..987001dda 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalTransferInputState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalTransferInputState.kt @@ -87,7 +87,7 @@ internal fun TransferInputSummary.Companion.safeCreate(existing: TransferInputSu filled = false, slippage = null, exchangeRate = null, - estimatedRouteDuration = null, + estimatedRouteDurationSeconds = null, bridgeFee = null, gasFee = null, toAmount = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt index b3cc627ff..8e0c4aad7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/AsyncAbacusStateManagerProtocol.kt @@ -105,6 +105,7 @@ interface AsyncAbacusStateManagerProtocol { fun getChainById(chainId: String): TransferChainInfo? fun registerPushNotification(token: String, languageCode: String?) + fun refreshVaultAccount() } @JsExport diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/Environment.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/Environment.kt index 626daa8ca..cc0991ba5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/Environment.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/Environment.kt @@ -34,6 +34,7 @@ data class EnvironmentEndpoints( val faucet: String?, val squid: String?, val skip: String?, + val metadataService: String?, val nobleValidator: String?, val geo: String?, ) { @@ -51,6 +52,7 @@ data class EnvironmentEndpoints( val faucet = parser.asString(data["faucet"]) val squid = parser.asString(data["0xsquid"]) val skip = parser.asString(data["skip"]) + val metadataService = parser.asString(data["metadataService"]) val nobleValidator = parser.asString(data["nobleValidator"]) val geo = parser.asString(data["geo"]) return EnvironmentEndpoints( @@ -59,6 +61,7 @@ data class EnvironmentEndpoints( faucet = faucet, squid = squid, skip = skip, + metadataService = metadataService, nobleValidator = nobleValidator, geo = geo, ) @@ -78,6 +81,7 @@ data class EnvironmentLinks( val blogs: String?, val help: String?, val vaultLearnMore: String?, + val vaultTos: String?, val launchIncentive: String?, val statusPage: String?, val withdrawalGateLearnMore: String?, @@ -102,6 +106,7 @@ data class EnvironmentLinks( val statusPage = parser.asString(data["statusPage"]) val withdrawalGateLearnMore = parser.asString(data["withdrawalGateLearnMore"]) val equityTiersLearnMore = parser.asString(data["equityTiersLearnMore"]) + val vaultTos = parser.asString(data["vaultTos"]) return EnvironmentLinks( tos, privacy, @@ -113,6 +118,7 @@ data class EnvironmentLinks( blogs, help, vaultLearnMore, + vaultTos, launchIncentive, statusPage, withdrawalGateLearnMore, @@ -549,6 +555,13 @@ class V4Environment( data object StatsigConfig { var dc_max_safe_bridge_fees: Float = Float.POSITIVE_INFINITY var ff_enable_limit_close: Boolean = false + var ff_enable_timestamp_nonce: Boolean = false +} + +@JsExport +@Suppress("PropertyName") +data object AutoSweepConfig { + var disable_autosweep: Boolean = false } @JsExport diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/configs/V4StateManagerConfigs.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/configs/V4StateManagerConfigs.kt index e51141a3a..e8e1fc8c0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/configs/V4StateManagerConfigs.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/configs/V4StateManagerConfigs.kt @@ -29,7 +29,11 @@ class V4StateManagerConfigs( "complianceScreen":"/v4/compliance/screen", "complianceGeoblock":"/v4/compliance/geoblock", "complianceGeoblockKeplr":"/v4/compliance/geoblock-keplr", - "height":"/v4/height" + "height":"/v4/height", + "vaultPositions":"/v4/vault/v1/megavault/positions", + "vaultHistoricalPnl":"/v4/vault/v1/megavault/historicalPnl", + "vaultMarketPnls":"/v4/vault/v1/vaults/historicalPnl", + "transfers":"/v4/transfers/between" }, "private":{ "account":"/v4/addresses", @@ -174,4 +178,9 @@ class V4StateManagerConfigs( val path = launchIncentivePath(type) ?: return null return "$api$path" } + + fun metadataServiceInfo(): String? { + val metadataServiceUrl = environment.endpoints.metadataService ?: return null + return "$metadataServiceUrl/info" + } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/notification/providers/FillsNotificationProvider.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/notification/providers/FillsNotificationProvider.kt index aa1b663d4..5ee66d4f8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/notification/providers/FillsNotificationProvider.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/notification/providers/FillsNotificationProvider.kt @@ -156,8 +156,11 @@ class FillsNotificationProvider( val asset = stateMachine.state?.assetOfMarket(marketId) ?: return null val assetText = asset.name val marketImageUrl = asset.resources?.imageUrl - val side = fill.side.rawValue + + // opposite because these notifications are all about positions, not the fill that triggered the notif. + val side = fill.side.opposite().rawValue val sideText = uiImplementations.localizer?.localize("APP.GENERAL.$side") + val amountText = parser.asString(fill.size) val priceText = parser.asString(fill.price) val fillType = fill.type.rawValue diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/PerpTradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/PerpTradingStateMachine.kt index 98e89761b..9703fed7d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/PerpTradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/PerpTradingStateMachine.kt @@ -13,8 +13,9 @@ class PerpTradingStateMachine( useParentSubaccount: Boolean, staticTyping: Boolean = false, trackingProtocol: TrackingProtocol?, + metadataService: Boolean = false, ) : - TradingStateMachine(environment, localizer, formatter, maxSubaccountNumber, useParentSubaccount, staticTyping, trackingProtocol) { + TradingStateMachine(environment, localizer, formatter, maxSubaccountNumber, useParentSubaccount, staticTyping, trackingProtocol, metadataService) { /* Placeholder for now. Eventually, the code specifically for Perpetual will be in this class */ diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt index 0828e4797..4d8954668 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt @@ -8,6 +8,7 @@ import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse +import indexer.models.configs.ConfigsAssetMetadata import indexer.models.configs.ConfigsMarketAsset import kollections.iListOf import kollections.toIList @@ -211,6 +212,38 @@ internal fun TradingStateMachine.receivedBatchedMarketsChanges( } } +internal fun TradingStateMachine.processMarketsConfigurationsWithMetadataService( + payload: Map, + subaccountNumber: Int?, + deploymentUri: String, +): StateChanges { + internalState.assets = assetsProcessor.processMetadataConfigurations( + existing = internalState.assets, + payload = payload, + ) + + marketsCalculator.calculate(internalState.marketsSummary) + val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( + parser = parser, + subaccounts = internalState.wallet.account.subaccounts, + subaccountNumber = subaccountNumber ?: 0, + tradeInput = internalState.input.trade, + ) + return if (subaccountNumber != null) { + StateChanges( + changes = iListOf(Changes.markets, Changes.assets, Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = subaccountNumbers, + ) + } else { + StateChanges( + changes = iListOf(Changes.markets, Changes.assets), + markets = null, + subaccountNumbers = null, + ) + } +} + internal fun TradingStateMachine.processMarketsConfigurations( payload: Map, subaccountNumber: Int?, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Vault.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Vault.kt index 1db139240..ff7dbeb4a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Vault.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Vault.kt @@ -7,7 +7,9 @@ import exchange.dydx.abacus.state.internalstate.InternalState import exchange.dydx.abacus.state.internalstate.InternalVaultState import indexer.codegen.IndexerMegavaultHistoricalPnlResponse import indexer.codegen.IndexerMegavaultPositionResponse +import indexer.codegen.IndexerTransferBetweenResponse import indexer.codegen.IndexerVaultsHistoricalPnlResponse +import indexer.models.chain.OnChainAccountVaultResponse import kollections.iListOf internal fun TradingStateMachine.onMegaVaultPnl( @@ -34,6 +36,22 @@ internal fun TradingStateMachine.onVaultMarketPositions( return updateVaultState(internalState, newState) } +internal fun TradingStateMachine.onVaultTransferHistory( + payload: String +): StateChanges { + val transferBetweenResponse = parser.asTypedObject(payload) + val newState = vaultProcessor.processTransferBetween(internalState.vault, transferBetweenResponse) + return updateVaultState(internalState, newState) +} + +internal fun TradingStateMachine.onAccountOwnerShares( + payload: String +): StateChanges { + val accountVaultResponse = parser.asTypedObject(payload) + val newState = vaultProcessor.processAccountOwnerShares(internalState.vault, accountVaultResponse) + return updateVaultState(internalState, newState) +} + private fun updateVaultState( state: InternalState, newVaultState: InternalVaultState? diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index c934531fa..a797ccc28 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -14,6 +14,7 @@ import exchange.dydx.abacus.calculator.v2.AdjustIsolatedMarginInputCalculatorV2 import exchange.dydx.abacus.calculator.v2.TransferInputCalculatorV2 import exchange.dydx.abacus.calculator.v2.TriggerOrdersInputCalculatorV2 import exchange.dydx.abacus.calculator.v2.tradeinput.TradeInputCalculatorV2 +import exchange.dydx.abacus.functional.vault.VaultAccountCalculator import exchange.dydx.abacus.functional.vault.VaultCalculator import exchange.dydx.abacus.output.Asset import exchange.dydx.abacus.output.Configs @@ -77,6 +78,7 @@ import exchange.dydx.abacus.utils.mutableMapOf import exchange.dydx.abacus.utils.safeSet import exchange.dydx.abacus.utils.typedSafeSet import exchange.dydx.abacus.validator.InputValidator +import indexer.models.configs.ConfigsAssetMetadata import indexer.models.configs.ConfigsMarketAsset import kollections.JsExport import kollections.iListOf @@ -99,6 +101,7 @@ open class TradingStateMachine( private val useParentSubaccount: Boolean, val staticTyping: Boolean = false, private val trackingProtocol: TrackingProtocol?, + val metadataService: Boolean = false, ) { internal var internalState: InternalState = InternalState() @@ -112,6 +115,7 @@ open class TradingStateMachine( val processor = AssetsProcessor( parser = parser, localizer = localizer, + metadataService = metadataService, ) processor.environment = environment processor @@ -313,17 +317,30 @@ open class TradingStateMachine( ): StateChanges { val json = parser.decodeJsonObject(payload) if (staticTyping) { - val parsedAssetPayload = parser.asTypedStringMap(json) - if (parsedAssetPayload == null) { - Logger.e { "Error parsing asset payload" } - return StateChanges.noChange - } + if (metadataService) { + val parsedAssetPayload = parser.asTypedStringMap(json) + if (parsedAssetPayload == null) { + Logger.e { "Error parsing asset payload" } + return StateChanges.noChange + } + return processMarketsConfigurationsWithMetadataService( + payload = parsedAssetPayload, + subaccountNumber = subaccountNumber, + deploymentUri = deploymentUri, + ) + } else { + val parsedAssetPayload = parser.asTypedStringMap(json) + if (parsedAssetPayload == null) { + Logger.e { "Error parsing asset payload" } + return StateChanges.noChange + } - return processMarketsConfigurations( - payload = parsedAssetPayload, - subaccountNumber = subaccountNumber, - deploymentUri = deploymentUri, - ) + return processMarketsConfigurations( + payload = parsedAssetPayload, + subaccountNumber = subaccountNumber, + deploymentUri = deploymentUri, + ) + } } else { return if (json != null) { receivedMarketsConfigurationsDeprecated(json, subaccountNumber, deploymentUri) @@ -1370,7 +1387,14 @@ open class TradingStateMachine( vault = internalState.vault, markets = marketsSummary?.markets, ) - vault = Vault(details = internalState.vault?.details, positions = positions) + val accountInfo = internalState.vault?.account + val transfers = internalState.vault?.transfers + val account = if (accountInfo != null && transfers != null) { + VaultAccountCalculator.calculateUserVaultInfo(vaultInfo = accountInfo, vaultTransfers = transfers) + } else { + null + } + vault = Vault(details = internalState.vault?.details, positions = positions, account = account) } else { vault = null } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt index a8b5123be..bf635e7bc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/AsyncAbacusStateManagerV2.kt @@ -452,6 +452,7 @@ class AsyncAbacusStateManagerV2( dataNotification = dataNotification, presentationProtocol = presentationProtocol, ) + adaptor?.gasToken = gasToken pushNotificationToken?.let { token -> adaptor?.registerPushNotification(token, pushNotificationLanguageCode) } @@ -684,4 +685,8 @@ class AsyncAbacusStateManagerV2( pushNotificationLanguageCode = languageCode adaptor?.registerPushNotification(token, languageCode) } + + override fun refreshVaultAccount() { + adaptor?.refreshVaultAccount() + } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt index aa7dbd7f4..332e04339 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/manager/StateManagerAdaptorV2.kt @@ -122,6 +122,7 @@ internal class StateManagerAdaptorV2( useParentSubaccount = appConfigs.accountConfigs.subaccountConfigs.useParentSubaccount, staticTyping = appConfigs.staticTyping, trackingProtocol = ioImplementations.tracking, + metadataService = appConfigs.metadataService, ) internal val jsonEncoder = JsonEncoder() @@ -291,6 +292,7 @@ internal class StateManagerAdaptorV2( } set(value) { accounts.accountAddress = value + vault.accountAddress = value } internal var walletConnectionType: WalletConnectionType? @@ -689,6 +691,10 @@ internal class StateManagerAdaptorV2( accounts.registerPushNotification(token, languageCode) } + internal fun refreshVaultAccount() { + vault.refreshVaultAccount() + } + private fun updateRestriction(indexerRestriction: UsageRestriction?) { restriction = indexerRestriction ?: accounts.addressRestriction ?: UsageRestriction.noRestriction } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt index 1bb4c1f81..c294fbf84 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/AccountSupervisor.kt @@ -19,6 +19,7 @@ import exchange.dydx.abacus.state.app.adaptors.V4TransactionErrors import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.state.manager.ApiData +import exchange.dydx.abacus.state.manager.AutoSweepConfig import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.state.manager.HistoricalTradingRewardsPeriod import exchange.dydx.abacus.state.manager.HumanReadableCancelAllOrdersPayload @@ -468,6 +469,9 @@ internal open class AccountSupervisor( nobleBalancesTimer = null return } + if (AutoSweepConfig.disable_autosweep) { + return + } val timer = helper.ioImplementations.timer ?: CoroutineTimer.instance nobleBalancesTimer = timer.schedule(0.0, nobleBalancePollingDuration) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/Configs.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/Configs.kt index 50dde4b11..39d7473f1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/Configs.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/Configs.kt @@ -189,7 +189,6 @@ data class OnboardingConfigs( @JsExport data class VaultConfigs( val retrieveVault: Boolean, - var useMocks: Boolean = true, ) { companion object { val forApp = VaultConfigs( @@ -223,6 +222,7 @@ data class AppConfigsV2( var enableLogger: Boolean = false, var triggerOrderToast: Boolean = false, var staticTyping: Boolean = false, + var metadataService: Boolean = false, ) { companion object { val forApp = AppConfigsV2( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/ConnectionsSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/ConnectionsSupervisor.kt index 430e78fef..d23cf2916 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/ConnectionsSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/ConnectionsSupervisor.kt @@ -7,6 +7,7 @@ import exchange.dydx.abacus.protocols.TransactionType import exchange.dydx.abacus.state.manager.GasToken import exchange.dydx.abacus.state.manager.IndexerURIs import exchange.dydx.abacus.state.manager.NetworkState +import exchange.dydx.abacus.state.manager.StatsigConfig import exchange.dydx.abacus.state.manager.SystemUtils import exchange.dydx.abacus.state.model.TradingStateMachine import exchange.dydx.abacus.utils.AnalyticsUtils @@ -102,7 +103,6 @@ internal class ConnectionsSupervisor( indexerConfig = null validatorConnected = false socketConnected = false - validatorUrl = null disconnectSocket() } } @@ -175,14 +175,8 @@ internal class ConnectionsSupervisor( } private fun bestEffortConnectChain() { - if (validatorUrl == null) { - val endpointUrls = helper.configs.validatorUrls() - validatorUrl = endpointUrls?.firstOrNull() - } findOptimalNode { url -> - if (url != this.validatorUrl) { - this.validatorUrl = url - } + this.validatorUrl = url } } @@ -250,6 +244,9 @@ internal class ConnectionsSupervisor( params.safeSet("CHAINTOKEN_DENOM", chainTokenDenom) params.safeSet("CHAINTOKEN_DECIMALS", chainTokenDecimals) params.safeSet("txnMemo", "dYdX Frontend (${SystemUtils.platform.rawValue})") + + params.safeSet("enableTimestampNonce", StatsigConfig.ff_enable_timestamp_nonce) + val jsonString = JsonEncoder().encode(params) ?: return helper.ioImplementations.threading?.async(ThreadingType.main) { @@ -391,7 +388,6 @@ internal class ConnectionsSupervisor( val timer = helper.ioImplementations.timer ?: CoroutineTimer.instance chainTimer = timer.schedule(serverPollingDuration, null) { if (readyToConnect) { - validatorUrl = null bestEffortConnectChain() } false diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/NetworkHelper.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/NetworkHelper.kt index 7864ebf5f..997ffd3b4 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/NetworkHelper.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/NetworkHelper.kt @@ -533,11 +533,15 @@ class NetworkHelper( val result = parser.decodeJsonObject(response) if (result != null) { val error = parser.asMap(result["error"]) + val rawLog = parser.asString(result["rawLog"]) if (error != null) { val message = parser.asString(error["message"]) val code = parser.asInt(error["code"]) val codespace = parser.asString(error["codespace"]) return V4TransactionErrors.error(code, message, codespace) + } else if (rawLog != null) { + // certain tx results (e.g. out of gas) are not error but should still be treated as one + return V4TransactionErrors.parseErrorFromRawLog(rawLog) } else { null } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SystemSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SystemSupervisor.kt index d1f93e758..2b57febe8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SystemSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SystemSupervisor.kt @@ -90,16 +90,31 @@ internal class SystemSupervisor( } private fun retrieveMarketConfigs() { - val oldState = stateMachine.state - val url = helper.configs.configsUrl("markets") - if (url != null) { - helper.get(url, null, null) { _, response, httpCode, _ -> - if (helper.success(httpCode) && response != null) { - update( - // TODO, subaccountNumber required to refresh - stateMachine.configurations(response, null, helper.deploymentUri), - oldState, - ) + if (stateMachine.metadataService) { + val oldState = stateMachine.state + val url = helper.configs.metadataServiceInfo() + if (url != null) { + helper.post(url, null, null) { _, response, httpCode, _ -> + if (helper.success(httpCode) && response != null) { + update( + stateMachine.configurations(response, null, helper.deploymentUri), + oldState, + ) + } + } + } + } else { + val oldState = stateMachine.state + val url = helper.configs.configsUrl("markets") + if (url != null) { + helper.get(url, null, null) { _, response, httpCode, _ -> + if (helper.success(httpCode) && response != null) { + update( + // TODO, subaccountNumber required to refresh + stateMachine.configurations(response, null, helper.deploymentUri), + oldState, + ) + } } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/VaultSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/VaultSupervisor.kt index 998b16477..021e50d5e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/VaultSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/VaultSupervisor.kt @@ -1,26 +1,16 @@ package exchange.dydx.abacus.state.v2.supervisor import exchange.dydx.abacus.protocols.LocalTimerProtocol +import exchange.dydx.abacus.protocols.TransactionType import exchange.dydx.abacus.state.model.TradingStateMachine +import exchange.dydx.abacus.state.model.onAccountOwnerShares import exchange.dydx.abacus.state.model.onMegaVaultPnl import exchange.dydx.abacus.state.model.onVaultMarketPnls import exchange.dydx.abacus.state.model.onVaultMarketPositions +import exchange.dydx.abacus.state.model.onVaultTransferHistory import exchange.dydx.abacus.utils.AnalyticsUtils import exchange.dydx.abacus.utils.CoroutineTimer -import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS -import indexer.codegen.IndexerAssetPositionResponseObject -import indexer.codegen.IndexerMegavaultHistoricalPnlResponse -import indexer.codegen.IndexerMegavaultPositionResponse -import indexer.codegen.IndexerPerpetualPositionResponseObject -import indexer.codegen.IndexerPerpetualPositionStatus -import indexer.codegen.IndexerPnlTicksResponseObject -import indexer.codegen.IndexerPositionSide -import indexer.codegen.IndexerVaultHistoricalPnl -import indexer.codegen.IndexerVaultPosition -import indexer.codegen.IndexerVaultsHistoricalPnlResponse -import kotlinx.datetime.Instant -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import exchange.dydx.abacus.utils.Logger internal class VaultSupervisor( stateMachine: TradingStateMachine, @@ -29,8 +19,26 @@ internal class VaultSupervisor( internal val configs: VaultConfigs, ) : NetworkSupervisor(stateMachine, helper, analyticsUtils) { + var accountAddress: String? = null + set(value) { + if (value != accountAddress) { + if (indexerConnected) { + stopPollingIndexerData() + startPollingIndexerData(value) + } + if (validatorConnected) { + stopPollingValidatorData() + startPollingValidatorData(value) + } + stateMachine.internalState.vault = null + + field = value + } + } + companion object { - private const val POLLING_DURATION = 60.0 + private const val POLLING_DURATION = 20.0 + private const val MEGAVAULT_MODULE_ADDRESS = "dydx18tkxrnrkqc2t0lr3zxr5g6a4hdvqksylxqje4r" } private var indexerTimer: LocalTimerProtocol? = null @@ -41,57 +49,99 @@ internal class VaultSupervisor( } } + private var validatorTimer: LocalTimerProtocol? = null + set(value) { + if (field !== value) { + field?.cancel() + field = value + } + } + + fun refreshVaultAccount() { + if (accountAddress != null && validatorConnected) { + stopPollingValidatorData() + stopPollingIndexerData() + startPollingValidatorData(accountAddress) + startPollingIndexerData(accountAddress) + } + } + override fun didSetIndexerConnected(indexerConnected: Boolean) { super.didSetIndexerConnected(indexerConnected) + if (indexerConnected) { + startPollingIndexerData(accountAddress) + } else { + stopPollingIndexerData() + } + } + + override fun didSetValidatorConnected(validatorConnected: Boolean) { + super.didSetValidatorConnected(validatorConnected) + + if (validatorConnected) { + startPollingValidatorData(accountAddress) + } else { + stopPollingValidatorData() + } + } + + private fun startPollingIndexerData(accountAddress: String?) { if (!configs.retrieveVault) { return } - if (indexerConnected) { - val timer = helper.ioImplementations.timer ?: CoroutineTimer.instance - indexerTimer = timer.schedule(delay = 0.0, repeat = Companion.POLLING_DURATION) { - if (readyToConnect) { - retrieveMegaVaultPnl() - retrieveVaultMarketPnls() - retrieveVaultMarketPositions() + val timer = helper.ioImplementations.timer ?: CoroutineTimer.instance + indexerTimer = timer.schedule(delay = 1.0, repeat = Companion.POLLING_DURATION) { + if (readyToConnect) { + retrieveMegaVaultPnl() + retrieveVaultMarketPnls() + retrieveVaultMarketPositions() + if (accountAddress != null) { + retrieveTransferHistory(accountAddress) } - true // Repeat } - } else { - indexerTimer = null + true // Repeat + } + } + + private fun stopPollingIndexerData() { + indexerTimer = null + } + + private fun startPollingValidatorData(accountAddress: String?) { + if (!configs.retrieveVault) { + return + } + + val timer = helper.ioImplementations.timer ?: CoroutineTimer.instance + validatorTimer = timer.schedule(delay = 1.0, repeat = Companion.POLLING_DURATION) { + if (readyToConnect) { + if (accountAddress != null) { + retrieveAccountShares(accountAddress) + } + } + true // Repeat } } + private fun stopPollingValidatorData() { + validatorTimer = null + } + private fun retrieveMegaVaultPnl() { - if (configs.useMocks) { - val mock = IndexerMegavaultHistoricalPnlResponse( - megavaultPnl = arrayOf( - IndexerPnlTicksResponseObject( - equity = "10000.0", - totalPnl = "1000.0", - netTransfers = "0.0", - createdAt = Instant.fromEpochMilliseconds(1659465600000).toString(), - ), - IndexerPnlTicksResponseObject( - equity = "5000.0", - totalPnl = "500", - netTransfers = "0.0", - createdAt = Instant.fromEpochMilliseconds(1659379200000).toString(), - ), - ), - ) - stateMachine.onMegaVaultPnl(Json.encodeToString(mock)) - } else { - val url = helper.configs.publicApiUrl("vault/megavault/historicalPnl") - if (url != null) { - helper.get( - url = url, - params = null, - headers = null, - ) { _, response, httpCode, _ -> - if (helper.success(httpCode) && response != null) { - stateMachine.onMegaVaultPnl(response) + val url = helper.configs.publicApiUrl("vaultHistoricalPnl") + if (url != null) { + helper.get( + url = url, + params = mapOf("resolution" to "day"), + headers = null, + ) { _, response, httpCode, _ -> + if (helper.success(httpCode) && response != null) { + stateMachine.onMegaVaultPnl(response) + } else { + Logger.e { + "Failed to retrieve mega vault pnl: $httpCode, $response" } } } @@ -99,40 +149,18 @@ internal class VaultSupervisor( } private fun retrieveVaultMarketPnls() { - if (configs.useMocks) { - val btcHistory = IndexerVaultHistoricalPnl( - ticker = "BTC-USD", - historicalPnl = arrayOf( - IndexerPnlTicksResponseObject( - id = "1", - equity = "10500.0", - totalPnl = "500.0", - netTransfers = "0.0", - createdAt = Instant.fromEpochMilliseconds(1659465600000).toString(), - ), - IndexerPnlTicksResponseObject( - id = "2", - equity = "10000.0", - totalPnl = "0.0", - netTransfers = "0.0", - createdAt = Instant.fromEpochMilliseconds(1659379200000).toString(), - ), - ), - ) - val marketPnls = IndexerVaultsHistoricalPnlResponse( - vaultsPnl = arrayOf(btcHistory), - ) - stateMachine.onVaultMarketPnls(Json.encodeToString(marketPnls)) - } else { - val url = helper.configs.publicApiUrl("vault/vaults/historicalPnl") - if (url != null) { - helper.get( - url = url, - params = null, - headers = null, - ) { _, response, httpCode, _ -> - if (helper.success(httpCode) && response != null) { - stateMachine.onVaultMarketPnls(response) + val url = helper.configs.publicApiUrl("vaultMarketPnls") + if (url != null) { + helper.get( + url = url, + params = mapOf("resolution" to "day"), + headers = null, + ) { _, response, httpCode, _ -> + if (helper.success(httpCode) && response != null) { + stateMachine.onVaultMarketPnls(response) + } else { + Logger.e { + "Failed to retrieve vault market pnls: $httpCode, $response" } } } @@ -140,53 +168,62 @@ internal class VaultSupervisor( } private fun retrieveVaultMarketPositions() { - if (configs.useMocks) { - val btcPosition = IndexerVaultPosition( - ticker = "BTC-USD", - assetPosition = IndexerAssetPositionResponseObject( - symbol = "USDC", - side = IndexerPositionSide.SHORT, - size = "40000.0", - assetId = "0", - subaccountNumber = NUM_PARENT_SUBACCOUNTS, - ), - perpetualPosition = IndexerPerpetualPositionResponseObject( - market = "BTC-USD", - status = IndexerPerpetualPositionStatus.OPEN, - side = IndexerPositionSide.LONG, - size = "1.0", - maxSize = null, - entryPrice = "50000.0", - realizedPnl = null, - createdAt = "2023-08-01T00:00:00Z", - createdAtHeight = "1000", - sumOpen = null, - sumClose = null, - netFunding = null, - unrealizedPnl = "5000.0", - closedAt = null, - exitPrice = null, - subaccountNumber = NUM_PARENT_SUBACCOUNTS, + val url = helper.configs.publicApiUrl("vaultPositions") + if (url != null) { + helper.get( + url = url, + params = null, + headers = null, + ) { _, response, httpCode, _ -> + if (helper.success(httpCode) && response != null) { + stateMachine.onVaultMarketPositions(response) + } else { + Logger.e { + "Failed to retrieve vault market positions: $httpCode, $response" + } + } + } + } + } + + private fun retrieveTransferHistory(accountAddress: String) { + val url = helper.configs.publicApiUrl("transfers") + if (url != null) { + helper.get( + url = url, + params = mapOf( + "sourceAddress" to accountAddress, + "sourceSubaccountNumber" to "0", + "recipientAddress" to MEGAVAULT_MODULE_ADDRESS, + "recipientSubaccountNumber" to "0", ), - equity = "15000.0", - ) - val megaVaultPosition = IndexerMegavaultPositionResponse( - positions = arrayOf(btcPosition), - ) - stateMachine.onVaultMarketPositions(Json.encodeToString(megaVaultPosition)) - } else { - val url = helper.configs.publicApiUrl("vault/positions") - if (url != null) { - helper.get( - url = url, - params = null, - headers = null, - ) { _, response, httpCode, _ -> - if (helper.success(httpCode) && response != null) { - stateMachine.onVaultMarketPositions(response) + headers = null, + ) { _, response, httpCode, _ -> + if (helper.success(httpCode) && response != null) { + stateMachine.onVaultTransferHistory(response) + } else { + Logger.e { + "Failed to retrieve transfer history: $httpCode, $response" } } } } } + + private fun retrieveAccountShares(accountAddress: String) { + val payload = + helper.jsonEncoder.encode( + mapOf( + "address" to accountAddress, + ), + ) + helper.transaction(TransactionType.GetMegavaultOwnerShares, payload) { response -> + val error = helper.parseTransactionResponse(response) + if (error != null) { + Logger.e { "getMegavaultOwnerShares error: $error" } + } else { + stateMachine.onAccountOwnerShares(response) + } + } + } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt index e27c6139f..50201128e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt @@ -141,7 +141,7 @@ internal class AccountInputValidator( wallet: InternalWalletState, subaccountNumber: Int?, ): ValidationError? { - val isChildSubaccountForIsolatedMargin = subaccountNumber == null || subaccountNumber >= NUM_PARENT_SUBACCOUNTS + val isChildSubaccountForIsolatedMargin = subaccountNumber != null && subaccountNumber >= NUM_PARENT_SUBACCOUNTS val subaccount = wallet.account.subaccounts[subaccountNumber] val equity = subaccount?.calculated?.get(CalculationPeriod.current)?.equity @@ -171,7 +171,7 @@ internal class AccountInputValidator( ): Map? { val equity = parser.asDouble(parser.value(subaccount, "equity.current")) val subaccountNumber = parser.asInt(subaccount?.get("subaccountNumber")) - val isChildSubaccountForIsolatedMargin = subaccountNumber == null || subaccountNumber >= NUM_PARENT_SUBACCOUNTS + val isChildSubaccountForIsolatedMargin = subaccountNumber != null && subaccountNumber >= NUM_PARENT_SUBACCOUNTS return if (equity != null && equity > 0) { null diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt index 9c89ef2d1..86340fdba 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt @@ -290,6 +290,9 @@ internal class TradeAccountStateValidator( return false } val type = trade.type ?: return false + // does not apply to trigger/stop trades + if (type.isSlTp) return false + val price = if (type == OrderType.Market) { trade.marketOrder?.worstPrice } else { @@ -347,6 +350,7 @@ internal class TradeAccountStateValidator( ): Boolean { if (orders != null) { val type = parser.asString(trade["type"]) ?: return false + if (listOf("STOP_MARKET", "TAKE_PROFIT_MARKET", "STOP_LIMIT", "TAKE_PROFIT").contains(type)) return false val price = parser.asDouble( parser.value( trade, diff --git a/src/commonMain/kotlin/indexer/models/chain/OnChainAccountVaultResponse.kt b/src/commonMain/kotlin/indexer/models/chain/OnChainAccountVaultResponse.kt new file mode 100644 index 000000000..a5eae2427 --- /dev/null +++ b/src/commonMain/kotlin/indexer/models/chain/OnChainAccountVaultResponse.kt @@ -0,0 +1,27 @@ +package indexer.models.chain + +import kotlinx.serialization.Serializable +import kotlin.js.JsExport + +@JsExport +@Serializable +data class OnChainShareUnlock( + val shares: OnChainNumShares?, + val unlockBlockHeight: Double?, +) + +@JsExport +@Serializable +data class OnChainNumShares( + val numShares: Double?, +) + +@JsExport +@Serializable +data class OnChainAccountVaultResponse( + val address: String? = null, + val shares: OnChainNumShares? = null, + val shareUnlocks: Array? = null, + val equity: Double? = null, + val withdrawableEquity: Double? = null, +) diff --git a/src/commonMain/kotlin/indexer/models/chain/OnChainTransactionResponse.kt b/src/commonMain/kotlin/indexer/models/chain/OnChainTransactionResponse.kt new file mode 100644 index 000000000..5f6ffa9e4 --- /dev/null +++ b/src/commonMain/kotlin/indexer/models/chain/OnChainTransactionResponse.kt @@ -0,0 +1,72 @@ +package indexer.models.chain + +import exchange.dydx.abacus.protocols.asTypedObject +import exchange.dydx.abacus.utils.Parser +import exchange.dydx.abacus.utils.QUANTUM_MULTIPLIER +import kotlinx.serialization.Serializable + +// Define the structure of the error message +@Serializable +data class ChainError( + val message: String, + val line: Int? = null, + val column: Int? = null, + val stack: String? = null +) { + companion object { + val unknownError = ChainError(message = "An unknown error occurred", line = null, column = null, stack = null) + } +} + +@Serializable +data class OnChainTransactionErrorResponse( + val error: ChainError +) { + companion object { + private val parser = Parser() + + fun fromPayload(payload: String?): OnChainTransactionErrorResponse? { + return parser.asTypedObject(payload) + } + } +} + +// Define the structure of the success message +@Serializable +data class ChainEvent( + val type: String, + val attributes: List +) + +@Serializable +data class ChainEventAttribute( + val key: String, + val value: String +) + +@Serializable +data class OnChainTransactionSuccessResponse( + val height: Int? = null, + val hash: String? = null, + val code: Int? = null, + val tx: String, + val txIndex: Int? = null, + val gasUsed: String? = null, + val gasWanted: String? = null, + val events: List? = null, +) { + companion object { + private val parser = Parser() + + fun fromPayload(payload: String?): OnChainTransactionSuccessResponse? { + return parser.asTypedObject(payload) + } + } + + val actualWithdrawalAmount: Double? + get() { + val withdrawalEvent = events?.firstOrNull { it.type == "withdraw_from_megavault" } + val amountAttribute = withdrawalEvent?.attributes?.firstOrNull { it.key == "redeemed_quote_quantums" } + return parser.asDouble(parser.asDecimal(amountAttribute?.value)?.div(QUANTUM_MULTIPLIER)) + } +} diff --git a/src/commonMain/kotlin/indexer/models/chain/OnChainVaultDepositWithdrawSlippageResponse.kt b/src/commonMain/kotlin/indexer/models/chain/OnChainVaultDepositWithdrawSlippageResponse.kt new file mode 100644 index 000000000..417997290 --- /dev/null +++ b/src/commonMain/kotlin/indexer/models/chain/OnChainVaultDepositWithdrawSlippageResponse.kt @@ -0,0 +1,11 @@ +package indexer.models.chain + +import kotlinx.serialization.Serializable +import kotlin.js.JsExport + +@JsExport +@Serializable +data class OnChainVaultDepositWithdrawSlippageResponse( + val sharesToWithdraw: OnChainNumShares, + val expectedQuoteQuantums: Double, +) diff --git a/src/commonMain/kotlin/indexer/models/configs/ConfigsMarketAssetResponse.kt b/src/commonMain/kotlin/indexer/models/configs/ConfigsMarketAssetResponse.kt index 1c480ea34..9586fa3c4 100644 --- a/src/commonMain/kotlin/indexer/models/configs/ConfigsMarketAssetResponse.kt +++ b/src/commonMain/kotlin/indexer/models/configs/ConfigsMarketAssetResponse.kt @@ -1,6 +1,7 @@ package indexer.models.configs import exchange.dydx.abacus.utils.IList +import exchange.dydx.abacus.utils.IMap import kotlinx.serialization.Serializable /** @@ -14,3 +15,16 @@ data class ConfigsMarketAsset( val coinMarketCapsLink: String? = null, val tags: IList? = null, ) + +/** + * @description Asset from MetadataService Info response + */ +@Suppress("ConstructorParameterNaming") +@Serializable +data class ConfigsAssetMetadata( + val name: String, + val logo: String, + val urls: IMap, + val sector_tags: IList? = null, +// val exchanges: IList +) diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultAccountTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultAccountTests.kt index af1485438..647230825 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultAccountTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultAccountTests.kt @@ -4,6 +4,9 @@ import exchange.dydx.abacus.functional.vault.VaultAccountCalculator.calculateUse import indexer.codegen.IndexerTransferBetweenResponse import indexer.codegen.IndexerTransferResponseObject import indexer.codegen.IndexerTransferType +import indexer.models.chain.OnChainAccountVaultResponse +import indexer.models.chain.OnChainNumShares +import indexer.models.chain.OnChainShareUnlock import kollections.iListOf import kotlinx.datetime.Instant import kotlin.test.Test @@ -14,10 +17,10 @@ class VaultAccountTests { @Test fun calculateUserVaultInfo_basic() { - val vaultInfo = AccountVaultResponse( + val vaultInfo = OnChainAccountVaultResponse( address = "0x123", - shares = NumShares(numShares = 100.0), - shareUnlocks = arrayOf(ShareUnlock(unlockBlockHeight = 0.0, shares = NumShares(numShares = 50.0))), + shares = OnChainNumShares(numShares = 100.0), + shareUnlocks = arrayOf(OnChainShareUnlock(unlockBlockHeight = 0.0, shares = OnChainNumShares(numShares = 50.0))), equity = 10000.0 * 1_000_000, withdrawableEquity = 5000.0 * 1_000_000, ) @@ -31,12 +34,14 @@ class VaultAccountTests { createdAt = Instant.fromEpochMilliseconds(1659465600000).toString(), size = "6000.0", type = IndexerTransferType.TRANSFER_OUT, + transactionHash = "tx1", ), IndexerTransferResponseObject( id = "2", createdAt = Instant.fromEpochMilliseconds(1659552000000).toString(), size = "2000.0", type = IndexerTransferType.TRANSFER_IN, + transactionHash = "tx2", ), ), ) @@ -56,14 +61,71 @@ class VaultAccountTests { amountUsdc = 6000.0, type = VaultTransferType.DEPOSIT, id = "1", + transactionHash = "tx1", ), VaultTransfer( timestampMs = 1659552000000.0, amountUsdc = 2000.0, type = VaultTransferType.WITHDRAWAL, id = "2", + transactionHash = "tx2", ), ), + vaultShareUnlocks = iListOf(VaultShareUnlock(unlockBlockHeight = 0.0, amountUsdc = 5000.0)), + ) + + assertEquals(expectedVaultAccount, vaultAccount) + } + + @Test + fun calculateUserVaultInfo_empty() { + val vaultTransfers = IndexerTransferBetweenResponse( + totalResults = 2, + totalNetTransfers = "-500.0", + transfersSubset = arrayOf( + IndexerTransferResponseObject( + id = "1", + createdAt = Instant.fromEpochMilliseconds(1659465600000).toString(), + size = "6000.0", + type = IndexerTransferType.TRANSFER_OUT, + transactionHash = "tx1", + ), + IndexerTransferResponseObject( + id = "2", + createdAt = Instant.fromEpochMilliseconds(1659552000000).toString(), + size = "6500.0", + type = IndexerTransferType.TRANSFER_IN, + transactionHash = "tx2", + ), + ), + ) + + val vaultAccount = calculateUserVaultInfo(null, vaultTransfers) + + val expectedVaultAccount = VaultAccount( + balanceUsdc = 0.0, + withdrawableUsdc = 0.0, + allTimeReturnUsdc = 500.0, + totalVaultTransfersCount = 2, + balanceShares = 0.0, + lockedShares = 0.0, + vaultTransfers = iListOf( + VaultTransfer( + timestampMs = 1659465600000.0, + amountUsdc = 6000.0, + type = VaultTransferType.DEPOSIT, + id = "1", + transactionHash = "tx1", + ), + VaultTransfer( + timestampMs = 1659552000000.0, + amountUsdc = 6500.0, + type = VaultTransferType.WITHDRAWAL, + id = "2", + transactionHash = "tx2", + ), + ), + vaultShareUnlocks = null, ) assertEquals(expectedVaultAccount, vaultAccount) diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultFormTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultFormTests.kt index a29f0cfc4..9aecd973b 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultFormTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultFormTests.kt @@ -1,6 +1,8 @@ package exchange.dydx.abacus.functional.vault import exchange.dydx.abacus.output.input.ValidationError +import indexer.models.chain.OnChainNumShares +import indexer.models.chain.OnChainVaultDepositWithdrawSlippageResponse import kollections.iListOf import kollections.toIList import kotlin.test.Test @@ -17,6 +19,110 @@ class VaultFormTests { totalVaultTransfersCount = null, withdrawableUsdc = withdrawableUsdc, balanceUsdc = balanceUsdc, + vaultShareUnlocks = null, + ) + } + + @Test + fun testShareCalculation() { + assertEquals( + VaultDepositWithdrawFormValidator.calculateSharesToWithdraw( + vaultAccount = makeVaultAccount( + balanceUsdc = 500.0, + balanceShares = 250.0, + withdrawableUsdc = 500.0, + ), + amount = 100.0, + ), + 50.0, + ) + } + + @Test + fun testShareCalculationCloseToZero() { + assertEquals( + VaultDepositWithdrawFormValidator.calculateSharesToWithdraw( + vaultAccount = makeVaultAccount( + balanceUsdc = 600.0, + balanceShares = 600000.0, + withdrawableUsdc = 500.0, + ), + amount = 500.0, + ), + 500000.0, + ) + + assertEquals( + VaultDepositWithdrawFormValidator.calculateSharesToWithdraw( + vaultAccount = makeVaultAccount( + balanceUsdc = 600.0, + balanceShares = 600000.0, + withdrawableUsdc = 500.0, + ), + amount = 499.02, + ), + 499020.0, + ) + + assertEquals( + VaultDepositWithdrawFormValidator.calculateSharesToWithdraw( + vaultAccount = makeVaultAccount( + balanceUsdc = 600.0, + balanceShares = 600000.0, + withdrawableUsdc = 500.0, + ), + amount = 499.07, + ), + 499070.0, + ) + + assertEquals( + VaultDepositWithdrawFormValidator.calculateSharesToWithdraw( + vaultAccount = makeVaultAccount( + balanceUsdc = 600.0, + balanceShares = 600000.0, + withdrawableUsdc = 500.0, + ), + amount = 499.02, + ), + 499020.0, + ) + + assertEquals( + VaultDepositWithdrawFormValidator.calculateSharesToWithdraw( + vaultAccount = makeVaultAccount( + balanceUsdc = 600.0, + balanceShares = 600000.0, + withdrawableUsdc = 500.0, + ), + amount = 499.989, + ), + // actually off by one because of double precision + cast to long + 499988.0, + ) + + assertEquals( + VaultDepositWithdrawFormValidator.calculateSharesToWithdraw( + vaultAccount = makeVaultAccount( + balanceUsdc = 600.0, + balanceShares = 600000.0, + withdrawableUsdc = 500.0, + ), + amount = 499.99, + ), + 500000.0, + ) + + assertEquals( + VaultDepositWithdrawFormValidator.calculateSharesToWithdraw( + vaultAccount = makeVaultAccount( + balanceUsdc = 600.0, + balanceShares = 600000.0, + withdrawableUsdc = 500.0, + ), + amount = 499.992, + ), + 500000.0, ) } @@ -27,6 +133,7 @@ class VaultFormTests { action = VaultFormAction.DEPOSIT, amount = 100.0, acknowledgedSlippage = true, + acknowledgedTerms = true, inConfirmationStep = true, ), accountData = VaultFormAccountData( @@ -54,9 +161,11 @@ class VaultFormTests { ), summaryData = VaultFormSummaryData( needSlippageAck = false, + needTermsAck = true, marginUsage = 0.5263157894736843, freeCollateral = 900.0, vaultBalance = 600.0, + withdrawableVaultBalance = 600.0, estimatedSlippage = 0.0, estimatedAmountReceived = null, ), @@ -72,6 +181,7 @@ class VaultFormTests { action = VaultFormAction.WITHDRAW, amount = 100.0, acknowledgedSlippage = true, + acknowledgedTerms = false, inConfirmationStep = true, ), accountData = VaultFormAccountData( @@ -84,8 +194,8 @@ class VaultFormTests { withdrawableUsdc = 500.0, balanceShares = 500.0, ), - slippageResponse = VaultDepositWithdrawSlippageResponse( - sharesToWithdraw = NumShares(numShares = 100.0), + slippageResponse = OnChainVaultDepositWithdrawSlippageResponse( + sharesToWithdraw = OnChainNumShares(numShares = 100.0), expectedQuoteQuantums = 98.0 * 1_000_000, ), ) @@ -93,7 +203,7 @@ class VaultFormTests { assertEquals( VaultFormValidationResult( errors = iListOf( - VaultFormValidationErrors.slippageTooHigh(0.02), + VaultFormValidationErrors().slippageTooHigh(0.02), ), submissionData = VaultDepositWithdrawSubmissionData( deposit = null, @@ -104,10 +214,12 @@ class VaultFormTests { ), ), summaryData = VaultFormSummaryData( - needSlippageAck = true, + needSlippageAck = false, + needTermsAck = false, marginUsage = 0.4766444232602478, freeCollateral = 1098.0, vaultBalance = 400.0, + withdrawableVaultBalance = 400.0, estimatedSlippage = 0.020000000000000018, estimatedAmountReceived = 98.0, ), @@ -124,6 +236,7 @@ class VaultFormTests { action = VaultFormAction.WITHDRAW, amount = 100.0, acknowledgedSlippage = true, + acknowledgedTerms = false, inConfirmationStep = true, ), accountData = VaultFormAccountData( @@ -136,8 +249,8 @@ class VaultFormTests { withdrawableUsdc = 500.0, balanceShares = 500.0, ), - slippageResponse = VaultDepositWithdrawSlippageResponse( - sharesToWithdraw = NumShares(numShares = 120.0), + slippageResponse = OnChainVaultDepositWithdrawSlippageResponse( + sharesToWithdraw = OnChainNumShares(numShares = 120.0), expectedQuoteQuantums = 98.0 * 1_000_000, ), ) @@ -145,15 +258,17 @@ class VaultFormTests { assertEquals( VaultFormValidationResult( errors = iListOf( - VaultFormValidationErrors.slippageResponseWrongShares(), - VaultFormValidationErrors.slippageTooHigh(0.02), + VaultFormValidationErrors().slippageResponseWrongShares(), + VaultFormValidationErrors().slippageTooHigh(0.02), ), submissionData = null, summaryData = VaultFormSummaryData( - needSlippageAck = true, + needSlippageAck = false, + needTermsAck = false, marginUsage = 0.4766444232602478, freeCollateral = 1098.0, vaultBalance = 400.0, + withdrawableVaultBalance = 400.0, estimatedSlippage = 0.020000000000000018, // unfortunate precision issues with direct equality checks estimatedAmountReceived = 98.0, ), @@ -169,6 +284,7 @@ class VaultFormTests { action = VaultFormAction.WITHDRAW, amount = 600.0, acknowledgedSlippage = false, + acknowledgedTerms = false, inConfirmationStep = true, ), accountData = VaultFormAccountData( @@ -181,8 +297,8 @@ class VaultFormTests { withdrawableUsdc = 500.0, balanceShares = 500.0, ), - slippageResponse = VaultDepositWithdrawSlippageResponse( - sharesToWithdraw = NumShares(numShares = 600.0), + slippageResponse = OnChainVaultDepositWithdrawSlippageResponse( + sharesToWithdraw = OnChainNumShares(numShares = 600.0), expectedQuoteQuantums = 500.0 * 1_000_000, ), ) @@ -190,16 +306,18 @@ class VaultFormTests { assertEquals( VaultFormValidationResult( errors = iListOf( - VaultFormValidationErrors.withdrawTooHigh(), - VaultFormValidationErrors.slippageTooHigh(0.166666), - VaultFormValidationErrors.mustAckSlippage(), + VaultFormValidationErrors().withdrawTooHigh(), + VaultFormValidationErrors().slippageTooHigh(0.166666), + VaultFormValidationErrors().mustAckSlippage(), ), submissionData = null, summaryData = VaultFormSummaryData( needSlippageAck = true, + needTermsAck = false, marginUsage = 0.4, freeCollateral = 1500.0, vaultBalance = -100.0, + withdrawableVaultBalance = -100.0, estimatedSlippage = 0.16666666666666663, estimatedAmountReceived = 500.0, ), @@ -207,4 +325,293 @@ class VaultFormTests { result, ) } + + @Test + fun testLowDeposit() { + val result = VaultDepositWithdrawFormValidator.validateVaultForm( + formData = VaultFormData( + action = VaultFormAction.DEPOSIT, + amount = 10.0, + acknowledgedSlippage = false, + acknowledgedTerms = false, + inConfirmationStep = false, + ), + accountData = VaultFormAccountData( + marginUsage = 0.5, + freeCollateral = 1000.0, + canViewAccount = true, + ), + vaultAccount = makeVaultAccount( + balanceUsdc = 1000.0, + withdrawableUsdc = 500.0, + balanceShares = 500.0, + ), + slippageResponse = null, + ) + + assertEquals( + VaultFormValidationResult( + errors = iListOf( + VaultFormValidationErrors().depositTooLow(), + ), + submissionData = null, + summaryData = VaultFormSummaryData( + needSlippageAck = false, + needTermsAck = false, + marginUsage = 0.5025125628140703, + freeCollateral = 990.0, + vaultBalance = 1010.0, + withdrawableVaultBalance = 510.0, + estimatedSlippage = 0.0, + estimatedAmountReceived = null, + ), + ), + result, + ) + } + + @Test + fun testDepositWithNoAccount() { + val result = VaultDepositWithdrawFormValidator.validateVaultForm( + formData = VaultFormData( + action = VaultFormAction.DEPOSIT, + amount = 100.0, + acknowledgedSlippage = true, + acknowledgedTerms = true, + inConfirmationStep = true, + ), + accountData = VaultFormAccountData( + marginUsage = 0.5, + freeCollateral = 1000.0, + canViewAccount = true, + ), + // null vault account is expected on first deposit + vaultAccount = null, + slippageResponse = null, + ) + + assertEquals( + VaultFormValidationResult( + errors = listOf().toIList(), + submissionData = VaultDepositWithdrawSubmissionData( + deposit = VaultDepositData( + subaccountFrom = "0", + amount = 100.0, + ), + withdraw = null, + ), + summaryData = VaultFormSummaryData( + needSlippageAck = false, + needTermsAck = true, + marginUsage = 0.5263157894736843, + freeCollateral = 900.0, + vaultBalance = 100.0, + withdrawableVaultBalance = 100.0, + estimatedSlippage = 0.0, + estimatedAmountReceived = null, + ), + ), + result, + ) + } + + @Test + fun testLowWithdraw() { + val result = VaultDepositWithdrawFormValidator.validateVaultForm( + formData = VaultFormData( + action = VaultFormAction.WITHDRAW, + amount = 10.0, + acknowledgedSlippage = false, + acknowledgedTerms = false, + inConfirmationStep = false, + ), + accountData = VaultFormAccountData( + marginUsage = 0.5, + freeCollateral = 1000.0, + canViewAccount = true, + ), + vaultAccount = makeVaultAccount( + balanceUsdc = 1000.0, + withdrawableUsdc = 500.0, + balanceShares = 500.0, + ), + slippageResponse = OnChainVaultDepositWithdrawSlippageResponse( + sharesToWithdraw = OnChainNumShares(numShares = 5.0), + expectedQuoteQuantums = 10.0 * 1_000_000, + ), + ) + + assertEquals( + VaultFormValidationResult( + errors = iListOf( + VaultFormValidationErrors().withdrawTooLow(), + ), + submissionData = null, + summaryData = VaultFormSummaryData( + needSlippageAck = false, + needTermsAck = false, + marginUsage = 0.49751243781094523, + freeCollateral = 1010.0, + vaultBalance = 990.0, + withdrawableVaultBalance = 490.0, + estimatedSlippage = 0.0, + estimatedAmountReceived = 10.0, + ), + ), + result, + ) + } + + @Test + fun testLowWithdrawFull() { + val result = VaultDepositWithdrawFormValidator.validateVaultForm( + formData = VaultFormData( + action = VaultFormAction.WITHDRAW, + amount = 10.0, + acknowledgedSlippage = false, + acknowledgedTerms = false, + inConfirmationStep = false, + ), + accountData = VaultFormAccountData( + marginUsage = 0.5, + freeCollateral = 1000.0, + canViewAccount = true, + ), + vaultAccount = makeVaultAccount( + balanceUsdc = 1000.0, + withdrawableUsdc = 10.0, + balanceShares = 500.0, + ), + slippageResponse = OnChainVaultDepositWithdrawSlippageResponse( + sharesToWithdraw = OnChainNumShares(numShares = 5.0), + expectedQuoteQuantums = 10.0 * 1_000_000, + ), + ) + + assertEquals( + VaultFormValidationResult( + errors = listOf().toIList(), + submissionData = VaultDepositWithdrawSubmissionData( + withdraw = VaultWithdrawData( + subaccountTo = "0", + shares = 5.0, + minAmount = 9.90, + ), + deposit = null, + ), + summaryData = VaultFormSummaryData( + needSlippageAck = false, + needTermsAck = false, + marginUsage = 0.49751243781094523, + freeCollateral = 1010.0, + vaultBalance = 990.0, + withdrawableVaultBalance = 0.0, + estimatedSlippage = 0.0, + estimatedAmountReceived = 10.0, + ), + ), + result, + ) + } + + @Test + fun testLowWithdrawNonFull() { + val result = VaultDepositWithdrawFormValidator.validateVaultForm( + formData = VaultFormData( + action = VaultFormAction.WITHDRAW, + amount = 6.0, + acknowledgedSlippage = false, + acknowledgedTerms = false, + inConfirmationStep = false, + ), + accountData = VaultFormAccountData( + marginUsage = 0.5, + freeCollateral = 1000.0, + canViewAccount = true, + ), + vaultAccount = makeVaultAccount( + balanceUsdc = 1000.0, + withdrawableUsdc = 10.0, + balanceShares = 500.0, + ), + slippageResponse = OnChainVaultDepositWithdrawSlippageResponse( + sharesToWithdraw = OnChainNumShares(numShares = 3.0), + expectedQuoteQuantums = 6.0 * 1_000_000, + ), + ) + + assertEquals( + VaultFormValidationResult( + errors = iListOf( + VaultFormValidationErrors().withdrawTooLow(), + ), + submissionData = null, + summaryData = VaultFormSummaryData( + needSlippageAck = false, + needTermsAck = false, + marginUsage = 0.4985044865403788, + freeCollateral = 1006.0, + vaultBalance = 994.0, + withdrawableVaultBalance = 4.0, + estimatedSlippage = 0.0, + estimatedAmountReceived = 6.0, + ), + ), + result, + ) + } + + @Test + fun testValidHighSlippageWithdrawWithAck() { + val result = VaultDepositWithdrawFormValidator.validateVaultForm( + formData = VaultFormData( + action = VaultFormAction.WITHDRAW, + amount = 500.0, + acknowledgedSlippage = true, + acknowledgedTerms = false, + inConfirmationStep = true, + ), + accountData = VaultFormAccountData( + marginUsage = 0.5, + freeCollateral = 1000.0, + canViewAccount = true, + ), + vaultAccount = makeVaultAccount( + balanceUsdc = 500.0, + withdrawableUsdc = 500.0, + balanceShares = 500.0, + ), + slippageResponse = OnChainVaultDepositWithdrawSlippageResponse( + sharesToWithdraw = OnChainNumShares(numShares = 500.0), + expectedQuoteQuantums = 400.0 * 1_000_000, + ), + ) + + assertEquals( + VaultFormValidationResult( + errors = iListOf( + VaultFormValidationErrors().slippageTooHigh(0.1999), + ), + submissionData = VaultDepositWithdrawSubmissionData( + deposit = null, + withdraw = VaultWithdrawData( + subaccountTo = "0", + shares = 500.0, + minAmount = 396.0, + ), + ), + summaryData = VaultFormSummaryData( + needSlippageAck = true, + needTermsAck = false, + marginUsage = 0.41666666666666663, + freeCollateral = 1400.0, + vaultBalance = 0.0, + withdrawableVaultBalance = 0.0, + estimatedSlippage = 0.19999999999999996, + estimatedAmountReceived = 400.0, + ), + ), + result, + ) + } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultTests.kt index ef5b8c659..1c85ce5c8 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/functional/vault/VaultTests.kt @@ -1,17 +1,21 @@ package exchange.dydx.abacus.functional.vault import exchange.dydx.abacus.functional.vault.VaultCalculator.calculateVaultPosition +import exchange.dydx.abacus.functional.vault.VaultCalculator.calculateVaultPositions import exchange.dydx.abacus.functional.vault.VaultCalculator.calculateVaultSummary import exchange.dydx.abacus.output.PerpetualMarket import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS +import exchange.dydx.abacus.utils.iMapOf import indexer.codegen.IndexerAssetPositionResponseObject import indexer.codegen.IndexerMegavaultHistoricalPnlResponse +import indexer.codegen.IndexerMegavaultPositionResponse import indexer.codegen.IndexerPerpetualPositionResponseObject import indexer.codegen.IndexerPerpetualPositionStatus import indexer.codegen.IndexerPnlTicksResponseObject import indexer.codegen.IndexerPositionSide import indexer.codegen.IndexerVaultHistoricalPnl import indexer.codegen.IndexerVaultPosition +import indexer.codegen.IndexerVaultsHistoricalPnlResponse import kollections.iListOf import kotlinx.datetime.Instant import kotlin.test.Test @@ -44,7 +48,7 @@ class VaultTests { val expectedVaultDetails = VaultDetails( totalValue = 10000.0, - thirtyDayReturnPercent = 0.1, + thirtyDayReturnPercent = 0.1 * 365, history = iListOf( VaultHistoryEntry( date = 1659465600000.0, @@ -119,7 +123,7 @@ class VaultTests { val vaultDetails = calculateVaultSummary(historicalPnl) assertNotNull(vaultDetails) - assertEquals(0.05263157894736842, vaultDetails.thirtyDayReturnPercent) + assertEquals(0.6403508771929824, vaultDetails.thirtyDayReturnPercent) } @Test @@ -207,4 +211,115 @@ class VaultTests { assertEquals(expectedVaultPosition, vaultPosition) } + + @Test + fun shouldCalculateVaultPositionsCorrectly() { + val position = IndexerVaultPosition( + ticker = "BTC-USD", + assetPosition = IndexerAssetPositionResponseObject( + symbol = "USDC", + side = IndexerPositionSide.SHORT, + size = "40000.0", + assetId = "0", + subaccountNumber = NUM_PARENT_SUBACCOUNTS, + ), + perpetualPosition = IndexerPerpetualPositionResponseObject( + market = "BTC-USD", + status = IndexerPerpetualPositionStatus.OPEN, + side = IndexerPositionSide.LONG, + size = "1.0", + maxSize = null, + entryPrice = "50000.0", + realizedPnl = null, + createdAt = "2023-08-01T00:00:00Z", + createdAtHeight = "1000", + sumOpen = null, + sumClose = null, + netFunding = null, + unrealizedPnl = "5000.0", + closedAt = null, + exitPrice = null, + subaccountNumber = NUM_PARENT_SUBACCOUNTS, + ), + equity = "15000.0", + ) + + val history = IndexerVaultHistoricalPnl( + ticker = "BTC-USD", + historicalPnl = arrayOf( + IndexerPnlTicksResponseObject( + id = "1", + equity = "10500.0", + totalPnl = "500.0", + netTransfers = "0.0", + createdAt = Instant.fromEpochMilliseconds(1659465600000).toString(), + ), + IndexerPnlTicksResponseObject( + id = "2", + equity = "10000.0", + totalPnl = "0.0", + netTransfers = "0.0", + createdAt = Instant.fromEpochMilliseconds(1659379200000).toString(), + ), + ), + ) + + val market = PerpetualMarket( + id = "BTC-USD", + assetId = "0", + market = "BTC-USD", + displayId = null, + oraclePrice = 55000.0, + marketCaps = null, + priceChange24H = null, + priceChange24HPercent = null, + status = null, + configs = null, + perpetual = null, + ) + + val vaultPositions = calculateVaultPositions( + IndexerMegavaultPositionResponse(positions = arrayOf(position)), + IndexerVaultsHistoricalPnlResponse(vaultsPnl = arrayOf(history)), + iMapOf("BTC-USD" to market), + 21000.0, + ) + + val expectedVaultPosition = VaultPosition( + marketId = "BTC-USD", + marginUsdc = 15000.0, + currentLeverageMultiple = 55.0 / 15.0, + currentPosition = CurrentPosition( + asset = 1.0, + usdc = 55000.0, + ), + thirtyDayPnl = ThirtyDayPnl( + percent = 0.05, + absolute = 500.0, + sparklinePoints = iListOf(0.0, 500.0), + ), + ) + + assertEquals( + iListOf( + expectedVaultPosition, + VaultPosition( + marketId = "USDC-USD", + marginUsdc = 6000.0, + equityUsdc = 6000.0, + currentLeverageMultiple = 1.0, + currentPosition = CurrentPosition( + asset = 6000.0, + usdc = 6000.0, + ), + thirtyDayPnl = ThirtyDayPnl( + percent = 0.0, + absolute = 0.0, + sparklinePoints = null, + ), + ), + ), + vaultPositions?.positions, + ) + } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/utils/MarketIdUtilsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/utils/MarketIdUtilsTests.kt index 2c3d0459f..fac685f4f 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/utils/MarketIdUtilsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/utils/MarketIdUtilsTests.kt @@ -31,6 +31,6 @@ class MarketIdUtilsTests { fun getAssetIdLong() { val marketId = "AART,raydium,F3nefJBcejYbtdREjui1T9DPh5dBgpkKq7u2GAAMXs5B-USD" val assetId = MarketId.getAssetId(marketId) - assertEquals("AART", assetId) + assertEquals("AART,raydium,F3nefJBcejYbtdREjui1T9DPh5dBgpkKq7u2GAAMXs5B", assetId) } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4VaultTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4VaultTests.kt index cd1c1744b..36a91fe5f 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4VaultTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4VaultTests.kt @@ -27,7 +27,7 @@ class V4VaultTests : V4BaseTests() { val vaultDetails = vault?.details assertEquals(10000.0, vaultDetails?.totalValue) - assertEquals(0.1, vaultDetails?.thirtyDayReturnPercent) + assertEquals(0.1 * 365, vaultDetails?.thirtyDayReturnPercent) assertEquals(2, vaultDetails?.history?.size) assertEquals(1000.0, vaultDetails?.history?.get(0)?.totalPnl) assertEquals(500.0, vaultDetails?.history?.get(1)?.totalPnl) @@ -42,7 +42,7 @@ class V4VaultTests : V4BaseTests() { private fun testMegaVaultPnlReceived() { perp.rest( - url = AbUrl.fromString("$testRestUrl/v4/vault/megavault/historicalPnl"), + url = AbUrl.fromString("$testRestUrl/v4/vault/v1/megavault/historicalPnl"), payload = mock.vaultMocks.megaVaultPnlMocks, subaccountNumber = 0, height = null, @@ -50,7 +50,7 @@ class V4VaultTests : V4BaseTests() { val vault = perp.internalState.vault assertEquals(10000.0, vault?.details?.totalValue) - assertEquals(0.1, vault?.details?.thirtyDayReturnPercent) + assertEquals(0.1 * 365, vault?.details?.thirtyDayReturnPercent) assertEquals(2, vault?.details?.history?.size) assertEquals(1000.0, vault?.details?.history?.get(0)?.totalPnl) assertEquals(500.0, vault?.details?.history?.get(1)?.totalPnl) @@ -58,7 +58,7 @@ class V4VaultTests : V4BaseTests() { private fun testVaultMarketPnlsReceived() { perp.rest( - url = AbUrl.fromString("$testRestUrl/v4/vault/vaults/historicalPnl"), + url = AbUrl.fromString("$testRestUrl/v4/vault/v1/vaults/historicalPnl"), payload = mock.vaultMocks.vaultMarketPnlsMocks, subaccountNumber = 0, height = null, @@ -74,7 +74,7 @@ class V4VaultTests : V4BaseTests() { private fun testVaultMarketPositionsReceived() { perp.rest( - url = AbUrl.fromString("$testRestUrl/v4/vault/positions"), + url = AbUrl.fromString("$testRestUrl/v4/vault/v1/megavault/positions"), payload = mock.vaultMocks.vaultMarketPositionsMocks, subaccountNumber = 0, height = null, diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessorTests.kt index cc253be9d..8ddf8f80f 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessorTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/router/skip/SkipRouteProcessorTests.kt @@ -7,6 +7,7 @@ import exchange.dydx.abacus.utils.DEFAULT_GAS_PRICE import exchange.dydx.abacus.utils.JsonEncoder import exchange.dydx.abacus.utils.Parser import exchange.dydx.abacus.utils.toJsonArray +import exchange.dydx.abacus.utils.toJsonElement import kotlin.test.Test import kotlin.test.assertEquals @@ -54,17 +55,20 @@ class SkipRouteProcessorTests { val expected = mapOf( "toAmountUSD" to 1498.18, "toAmount" to 1499.8, + "estimatedRouteDurationSeconds" to 25, "bridgeFee" to .2, "slippage" to "1", "requestPayload" to mapOf( - "data" to "mock-encoded-solana-tx", "fromChainId" to "solana", "fromAddress" to "98bVPZQCHZmCt9v3ni9kwtjKgLuzHBpstQkdPyAucBNx", "toChainId" to "noble-1", "toAddress" to "uusdc", + "data" to "mock-encoded-solana-tx", ), ) - assertEquals(expected, result) +// Sometimes assertEquals behaves strangely where it insists on comparing the memory address. +// When this happens I need to serialize the objects first. This behavior appear to be non-deterministic. + assertEquals(expected.toJsonElement(), result.toJsonElement()) } /** diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/extensions/TradingStateMachine+TestUtils.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/extensions/TradingStateMachine+TestUtils.kt index a15408c58..4f8bcbd9a 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/extensions/TradingStateMachine+TestUtils.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/extensions/TradingStateMachine+TestUtils.kt @@ -596,15 +596,15 @@ fun TradingStateMachine.rest( changes = transfers(payload, subaccountNumber) } - "/v4/vault/megavault/historicalPnl" -> { + "/v4/vault/v1/megavault/historicalPnl" -> { changes = onMegaVaultPnl(payload) } - "/v4/vault/positions" -> { + "/v4/vault/v1/megavault/positions" -> { changes = onVaultMarketPositions(payload) } - "/v4/vault/vaults/historicalPnl" -> { + "/v4/vault/v1/vaults/historicalPnl" -> { changes = onVaultMarketPnls(payload) } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/AbacusMockData.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/AbacusMockData.kt index fb9792217..c8ce60ded 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/AbacusMockData.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/AbacusMockData.kt @@ -68,6 +68,7 @@ class AbacusMockData { faucet = null, squid = null, skip = null, + metadataService = null, nobleValidator = null, geo = null, ), @@ -82,6 +83,7 @@ class AbacusMockData { blogs = "https://www.dydx.foundation/blog", help = "https://help.dydx.exchange/", vaultLearnMore = "https://help.dydx.exchange/", + vaultTos = "https://help.dydx.exchange/", launchIncentive = "https://dydx.exchange/v4-launch-incentive", statusPage = "https://status.v4testnet.dydx.exchange/", withdrawalGateLearnMore = "https://help.dydx.exchange/en/articles/8981384-withdrawals-on-dydx-chain#h_23e97bc665", diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 2ed88abc8..c4b934c2e 100644 --- a/v4_abacus.podspec +++ b/v4_abacus.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'v4_abacus' - spec.version = '1.11.24' + spec.version = '1.13.4' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''