diff --git a/build.gradle.kts b/build.gradle.kts index d89f62ead..b5c9cfab5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,7 +51,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.7.19" +version = "1.7.20" repositories { google() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/di/AbacusComponent.kt b/src/commonMain/kotlin/exchange.dydx.abacus/di/AbacusComponent.kt index a6fe3de2e..61920b44b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/di/AbacusComponent.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/di/AbacusComponent.kt @@ -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 @@ -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) @@ -34,6 +40,7 @@ object AbacusFactory { stateNotification: StateNotificationProtocol? = null, dataNotification: DataNotificationProtocol? = null, presentationProtocol: PresentationProtocol? = null, + environmentIdParameter: EnvironmentIdParameter? = null, ): AbacusComponent = AbacusComponent::class.create( deploymentUri, deployment, @@ -43,6 +50,7 @@ object AbacusFactory { stateNotification, dataNotification, presentationProtocol, + environmentIdParameter, ) } @@ -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 } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/ConfigsLoader.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/ConfigsLoader.kt new file mode 100644 index 000000000..85cab8570 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/ConfigsLoader.kt @@ -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 load(configFile: ConfigFile, parse: (String) -> Result): Result { + 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 fetchRemoteConfigFile(configFile: ConfigFile, parse: (String) -> Result) { + 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, + ) + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/DocumentationLoader.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/DocumentationLoader.kt new file mode 100644 index 000000000..0a31a0834 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/DocumentationLoader.kt @@ -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(it) } + }.getOrNull() // Not the end of the world if we fail to read. +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/EnvironmentLoader.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/EnvironmentLoader.kt new file mode 100644 index 000000000..8d647cb95 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/manager/EnvironmentLoader.kt @@ -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, + 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 { + 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() + 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() + 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.")) + } + } +} diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 8a843c550..c0360d4e6 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.7.19' + spec.version = '1.7.20' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''