Skip to content

Commit

Permalink
Add V4Environment and Documentation to the graph. Pre-req for moving …
Browse files Browse the repository at this point in the history
…state-machine components into the graph.
  • Loading branch information
prashanDYDX committed May 16, 2024
1 parent 465f220 commit 45d155e
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 2 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ allprojects {
}

group = "exchange.dydx.abacus"
version = "1.7.19"
version = "1.7.20"

repositories {
google()
Expand Down
16 changes: 16 additions & 0 deletions src/commonMain/kotlin/exchange.dydx.abacus/di/AbacusComponent.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package exchange.dydx.abacus.di

import exchange.dydx.abacus.output.Documentation
import exchange.dydx.abacus.protocols.DataNotificationProtocol
import exchange.dydx.abacus.protocols.PresentationProtocol
import exchange.dydx.abacus.protocols.StateNotificationProtocol
import exchange.dydx.abacus.state.manager.DocumentationLoader
import exchange.dydx.abacus.state.manager.EnvironmentLoader
import exchange.dydx.abacus.state.manager.V4Environment
import exchange.dydx.abacus.state.v2.manager.AsyncAbacusStateManagerV2
import exchange.dydx.abacus.state.v2.supervisor.AppConfigsV2
import exchange.dydx.abacus.utils.IOImplementations
Expand All @@ -18,6 +22,8 @@ import kotlin.js.JsExport
// kotlin-inject handles qualifiers via typealiases (though Dagger-style @Qualifier annotations are coming soon)
typealias DeploymentUri = String
typealias Deployment = String // MAINNET, TESTNET, DEV
typealias EnvironmentId = String // final computed env id
typealias EnvironmentIdParameter = String // env id passed in by clients

@Scope
@Target(CLASS, FUNCTION, PROPERTY_GETTER)
Expand All @@ -34,6 +40,7 @@ object AbacusFactory {
stateNotification: StateNotificationProtocol? = null,
dataNotification: DataNotificationProtocol? = null,
presentationProtocol: PresentationProtocol? = null,
environmentIdParameter: EnvironmentIdParameter? = null,
): AbacusComponent = AbacusComponent::class.create(
deploymentUri,
deployment,
Expand All @@ -43,6 +50,7 @@ object AbacusFactory {
stateNotification,
dataNotification,
presentationProtocol,
environmentIdParameter,
)
}

Expand All @@ -58,6 +66,14 @@ abstract class AbacusComponent(
@get:Provides protected val stateNotification: StateNotificationProtocol?,
@get:Provides protected val dataNotification: DataNotificationProtocol?,
@get:Provides protected val presentationProtocol: PresentationProtocol?,
@get:Provides protected val environmentIdParameter: EnvironmentIdParameter?,
) {
abstract val stateManager: AsyncAbacusStateManagerV2
abstract val documentation: Documentation?

@Provides protected fun provideV4Environment(environmentLoader: EnvironmentLoader): V4Environment =
environmentLoader.envAndAppSettings.run { environments.first { it.id == environmentId } }

@Provides protected fun provideDocumentation(documentationLoader: DocumentationLoader): Documentation? =
documentationLoader.documentation
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package exchange.dydx.abacus.state.manager

import exchange.dydx.abacus.di.DeploymentUri
import exchange.dydx.abacus.protocols.FileLocation
import exchange.dydx.abacus.protocols.readCachedTextFile
import exchange.dydx.abacus.state.v2.supervisor.AppConfigsV2
import exchange.dydx.abacus.utils.IOImplementations
import me.tatarka.inject.annotations.Inject

@Inject
class ConfigFileLoader(
private val deploymentUri: DeploymentUri,
private val appConfigs: AppConfigsV2,
private val ioImplementations: IOImplementations,
) {
fun <T> load(configFile: ConfigFile, parse: (String) -> Result<T>): Result<T> {
val config = if (appConfigs.loadRemote) {
loadFromCachedConfigFile(configFile).also {
fetchRemoteConfigFile(configFile, parse)
}
} else {
loadFromBundledLocalConfigFile(configFile)
}
return config?.let { parse(it) } ?: Result.failure(RuntimeException("Could not parse config file."))
}

private fun <T> fetchRemoteConfigFile(configFile: ConfigFile, parse: (String) -> Result<T>) {
val path = configFile.path
val configFileUrl = "$deploymentUri$path"
ioImplementations.rest?.get(configFileUrl, null, callback = { response, httpCode, _ ->
if (httpCode in 200..299 && response != null) {
if (parse(response).isSuccess) {
writeToLocalFile(response, path)
}
}
})
}

private fun loadFromCachedConfigFile(configFile: ConfigFile): String? {
return ioImplementations.fileSystem?.readCachedTextFile(
configFile.path,
)
}

private fun loadFromBundledLocalConfigFile(configFile: ConfigFile): String? {
return ioImplementations.fileSystem?.readTextFile(
FileLocation.AppBundle,
configFile.path,
)
}

private fun writeToLocalFile(response: String, file: String) {
ioImplementations.fileSystem?.writeTextFile(
file,
response,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package exchange.dydx.abacus.state.manager

import exchange.dydx.abacus.di.AbacusScope
import exchange.dydx.abacus.output.Documentation
import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Inject
import kotlin.js.JsExport

@JsExport
@AbacusScope
@Inject
class DocumentationLoader internal constructor(
configFileLoader: ConfigFileLoader
) {

val documentation: Documentation? =
// This is a blocking disk-read. Would be better to access this asynchronously,
// but that is a larger refactor and this is fairly low prio (not shown on a main screen).
// We don't need lazy here, because kotlin-inject accessors are handled lazily already.
configFileLoader.load(ConfigFile.DOCUMENTATION) {
runCatching { Json.decodeFromString<Documentation>(it) }
}.getOrNull() // Not the end of the world if we fail to read.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package exchange.dydx.abacus.state.manager

import exchange.dydx.abacus.di.AbacusScope
import exchange.dydx.abacus.di.Deployment
import exchange.dydx.abacus.di.DeploymentUri
import exchange.dydx.abacus.di.EnvironmentId
import exchange.dydx.abacus.di.EnvironmentIdParameter
import exchange.dydx.abacus.utils.Parser
import exchange.dydx.abacus.utils.UIImplementations
import kollections.iMutableListOf
import me.tatarka.inject.annotations.Inject

data class EnvAndAppSettings(
val environmentId: EnvironmentId,
val environments: List<V4Environment>,
val appSettings: AppSettings?,
)

@AbacusScope
@Inject
class EnvironmentLoader(
private val environmentParser: EnvironmentParser,
configFileLoader: ConfigFileLoader,
) {
val envAndAppSettings: EnvAndAppSettings =
// This is a blocking disk-read call.
// Since this is only at startup, and environment info is absolutely critical, we are okay with this.
// It would be far more complex to provide the final environment to the graph asynchronously, as all downstream consumers would
// need to become reactive, and literally everything depends on the environment.
configFileLoader.load(ConfigFile.ENV, environmentParser::parse).getOrThrow()
}

@Inject
class EnvironmentParser(
private val deploymentUri: DeploymentUri,
private val deployment: Deployment,
private val environmentIdParameter: EnvironmentIdParameter?,
private val uiImplementations: UIImplementations,
) {

fun parse(environmentsJson: String): Result<EnvAndAppSettings> {
val parser = Parser()
val items = parser.decodeJsonObject(environmentsJson)
val deployments = parser.asMap(items?.get("deployments")) ?: return Result.failure(RuntimeException("Failure to parse deployments"))
val target = parser.asMap(deployments[deployment]) ?: return Result.failure(RuntimeException("Failure to parse deployment: $deployment"))
val targetEnvironments = parser.asList(target["environments"]) ?: return Result.failure(RuntimeException("Failure to parse target environments"))
val targetDefault = parser.asString(target["default"])

val tokensData = parser.asNativeMap(items?.get("tokens"))
val linksData = parser.asNativeMap(items?.get("links"))
val walletsData = parser.asNativeMap(items?.get("wallets"))
val governanceData = parser.asNativeMap(items?.get("governance"))

if (items != null) {
val environmentsData = parser.asMap(items["environments"]) ?: return Result.failure(RuntimeException("Failure to parse environments"))
val parsedEnvironments = mutableMapOf<String, V4Environment>()
for ((key, value) in environmentsData) {
val data = parser.asMap(value) ?: continue
val dydxChainId = parser.asString(data["dydxChainId"]) ?: continue
val environment = V4Environment.parse(
key,
data,
parser,
deploymentUri,
uiImplementations.localizer,
parser.asNativeMap(tokensData?.get(dydxChainId)),
parser.asNativeMap(linksData?.get(dydxChainId)),
parser.asNativeMap(walletsData?.get(dydxChainId)),
parser.asNativeMap(governanceData?.get(dydxChainId)),
) ?: continue
parsedEnvironments[environment.id] = environment
}
if (parsedEnvironments.isEmpty()) {
return Result.failure(RuntimeException("Parsed environments was empty."))
}
val environments = iMutableListOf<V4Environment>()
for (environmentId in targetEnvironments) {
val environment = parsedEnvironments[parser.asString(environmentId)!!]
if (environment != null) {
environments.add(environment)
}
}

val appSettings = parser.asMap(items["apps"])?.let { AppSettings.parse(it, parser) }

return Result.success(
EnvAndAppSettings(
environmentId = requireNotNull(environmentIdParameter ?: targetDefault) { "environmentId was null and no target default defined." },
environments = environments,
appSettings = appSettings,
),
)
} else {
return Result.failure(RuntimeException("Failure to env json."))
}
}
}
2 changes: 1 addition & 1 deletion v4_abacus.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'v4_abacus'
spec.version = '1.7.19'
spec.version = '1.7.20'
spec.homepage = 'https://github.com/dydxprotocol/v4-abacus'
spec.source = { :http=> ''}
spec.authors = ''
Expand Down

0 comments on commit 45d155e

Please sign in to comment.