Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add V4Environment and Documentation to the graph. Pre-req for moving … #363

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading