From f1dd86bbba9e2594d59f3bf5675b3a7dc55d387c Mon Sep 17 00:00:00 2001 From: Julia Samol Date: Mon, 29 Jan 2024 17:45:41 +0100 Subject: [PATCH] Feat/rpc engine --- buildSrc/src/main/kotlin/Project.kt | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../acurast/rpc/{rpc.kt => AcurastRpc.kt} | 136 +++++------- rpc/src/main/kotlin/acurast/rpc/Rpc.kt | 15 ++ .../kotlin/acurast/rpc/engine/RpcEngine.kt | 22 ++ .../http/HttpClient.kt} | 6 +- .../acurast/rpc/engine/http/HttpRpcEngine.kt | 65 ++++++ .../http/ktor/KtorHttpClient.kt} | 23 +- .../http/ktor/KtorLogger.kt} | 4 +- .../main/kotlin/acurast/rpc/pallet/Author.kt | 33 +-- .../main/kotlin/acurast/rpc/pallet/Chain.kt | 68 ++---- .../kotlin/acurast/rpc/pallet/PalletRpc.kt | 22 ++ .../main/kotlin/acurast/rpc/pallet/State.kt | 191 +++++----------- .../main/kotlin/acurast/rpc/pallet/generic.kt | 32 --- rpc/src/main/kotlin/acurast/rpc/utils/Json.kt | 12 +- .../rpc/{RpcTest.kt => AcurastRpcTest.kt} | 205 +++++++++++------- .../kotlin/acurast/rpc/pallet/ChainTest.kt | 33 ++- rpc/src/test/kotlin/matcher/JsonRpc.kt | 23 ++ 18 files changed, 471 insertions(+), 423 deletions(-) rename rpc/src/main/kotlin/acurast/rpc/{rpc.kt => AcurastRpc.kt} (64%) create mode 100644 rpc/src/main/kotlin/acurast/rpc/Rpc.kt create mode 100644 rpc/src/main/kotlin/acurast/rpc/engine/RpcEngine.kt rename rpc/src/main/kotlin/acurast/rpc/{http/IHttpClientProvider.kt => engine/http/HttpClient.kt} (95%) create mode 100644 rpc/src/main/kotlin/acurast/rpc/engine/http/HttpRpcEngine.kt rename rpc/src/main/kotlin/acurast/rpc/{http/KtorHttpClientProvider.kt => engine/http/ktor/KtorHttpClient.kt} (88%) rename rpc/src/main/kotlin/acurast/rpc/{http/Logger.kt => engine/http/ktor/KtorLogger.kt} (87%) create mode 100644 rpc/src/main/kotlin/acurast/rpc/pallet/PalletRpc.kt delete mode 100644 rpc/src/main/kotlin/acurast/rpc/pallet/generic.kt rename rpc/src/test/kotlin/acurast/rpc/{RpcTest.kt => AcurastRpcTest.kt} (58%) create mode 100644 rpc/src/test/kotlin/matcher/JsonRpc.kt diff --git a/buildSrc/src/main/kotlin/Project.kt b/buildSrc/src/main/kotlin/Project.kt index 0e730ae..a0c7ae9 100644 --- a/buildSrc/src/main/kotlin/Project.kt +++ b/buildSrc/src/main/kotlin/Project.kt @@ -1,4 +1,4 @@ object Project { const val group = "com.github.acurast" - const val version = "0.1.11" + const val version = "0.1.12-beta01" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69a9715..ffed3a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/rpc/src/main/kotlin/acurast/rpc/rpc.kt b/rpc/src/main/kotlin/acurast/rpc/AcurastRpc.kt similarity index 64% rename from rpc/src/main/kotlin/acurast/rpc/rpc.kt rename to rpc/src/main/kotlin/acurast/rpc/AcurastRpc.kt index 6e6effb..caff93d 100644 --- a/rpc/src/main/kotlin/acurast/rpc/rpc.kt +++ b/rpc/src/main/kotlin/acurast/rpc/AcurastRpc.kt @@ -1,16 +1,16 @@ package acurast.rpc -import acurast.codec.extensions.* +import acurast.codec.extensions.blake2b +import acurast.codec.extensions.hexToBa +import acurast.codec.extensions.toU8a +import acurast.codec.extensions.xxH128 import acurast.codec.type.ProcessorVersion import acurast.codec.type.acurast.JobEnvironment import acurast.codec.type.acurast.JobIdentifier import acurast.codec.type.acurast.JobRegistration import acurast.codec.type.manager.ProcessorUpdateInfo import acurast.codec.type.marketplace.JobAssignment -import acurast.rpc.http.HttpHeader -import acurast.rpc.http.IHttpClientProvider -import acurast.rpc.http.KtorHttpClientProvider -import acurast.rpc.http.KtorLogger +import acurast.rpc.engine.RpcEngine import acurast.rpc.pallet.Author import acurast.rpc.pallet.Chain import acurast.rpc.pallet.State @@ -20,17 +20,10 @@ import acurast.rpc.type.readAccountInfo import acurast.rpc.type.readPalletAssetsAssetAccount import java.nio.ByteBuffer -public class RPC public constructor( - rpc_url: String, - http_client: IHttpClientProvider = KtorHttpClientProvider(object : KtorLogger() { - override fun log(message: String) { - println(message) - } - }) -) { - public val author: Author = Author(http_client, rpc_url) - public val chain: Chain = Chain(http_client, rpc_url) - public val state: State = State(http_client, rpc_url) +public class AcurastRpc(override val defaultEngine: RpcEngine) : Rpc { + public val author: Author = Author(defaultEngine) + public val chain: Chain = Chain(defaultEngine) + public val state: State = State(defaultEngine) /** * Query account information. (nonce, etc...) @@ -38,21 +31,19 @@ public class RPC public constructor( public suspend fun getAccountInfo( accountId: ByteArray, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null, + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): FrameSystemAccountInfo { val key = "System".toByteArray().xxH128() + "Account".toByteArray().xxH128() + - accountId.blake2b(128) + accountId; + accountId.blake2b(128) + accountId val storage = state.getStorage( storageKey = key, - blockHash = blockHash, - headers, - requestTimeout, - connectionTimeout + blockHash, + timeout, + engine, ) if (storage.isNullOrEmpty()) { @@ -69,23 +60,21 @@ public class RPC public constructor( assetId: Int, accountId: ByteArray, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null, + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): PalletAssetsAssetAccount? { val assetIdBytes = assetId.toU8a(); val key = "Assets".toByteArray().xxH128() + "Account".toByteArray().xxH128() + assetIdBytes.blake2b(128) + assetIdBytes + - accountId.blake2b(128) + accountId; + accountId.blake2b(128) + accountId val storage = state.getStorage( storageKey = key, - blockHash = blockHash, - headers, - requestTimeout, - connectionTimeout + blockHash, + timeout, + engine, ) if (storage.isNullOrEmpty()) { @@ -101,9 +90,8 @@ public class RPC public constructor( public suspend fun getJobRegistration( jobIdentifier: JobIdentifier, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null, + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): JobRegistration? { val origin = jobIdentifier.origin.toU8a() val jobId = jobIdentifier.id.toU8a() @@ -115,10 +103,9 @@ public class RPC public constructor( val storage = state.getStorage( storageKey = indexKey, - blockHash = blockHash, - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout + blockHash, + timeout, + engine ) if (storage.isNullOrEmpty()) { @@ -134,31 +121,28 @@ public class RPC public constructor( public suspend fun getAssignedJobs( accountId: ByteArray, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null, + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): List { val jobs: MutableList = mutableListOf() val indexKey = "AcurastMarketplace".toByteArray().xxH128() + "StoredMatches".toByteArray().xxH128() + - accountId.blake2b(128) + accountId; + accountId.blake2b(128) + accountId val keys = state.getKeys( - indexKey, - blockHash = blockHash, - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout + key = indexKey, + blockHash, + timeout, + engine, ) val result = state.queryStorageAt( storageKeys = keys, - blockHash = blockHash, - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout + blockHash, + timeout, + engine, ) if (result.isNotEmpty()) { @@ -176,9 +160,8 @@ public class RPC public constructor( public suspend fun isAttested( accountId: ByteArray, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null, + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): Boolean { val key = "Acurast".toByteArray().xxH128() + @@ -188,10 +171,9 @@ public class RPC public constructor( return try { val result = state.getStorage( storageKey = key, - blockHash = blockHash, - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout + blockHash, + timeout, + engine, ) !result.isNullOrEmpty() @@ -204,9 +186,8 @@ public class RPC public constructor( jobIdentifier: JobIdentifier, accountId: ByteArray, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null, + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): JobEnvironment? { val jobId = jobIdentifier.origin.toU8a() + jobIdentifier.id.toU8a() @@ -218,10 +199,9 @@ public class RPC public constructor( val storage = state.getStorage( storageKey = key, - blockHash = blockHash, - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout, + blockHash, + timeout, + engine, ) if (storage.isNullOrEmpty()) { @@ -234,9 +214,8 @@ public class RPC public constructor( public suspend fun getUpdateInfo( accountId: ByteArray, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null, + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): ProcessorUpdateInfo? { val key = "AcurastProcessorManager".toByteArray().xxH128() + @@ -245,10 +224,9 @@ public class RPC public constructor( val storage = state.getStorage( storageKey = key, - blockHash = blockHash, - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout, + blockHash, + timeout, + engine, ) if (storage.isNullOrEmpty()) { @@ -261,9 +239,8 @@ public class RPC public constructor( public suspend fun getKnownBinaryHash( version: ProcessorVersion, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null, + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): ByteArray? { val versionBytes = version.toU8a() val key = @@ -273,10 +250,9 @@ public class RPC public constructor( val storage = state.getStorage( storageKey = key, - blockHash = blockHash, - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout, + blockHash, + timeout, + engine ) return storage?.takeIf { it.isNotEmpty() }?.hexToBa() diff --git a/rpc/src/main/kotlin/acurast/rpc/Rpc.kt b/rpc/src/main/kotlin/acurast/rpc/Rpc.kt new file mode 100644 index 0000000..3a164a8 --- /dev/null +++ b/rpc/src/main/kotlin/acurast/rpc/Rpc.kt @@ -0,0 +1,15 @@ +package acurast.rpc + +import acurast.rpc.engine.RpcEngine + +public interface Rpc { + public val defaultEngine: RpcEngine +} + +internal object JsonRpc { + object Key { + const val RESULT = "result" + const val ERROR = "error" + const val MESSAGE = "message" + } +} \ No newline at end of file diff --git a/rpc/src/main/kotlin/acurast/rpc/engine/RpcEngine.kt b/rpc/src/main/kotlin/acurast/rpc/engine/RpcEngine.kt new file mode 100644 index 0000000..398a8a8 --- /dev/null +++ b/rpc/src/main/kotlin/acurast/rpc/engine/RpcEngine.kt @@ -0,0 +1,22 @@ +package acurast.rpc.engine + +import acurast.rpc.utils.jsonRpcRequest +import org.json.JSONArray +import org.json.JSONObject +import kotlin.random.Random + +public interface RpcEngine { + public val id: String + public suspend fun request(body: JSONObject, timeout: Long? = null): JSONObject +} + +public suspend fun RpcEngine.request( + id: UInt = Random.nextLong().toUInt(), + method: String, + params: JSONArray = JSONArray(), + timeout: Long? = null, +): JSONObject { + val body = jsonRpcRequest(id, method, params) + + return request(body, timeout) +} \ No newline at end of file diff --git a/rpc/src/main/kotlin/acurast/rpc/http/IHttpClientProvider.kt b/rpc/src/main/kotlin/acurast/rpc/engine/http/HttpClient.kt similarity index 95% rename from rpc/src/main/kotlin/acurast/rpc/http/IHttpClientProvider.kt rename to rpc/src/main/kotlin/acurast/rpc/engine/http/HttpClient.kt index 8627368..b9a8f85 100644 --- a/rpc/src/main/kotlin/acurast/rpc/http/IHttpClientProvider.kt +++ b/rpc/src/main/kotlin/acurast/rpc/engine/http/HttpClient.kt @@ -1,4 +1,4 @@ -package acurast.rpc.http; +package acurast.rpc.engine.http; /** * A key value HTTP header. @@ -15,9 +15,9 @@ public typealias HttpParameter = Pair * * Use this interface to register a custom HTTP client implementation. * See: - * - [KtorHttpClientProvider] for a ready-to-use implementation. + * - [KtorHttpClient] for a ready-to-use implementation. */ -public interface IHttpClientProvider { +public interface HttpClient { /** * Call DELETE HTTP method on specified [baseUrl] with [headers] and [parameters]. diff --git a/rpc/src/main/kotlin/acurast/rpc/engine/http/HttpRpcEngine.kt b/rpc/src/main/kotlin/acurast/rpc/engine/http/HttpRpcEngine.kt new file mode 100644 index 0000000..c042a96 --- /dev/null +++ b/rpc/src/main/kotlin/acurast/rpc/engine/http/HttpRpcEngine.kt @@ -0,0 +1,65 @@ +package acurast.rpc.engine.http + +import acurast.rpc.engine.RpcEngine +import acurast.rpc.engine.http.ktor.KtorHttpClient +import acurast.rpc.engine.http.ktor.KtorLogger +import org.json.JSONObject + +public class HttpRpcEngine internal constructor( + private val config: HttpRpcEngineConfig, + private val client: HttpClient, +) : RpcEngine { + override val id: String + get() = config.url + + override suspend fun request(body: JSONObject, timeout: Long?): JSONObject = with(config) { + val body = body.toString() + val requestTimeout = timeout + + val response = client.post( + url, + body, + headers, + parameters, + requestTimeout = requestTimeout, + connectionTimeout = connectionTimeout, + ) + + return JSONObject(response) + } +} +public interface HttpRpcEngineConfig { + public val url: String + public val headers: List? + public val parameters: List? + + public val connectionTimeout: Long? +} + +public data class MutableHttpRpcEngineConfig(override val url: String) : HttpRpcEngineConfig { + override var headers: List? = null + override var parameters: List? = null + + override var connectionTimeout: Long? = null +} + +private fun DefaultHttpClient(): HttpClient = KtorHttpClient(object : KtorLogger() { + override fun log(message: String) { + println(message) + } +}) + +public fun HttpRpcEngine( + url: String, + client: HttpClient = DefaultHttpClient(), + block: MutableHttpRpcEngineConfig.() -> Unit = {}, +): HttpRpcEngine { + val config = MutableHttpRpcEngineConfig(url).apply(block).apply { + headers = (this.headers.orEmpty() + listOf( + "Content-Type" to "application/json", + "Accept" to "application/json", + )).distinctBy { it.first } + } + + return HttpRpcEngine(config, client) +} \ No newline at end of file diff --git a/rpc/src/main/kotlin/acurast/rpc/http/KtorHttpClientProvider.kt b/rpc/src/main/kotlin/acurast/rpc/engine/http/ktor/KtorHttpClient.kt similarity index 88% rename from rpc/src/main/kotlin/acurast/rpc/http/KtorHttpClientProvider.kt rename to rpc/src/main/kotlin/acurast/rpc/engine/http/ktor/KtorHttpClient.kt index b744a45..dcac921 100644 --- a/rpc/src/main/kotlin/acurast/rpc/http/KtorHttpClientProvider.kt +++ b/rpc/src/main/kotlin/acurast/rpc/engine/http/ktor/KtorHttpClient.kt @@ -1,6 +1,8 @@ -package acurast.rpc.http; +package acurast.rpc.engine.http.ktor; -import io.ktor.client.* +import acurast.rpc.engine.http.HttpClient +import acurast.rpc.engine.http.HttpHeader +import acurast.rpc.engine.http.HttpParameter import io.ktor.client.engine.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* @@ -10,23 +12,22 @@ import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement /** - * [KtorHttpClientProvider] implementation that uses [Ktor](https://ktor.io/) to satisfy the interface requirements. + * [KtorHttpClient] implementation that uses [Ktor](https://ktor.io/) to satisfy the interface requirements. * - * @property engineFactory [Ktor HttpClientEngineFactory][HttpClientEngineFactory] that the underlying [Ktor HttpClient][HttpClient] should be configured with. + * @property engineFactory [Ktor HttpClientEngineFactory][HttpClientEngineFactory] that the underlying [Ktor HttpClient][io.ktor.client.HttpClient] should be configured with. * @property logger An optional logging configuration. */ -public class KtorHttpClientProvider( +public class KtorHttpClient( private val engineFactory: HttpClientEngineFactory<*> = CIO, private val logger: KtorLogger? = null, -): IHttpClientProvider { +): HttpClient { private val json: Json by lazy { Json.Default } - private val ktor: HttpClient by lazy { - HttpClient(engineFactory) { + private val ktor: io.ktor.client.HttpClient by lazy { + io.ktor.client.HttpClient(engineFactory) { expectSuccess = true install(HttpTimeout) @@ -181,6 +182,6 @@ public class KtorHttpClientProvider( } /** - * Creates a new [KtorHttpClientProvider] instance with a default [CIO HttpClientEngineFactory][CIO] and optional [logger]. + * Creates a new [KtorHttpClient] instance with a default [CIO HttpClientEngineFactory][CIO] and optional [logger]. */ -public fun KtorHttpClientProvider(logger: KtorLogger? = null): KtorHttpClientProvider = KtorHttpClientProvider(CIO, logger) \ No newline at end of file +public fun KtorHttpClient(logger: KtorLogger? = null): KtorHttpClient = KtorHttpClient(CIO, logger) \ No newline at end of file diff --git a/rpc/src/main/kotlin/acurast/rpc/http/Logger.kt b/rpc/src/main/kotlin/acurast/rpc/engine/http/ktor/KtorLogger.kt similarity index 87% rename from rpc/src/main/kotlin/acurast/rpc/http/Logger.kt rename to rpc/src/main/kotlin/acurast/rpc/engine/http/ktor/KtorLogger.kt index 9267c3b..d3a4cae 100644 --- a/rpc/src/main/kotlin/acurast/rpc/http/Logger.kt +++ b/rpc/src/main/kotlin/acurast/rpc/engine/http/ktor/KtorLogger.kt @@ -1,4 +1,4 @@ -package acurast.rpc.http +package acurast.rpc.engine.http.ktor public abstract class KtorLogger(public val level: LogLevel = LogLevel.All) { public open fun log(message: String) { @@ -12,4 +12,4 @@ public abstract class KtorLogger(public val level: LogLevel = LogLevel.All) { Info, None, } -} +} \ No newline at end of file diff --git a/rpc/src/main/kotlin/acurast/rpc/pallet/Author.kt b/rpc/src/main/kotlin/acurast/rpc/pallet/Author.kt index c772647..54b6615 100644 --- a/rpc/src/main/kotlin/acurast/rpc/pallet/Author.kt +++ b/rpc/src/main/kotlin/acurast/rpc/pallet/Author.kt @@ -1,38 +1,27 @@ package acurast.rpc.pallet import acurast.codec.extensions.toHex -import acurast.rpc.http.IHttpClientProvider -import acurast.rpc.http.HttpHeader +import acurast.rpc.JsonRpc +import acurast.rpc.engine.RpcEngine +import acurast.rpc.engine.request import acurast.rpc.utils.nullableOptString import org.json.JSONArray -import org.json.JSONObject -public class Author(http_client: IHttpClientProvider, rpc_url: String) : PalletRPC(http_client, rpc_url) { +public class Author(defaultEngine: RpcEngine) : PalletRpc(defaultEngine) { /** * Submit an extrinsic. */ public suspend fun submitExtrinsic( extrinsic: ByteArray, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): String? { - val body = prepareJSONRequest("author_submitExtrinsic", JSONArray().put(extrinsic.toHex())); - - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) - - val json = JSONObject(response) - - if (json.has("error")) { - throw handleError(json) + val params = JSONArray().apply { + put(extrinsic.toHex()) } - return json.nullableOptString("result") + val response = engine.request(method = "author_submitExtrinsic", params = params, timeout = timeout) + + return if (response.has(JsonRpc.Key.RESULT)) response.nullableOptString(JsonRpc.Key.RESULT) else throw handleError(response) } } \ No newline at end of file diff --git a/rpc/src/main/kotlin/acurast/rpc/pallet/Chain.kt b/rpc/src/main/kotlin/acurast/rpc/pallet/Chain.kt index 0a88699..1a087d0 100644 --- a/rpc/src/main/kotlin/acurast/rpc/pallet/Chain.kt +++ b/rpc/src/main/kotlin/acurast/rpc/pallet/Chain.kt @@ -1,43 +1,31 @@ package acurast.rpc.pallet import acurast.codec.extensions.toHex -import acurast.rpc.http.IHttpClientProvider -import acurast.rpc.http.HttpHeader +import acurast.rpc.JsonRpc +import acurast.rpc.engine.RpcEngine +import acurast.rpc.engine.request import acurast.rpc.type.Header -import acurast.rpc.utils.JSON_RPC_KEY_RESULT import acurast.rpc.utils.nullableOptString import org.json.JSONArray -import org.json.JSONObject import java.math.BigInteger -public class Chain(http_client: IHttpClientProvider, rpc_url: String) : PalletRPC(http_client, rpc_url) { +public class Chain(defaultEngine: RpcEngine) : PalletRpc(defaultEngine) { /** * Query the hash of a block at a given height. */ public suspend fun getBlockHash( blockNumber: BigInteger? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): String? { - val param = JSONArray() - // Add block number if provided - if (blockNumber != null) { - param.put(blockNumber.toString(16)) + val params = JSONArray().apply { + // Add block number if provided + blockNumber?.let { put(it.toLong()) } } - val body = prepareJSONRequest("chain_getBlockHash", param) + val response = engine.request(method = "chain_getBlockHash", params = params, timeout = timeout) - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) - - val json = JSONObject(response) - return if (json.has(JSON_RPC_KEY_RESULT)) json.nullableOptString(JSON_RPC_KEY_RESULT) else throw handleError(json) + return if (response.has(JsonRpc.Key.RESULT)) response.nullableOptString(JsonRpc.Key.RESULT) else throw handleError(response) } /** @@ -45,33 +33,21 @@ public class Chain(http_client: IHttpClientProvider, rpc_url: String) : PalletRP */ public suspend fun getHeader( blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): Header { - val param = JSONArray() - // Add block hash if provided, otherwise the head block will be queried. - if (blockHash != null) { - param.put(blockHash.toHex()) + val params = JSONArray().apply { + // Add block hash if provided, otherwise the head block will be queried. + blockHash?.let { put(it.toHex()) } } - val body = prepareJSONRequest("chain_getHeader", param) - - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) - - val json = JSONObject(response) - val result = json.optJSONObject("result") ?: throw handleError(json) + val response = engine.request(method = "chain_getHeader", params = params, timeout = timeout) + val result = response.optJSONObject(JsonRpc.Key.RESULT) ?: throw handleError(response) - val parentHash = result.nullableOptString("parentHash") ?: throw handleError(json) - val number = result.nullableOptString("number") ?: throw handleError(json) - val stateRoot = result.nullableOptString("stateRoot") ?: throw handleError(json) - val extrinsicsRoot = result.nullableOptString("extrinsicsRoot") ?: throw handleError(json) + val parentHash = result.nullableOptString("parentHash") ?: throw handleError(response) + val number = result.nullableOptString("number") ?: throw handleError(response) + val stateRoot = result.nullableOptString("stateRoot") ?: throw handleError(response) + val extrinsicsRoot = result.nullableOptString("extrinsicsRoot") ?: throw handleError(response) return Header( parentHash = parentHash, diff --git a/rpc/src/main/kotlin/acurast/rpc/pallet/PalletRpc.kt b/rpc/src/main/kotlin/acurast/rpc/pallet/PalletRpc.kt new file mode 100644 index 0000000..1f183e3 --- /dev/null +++ b/rpc/src/main/kotlin/acurast/rpc/pallet/PalletRpc.kt @@ -0,0 +1,22 @@ +package acurast.rpc.pallet + +import acurast.rpc.JsonRpc +import acurast.rpc.Rpc +import acurast.rpc.engine.RpcEngine +import com.google.gson.GsonBuilder +import org.json.JSONArray +import org.json.JSONObject + +public abstract class PalletRpc(override val defaultEngine: RpcEngine) : Rpc { + + protected inline fun JSONObject.toTypedObject(): T = parseJSON(toString()) + protected inline fun JSONArray.toTypedList(): List = parseJSON>(toString()).toList() + + protected fun handleError(json: JSONObject): Exception = + json.optJSONObject(JsonRpc.Key.ERROR)?.let { error -> + Exception(error.optString(JsonRpc.Key.MESSAGE)) + } ?: Exception("something went wrong") + + protected inline fun parseJSON(json: String): R = + GsonBuilder().create().fromJson(json, R::class.java) +} \ No newline at end of file diff --git a/rpc/src/main/kotlin/acurast/rpc/pallet/State.kt b/rpc/src/main/kotlin/acurast/rpc/pallet/State.kt index 36b880b..d88a430 100644 --- a/rpc/src/main/kotlin/acurast/rpc/pallet/State.kt +++ b/rpc/src/main/kotlin/acurast/rpc/pallet/State.kt @@ -2,13 +2,14 @@ package acurast.rpc.pallet import acurast.codec.extensions.hexToBa import acurast.codec.extensions.toHex -import acurast.rpc.http.HttpHeader -import acurast.rpc.http.IHttpClientProvider -import acurast.rpc.type.* +import acurast.rpc.JsonRpc +import acurast.rpc.engine.RpcEngine +import acurast.rpc.engine.request +import acurast.rpc.type.RuntimeMetadataV14 +import acurast.rpc.type.StorageQueryResult +import acurast.rpc.type.readMetadata import acurast.rpc.utils.nullableOptString -import com.google.gson.GsonBuilder import org.json.JSONArray -import org.json.JSONObject import java.nio.ByteBuffer public data class RuntimeVersion( @@ -16,7 +17,7 @@ public data class RuntimeVersion( val transactionVersion: Int, ) -public class State(http_client: IHttpClientProvider, rpc_url: String) : PalletRPC(http_client, rpc_url) { +public class State(defaultEngine: RpcEngine) : PalletRpc(defaultEngine) { /** * Perform a call to a builtin on the chain. */ @@ -24,37 +25,22 @@ public class State(http_client: IHttpClientProvider, rpc_url: String) : PalletRP method: String, data: ByteArray? = null, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): String? { - val param = JSONArray().put(method) - // Add method payload - if (data != null) { - param.put(data.toHex()) - } - // Add block hash if provided - if (blockHash != null) { - param.put(blockHash.toHex()) - } - - val body = prepareJSONRequest("state_call", param) + val params = JSONArray().apply { + put(method) - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) - - val json = JSONObject(response) + // Add method payload + data?.let { put(it.toHex()) } - if (json.has("error")) { - throw handleError(json) + // Add block hash if provided + blockHash?.let { put(it.toHex()) } } - return json.nullableOptString("result") + val response = engine.request(method = "state_call", params = params, timeout = timeout) + + return if (response.has(JsonRpc.Key.RESULT)) response.nullableOptString(JsonRpc.Key.RESULT) else throw handleError(response) } /** @@ -63,33 +49,19 @@ public class State(http_client: IHttpClientProvider, rpc_url: String) : PalletRP public suspend fun getStorage( storageKey: ByteArray, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): String? { - val param = JSONArray().put(storageKey.toHex()) - // Add block hash if provided - if (blockHash != null) { - param.put(blockHash.toHex()) - } - - val body = prepareJSONRequest("state_getStorage", param) - - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) - - val json = JSONObject(response) + val params = JSONArray().apply { + put(storageKey.toHex()) - if (json.has("error")) { - throw handleError(json) + // Add block hash if provided + blockHash?.let { put(it.toHex()) } } - return json.nullableOptString("result") + val response = engine.request(method = "state_getStorage", params = params, timeout = timeout) + + return if (response.has(JsonRpc.Key.RESULT)) response.nullableOptString(JsonRpc.Key.RESULT) else throw handleError(response) } /** @@ -98,32 +70,19 @@ public class State(http_client: IHttpClientProvider, rpc_url: String) : PalletRP public suspend fun queryStorageAt( storageKeys: List, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): List { - val param = JSONArray().put(storageKeys.fold(JSONArray()) { acc, key -> acc.put(key) }) - // Add block hash if provided - if (blockHash != null) { - param.put(blockHash.toHex()) - } + val params = JSONArray().apply { + put(storageKeys.fold(JSONArray()) { acc, key -> acc.put(key) }) - val body = prepareJSONRequest("state_queryStorageAt", param) + // Add block hash if provided + blockHash?.let { put(it.toHex()) } + } - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) + val response = engine.request(method = "state_queryStorageAt", params = params, timeout = timeout) - val json = JSONObject(response) - if (json.has("result")) { - val result = JSONObject(response).optJSONArray("result").toString() - return GsonBuilder().create().fromJson(result, Array::class.java).toList() - } - throw handleError(json) + return response.optJSONArray(JsonRpc.Key.RESULT)?.toTypedList() ?: throw handleError(response) } /** @@ -132,28 +91,19 @@ public class State(http_client: IHttpClientProvider, rpc_url: String) : PalletRP public suspend fun getKeys( key: ByteArray, blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): List { - val param = JSONArray().put(key.toHex()) - // Add block hash if provided - if (blockHash != null) { - param.put(blockHash.toHex()) - } + val params = JSONArray().apply { + put(key.toHex()) - val body = prepareJSONRequest("state_getKeys", param) + // Add block hash if provided + blockHash?.let { put(it.toHex()) } + } - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) + val response = engine.request(method = "state_getKeys", params = params, timeout = timeout) - val result = JSONObject(response).optJSONArray("result").toString() - return GsonBuilder().create().fromJson(result, Array::class.java).toList() + return response.optJSONArray(JsonRpc.Key.RESULT)?.toTypedList() ?: throw handleError(response) } /** @@ -161,28 +111,17 @@ public class State(http_client: IHttpClientProvider, rpc_url: String) : PalletRP */ public suspend fun getRuntimeVersion( blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): RuntimeVersion { - val param = JSONArray() - // Add block hash if provided - if (blockHash != null) { - param.put(blockHash.toHex()) + val params = JSONArray().apply { + // Add block hash if provided + blockHash?.let { put(it.toHex()) } } - val body = prepareJSONRequest("state_getRuntimeVersion", param) - - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) + val response = engine.request(method = "state_getRuntimeVersion", params = params, timeout = timeout) - val json = JSONObject(response) - val result = json.optJSONObject("result") ?: throw handleError(json) + val result = response.optJSONObject(JsonRpc.Key.RESULT) ?: throw handleError(response) return RuntimeVersion( result.optInt("specVersion"), @@ -195,28 +134,16 @@ public class State(http_client: IHttpClientProvider, rpc_url: String) : PalletRP */ public suspend fun getMetadata( blockHash: ByteArray? = null, - headers: List? = null, - requestTimeout: Long? = null, - connectionTimeout: Long? = null + timeout: Long? = null, + engine: RpcEngine = defaultEngine, ): RuntimeMetadataV14 { - val param = JSONArray() - // Add block hash if provided - if (blockHash != null) { - param.put(blockHash.toHex()) + val params = JSONArray().apply { + // Add block hash if provided + blockHash?.let { put(it.toHex()) } } - val body = prepareJSONRequest("state_getMetadata", param) - - val response = http_client.post( - rpc_url, - body = body.toString(), - headers = headers, - requestTimeout = requestTimeout, - connectionTimeout = connectionTimeout - ) - - val json = JSONObject(response) - val result = json.nullableOptString("result") ?: throw handleError(json) + val response = engine.request(method = "state_getMetadata", params = params, timeout = timeout) + val result = response.nullableOptString(JsonRpc.Key.RESULT) ?: throw handleError(response) return ByteBuffer.wrap(result.hexToBa()).readMetadata() } diff --git a/rpc/src/main/kotlin/acurast/rpc/pallet/generic.kt b/rpc/src/main/kotlin/acurast/rpc/pallet/generic.kt deleted file mode 100644 index f7cefd9..0000000 --- a/rpc/src/main/kotlin/acurast/rpc/pallet/generic.kt +++ /dev/null @@ -1,32 +0,0 @@ -package acurast.rpc.pallet - -import acurast.rpc.http.IHttpClientProvider -import org.json.JSONArray -import org.json.JSONObject -import java.net.URL - -public fun JSONRequest(method: String, params: JSONArray = JSONArray()): JSONObject { - val body = JSONObject() - body.put("id", 1) - body.put("jsonrpc", "2.0") - body.put("method", method) - body.put("params", params) - return body; -} - -public abstract class PalletRPC constructor( - protected val http_client: IHttpClientProvider, - protected val rpc_url: String -) { - protected val url: URL = URL(rpc_url); - - protected fun handleError(json: JSONObject): Exception { - return json.optJSONObject("error")?.let { error -> - Exception(error.optString("message")) - } ?: Exception("something went wrong (JSON: $json)") - } - - protected fun prepareJSONRequest(method: String, params: JSONArray = JSONArray()): JSONObject { - return JSONRequest(method, params); - } -} \ No newline at end of file diff --git a/rpc/src/main/kotlin/acurast/rpc/utils/Json.kt b/rpc/src/main/kotlin/acurast/rpc/utils/Json.kt index c0d110c..16c5419 100644 --- a/rpc/src/main/kotlin/acurast/rpc/utils/Json.kt +++ b/rpc/src/main/kotlin/acurast/rpc/utils/Json.kt @@ -1,6 +1,8 @@ package acurast.rpc.utils +import org.json.JSONArray import org.json.JSONObject +import kotlin.random.Random internal const val JSON_RPC_KEY_RESULT = "result" @@ -14,4 +16,12 @@ internal const val JSON_RPC_KEY_RESULT = "result" * trying to access and return it, and returning `null` if it doesn't. */ internal fun JSONObject.nullableOptString(key: String): String? = - if (has(key) && !isNull(key)) getString(key) else null \ No newline at end of file + if (has(key) && !isNull(key)) getString(key) else null + +public fun jsonRpcRequest(id: UInt = Random.nextLong().toUInt(), method: String, params: JSONArray = JSONArray()): JSONObject = + JSONObject().apply { + put("id", id) + put("jsonrpc", "2.0") + put("method", method) + put("params", params) + } \ No newline at end of file diff --git a/rpc/src/test/kotlin/acurast/rpc/RpcTest.kt b/rpc/src/test/kotlin/acurast/rpc/AcurastRpcTest.kt similarity index 58% rename from rpc/src/test/kotlin/acurast/rpc/RpcTest.kt rename to rpc/src/test/kotlin/acurast/rpc/AcurastRpcTest.kt index 095fcc8..2e2b4ce 100644 --- a/rpc/src/test/kotlin/acurast/rpc/RpcTest.kt +++ b/rpc/src/test/kotlin/acurast/rpc/AcurastRpcTest.kt @@ -3,20 +3,17 @@ package acurast.rpc import acurast.codec.extensions.hexToBa import acurast.codec.extensions.toHex import acurast.codec.type.AccountId32 -import acurast.codec.type.Fungibility import acurast.codec.type.acurast.JobIdentifier import acurast.codec.type.acurast.MultiOrigin -import acurast.rpc.http.IHttpClientProvider -import acurast.rpc.pallet.JSONRequest +import acurast.rpc.engine.RpcEngine import acurast.rpc.type.FrameSystemAccountInfo import acurast.rpc.type.PalletAssetsAssetAccount -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.unmockkAll import kotlinx.coroutines.runBlocking +import matcher.matchJsonRpcRequest import org.json.JSONArray +import org.json.JSONObject import org.junit.After import org.junit.Before import org.junit.Test @@ -24,17 +21,16 @@ import org.junit.jupiter.api.Assertions.assertTrue import java.math.BigInteger import kotlin.test.assertEquals -class RpcTest { +class AcurastRpcTest { @MockK - private lateinit var httpClient : IHttpClientProvider - private lateinit var rpc: RPC + private lateinit var rpcEngine: RpcEngine - private val rpcURL = "https://example.com" + private lateinit var acurastRpc: AcurastRpc @Before fun setup() { MockKAnnotations.init(this) - rpc = RPC(rpcURL, httpClient) + acurastRpc = AcurastRpc(rpcEngine) } @After @@ -45,33 +41,37 @@ class RpcTest { @Test fun `Verify if a given account is attested`() { val account = "0x58aee56e2857bde581bb6fe383de7fb7fac7e6c13ab54952b9d1bfec9af7ee00" - val param = JSONArray().put("d8f45172ad1e7575680eaa8157102800f75db0c33624f81fd193acdc07620654f9f1bd810967c8bb5367f6626d3c7c0c58aee56e2857bde581bb6fe383de7fb7fac7e6c13ab54952b9d1bfec9af7ee00") - val body = JSONRequest("state_getStorage", param).toString() + val method = "state_getStorage" + val params = JSONArray().apply { + put("d8f45172ad1e7575680eaa8157102800f75db0c33624f81fd193acdc07620654f9f1bd810967c8bb5367f6626d3c7c0c58aee56e2857bde581bb6fe383de7fb7fac7e6c13ab54952b9d1bfec9af7ee00") + } - val jsonResponse = """ + val jsonResponse = JSONObject(""" { "jsonrpc": "2.0", "result": "0x08d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48", "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(any(), any(), any(), any()) } returns jsonResponse + coEvery { rpcEngine.request(any(), any()) } returns jsonResponse val response = runBlocking { - rpc.isAttested(account.hexToBa()) + acurastRpc.isAttested(account.hexToBa()) } assertTrue(response) - coVerify { httpClient.post(rpcURL, body = body ) } + coVerify { rpcEngine.request(body = matchJsonRpcRequest(method, params), timeout = any()) } } @Test fun `Get Account Information`() { val account = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" - val param = JSONArray().put("26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") - val body = JSONRequest("state_getStorage", param).toString() + val method = "state_getStorage" + val params = JSONArray().apply { + put("26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") + } val expectedResponse = FrameSystemAccountInfo( nonce = 0U, @@ -86,31 +86,33 @@ class RpcTest { ) ) - val jsonResponse = """ + val jsonResponse = JSONObject(""" { "jsonrpc": "2.0", "result": "0x0000000001000000010000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(any(), any(), any(), any()) } returns jsonResponse + coEvery { rpcEngine.request(any(), any()) } returns jsonResponse val response = runBlocking { - rpc.getAccountInfo(account.hexToBa()) + acurastRpc.getAccountInfo(account.hexToBa()) } assertEquals(expectedResponse, response) - coVerify { httpClient.post(rpcURL, body = body, ) } + coVerify { rpcEngine.request(body = matchJsonRpcRequest(method, params), timeout = any()) } } @Test fun `Get Account Asset Information`() { val assetId = 10 val account = "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" - val param = JSONArray().put("682a59d51ab9e48a8c8cc418ff9708d2b99d880ec681799c0cf30e8886371da91523c4974e05c5b917b6037dec663b5d0a000000de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") - val body = JSONRequest("state_getStorage", param).toString() + val method = "state_getStorage" + val params = JSONArray().apply { + put("682a59d51ab9e48a8c8cc418ff9708d2b99d880ec681799c0cf30e8886371da91523c4974e05c5b917b6037dec663b5d0a000000de1e86a9a8c739864cf3cc5ec2bea59fd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") + } val expectedResponse = PalletAssetsAssetAccount( balance = BigInteger("99999999999999999999999999000000000000"), @@ -118,31 +120,33 @@ class RpcTest { reason = 0, ) - val jsonResponse = """ + val jsonResponse = JSONObject(""" { "jsonrpc": "2.0", "result": "0x00f05a2b57218a097ac4865aa84c3b4b0000", "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(any(), any(), any(), any()) } returns jsonResponse + coEvery { rpcEngine.request(any(), any()) } returns jsonResponse val response = runBlocking { - rpc.getAccountAssetInfo(assetId, account.hexToBa()) + acurastRpc.getAccountAssetInfo(assetId, account.hexToBa()) } assertEquals(expectedResponse, response) - coVerify { httpClient.post(rpcURL, body = body, ) } + coVerify { rpcEngine.request(body = matchJsonRpcRequest(method, params), timeout = any()) } } @Test fun `Get Job Matches`() { - val paramGetKeys = "1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d733e7c1e81d34082d8f34de585b45ce09353cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196" - val bodyGetKeys = JSONRequest("state_getKeys", JSONArray().put(paramGetKeys)).toString() + val getKeysMethod = "state_getKeys" + val getKeysParams = JSONArray().apply { + put("1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d733e7c1e81d34082d8f34de585b45ce09353cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196") + } - val getKeysResponse = """ + val getKeysResponse = JSONObject(""" { "jsonrpc": "2.0", "result": [ @@ -150,15 +154,24 @@ class RpcTest { ], "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(any(), bodyGetKeys, any(), any(), any(), any()) } returns getKeysResponse + coEvery { + rpcEngine.request( + body = matchJsonRpcRequest(method = getKeysMethod, params = getKeysParams), + timeout = any(), + ) + } returns getKeysResponse val account = "53cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196" - val param = JSONArray().put("0x1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d73bda717e2c1dbd94fc3adce6ba1ba9c0fd80a8b0d800a3320528693947f7317871b2d51e5f3c8f3d0d4e4f7e6938ed68f6417d15b337c54cc1c927a3d3125787500d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d02000000000000000000000000000000") - val body = JSONRequest("state_queryStorageAt", JSONArray().put(param)).toString() + val queryStorageMethod = "state_queryStorageAt" + val queryStorageParams = JSONArray().apply { + put(JSONArray().apply { + put("0x1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d73bda717e2c1dbd94fc3adce6ba1ba9c0fd80a8b0d800a3320528693947f7317871b2d51e5f3c8f3d0d4e4f7e6938ed68f6417d15b337c54cc1c927a3d3125787500d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d02000000000000000000000000000000") + }) + } - val jsonResponse = """ + val queryStorageResponse = JSONObject(""" { "jsonrpc": "2.0", "result": [ @@ -178,12 +191,17 @@ class RpcTest { ], "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(rpcURL, body, any(), any()) } returns jsonResponse + coEvery { + rpcEngine.request( + body = matchJsonRpcRequest(method = queryStorageMethod, params = queryStorageParams), + timeout = any(), + ) + } returns queryStorageResponse val response = runBlocking { - rpc.getAssignedJobs(account.hexToBa()) + acurastRpc.getAssignedJobs(account.hexToBa()) } assertEquals(2, response.size) @@ -195,10 +213,12 @@ class RpcTest { @Test fun `Get Job Matches 2`() { - val paramGetKeys = "1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d733e7c1e81d34082d8f34de585b45ce09353cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196" - val bodyGetKeys = JSONRequest("state_getKeys", JSONArray().put(paramGetKeys)).toString() + val getKeysMethod = "state_getKeys" + val getKeysParams = JSONArray().apply { + put("1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d733e7c1e81d34082d8f34de585b45ce09353cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196") + } - val getKeysResponse = """ + val getKeysResponse = JSONObject(""" { "jsonrpc": "2.0", "result": [ @@ -206,16 +226,24 @@ class RpcTest { ], "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(any(), bodyGetKeys, any(), any(), any(), any()) } returns getKeysResponse + coEvery { + rpcEngine.request( + body = matchJsonRpcRequest(method = getKeysMethod, params = getKeysParams), + timeout = any(), + ) + } returns getKeysResponse val account = "53cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196" - val param = JSONArray() - .put("0x1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d73d09150b9053d7f4684e2946402e967ac998d2cf8c0590b83951070633f0ff21fe613fae2c18ed2db69b8404f66f4311578dfc438585deb389231ab96e2c8beeb009a1a0c52c2d7f23820caa7757acf049e07fcb68015919638feece41a4b0ee538df000000000000000000000000000000") - val body = JSONRequest("state_queryStorageAt", JSONArray().put(param)).toString() + val queryStorageMethod = "state_queryStorageAt" + val queryStorageParams = JSONArray().apply { + put(JSONArray().apply { + put("0x1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d73d09150b9053d7f4684e2946402e967ac998d2cf8c0590b83951070633f0ff21fe613fae2c18ed2db69b8404f66f4311578dfc438585deb389231ab96e2c8beeb009a1a0c52c2d7f23820caa7757acf049e07fcb68015919638feece41a4b0ee538df000000000000000000000000000000") + }) + } - val jsonResponse = """ + val queryStorageResponse = JSONObject(""" { "jsonrpc": "2.0", "result": [ @@ -231,12 +259,17 @@ class RpcTest { ], "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(rpcURL, body, any(), any()) } returns jsonResponse + coEvery { + rpcEngine.request( + body = matchJsonRpcRequest(method = queryStorageMethod, params = queryStorageParams), + timeout = any(), + ) + } returns queryStorageResponse val response = runBlocking { - rpc.getAssignedJobs(account.hexToBa()) + acurastRpc.getAssignedJobs(account.hexToBa()) } assertEquals(1, response.size) @@ -248,10 +281,12 @@ class RpcTest { @Test fun `Get Job Matches 3`() { - val paramGetKeys = "1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d733e7c1e81d34082d8f34de585b45ce09353cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196" - val bodyGetKeys = JSONRequest("state_getKeys", JSONArray().put(paramGetKeys)).toString() + val getKeysMethod = "state_getKeys" + val getKeysParams = JSONArray().apply { + put("1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d733e7c1e81d34082d8f34de585b45ce09353cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196") + } - val getKeysResponse = """ + val getKeysResponse = JSONObject(""" { "jsonrpc": "2.0", "result": [ @@ -260,17 +295,25 @@ class RpcTest { ], "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(any(), bodyGetKeys, any(), any(), any(), any()) } returns getKeysResponse + coEvery { + rpcEngine.request( + body = matchJsonRpcRequest(method = getKeysMethod, params = getKeysParams), + timeout = any(), + ) + } returns getKeysResponse val account = "53cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196" - val param = JSONArray() - .put("0x1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d7356e3d8d388a039de16da63e7231ac9839717c7ea2bb68aec76fdd8c6761490939f721d5719d42f80f13529cd69be59261072337b97a36a2853d61ae29668469d00100b4cf29c67f598e147c5481bba5d332ca37256df9a9984c58ca30fb3624e526e000000000000000000000000000000") - .put("0x1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d7356e3d8d388a039de16da63e7231ac9839717c7ea2bb68aec76fdd8c6761490939f721d5719d42f80f13529cd69be592678dfc438585deb389231ab96e2c8beeb009a1a0c52c2d7f23820caa7757acf049e07fcb68015919638feece41a4b0ee538df000000000000000000000000000000") - val body = JSONRequest("state_queryStorageAt", JSONArray().put(param)).toString() + val queryStorageMethod = "state_queryStorageAt" + val queryStorageParams = JSONArray().apply { + put(JSONArray().apply { + put("0x1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d7356e3d8d388a039de16da63e7231ac9839717c7ea2bb68aec76fdd8c6761490939f721d5719d42f80f13529cd69be59261072337b97a36a2853d61ae29668469d00100b4cf29c67f598e147c5481bba5d332ca37256df9a9984c58ca30fb3624e526e000000000000000000000000000000") + put("0x1aee6710ac79060b1e13291ba85112af2b949d1a72012eeaa1f6b481830d0d7356e3d8d388a039de16da63e7231ac9839717c7ea2bb68aec76fdd8c6761490939f721d5719d42f80f13529cd69be592678dfc438585deb389231ab96e2c8beeb009a1a0c52c2d7f23820caa7757acf049e07fcb68015919638feece41a4b0ee538df000000000000000000000000000000") + }) + } - val jsonResponse = """ + val queryStorageResponse = JSONObject(""" { "jsonrpc": "2.0", "result": [ @@ -290,12 +333,17 @@ class RpcTest { ], "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(rpcURL, body, any(), any()) } returns jsonResponse + coEvery { + rpcEngine.request( + body = matchJsonRpcRequest(method = queryStorageMethod, params = queryStorageParams), + timeout = any(), + ) + } returns queryStorageResponse val response = runBlocking { - rpc.getAssignedJobs(account.hexToBa()) + acurastRpc.getAssignedJobs(account.hexToBa()) } assertEquals(2, response.size) @@ -310,21 +358,28 @@ class RpcTest { val account = "1cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c" val script = "697066733a2f2f516d516b6d7234576a3772666e4232445a45565a67474d58566b6f4a684d4d6e5a784e336a733565693542514353" - val param = JSONArray().put("d8f45172ad1e7575680eaa8157102800f86e394d601279aa535d660453ff8e0928c68911e8cc8866ccf20f186a8f202b001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c3ba80a3778f04ebf45e806d19a05202501000000000000000000000000000000") - val body = JSONRequest("state_getStorage", param).toString() + val method = "state_getStorage" + val params = JSONArray().apply { + put("d8f45172ad1e7575680eaa8157102800f86e394d601279aa535d660453ff8e0928c68911e8cc8866ccf20f186a8f202b001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c3ba80a3778f04ebf45e806d19a05202501000000000000000000000000000000") + } - val jsonResponse = """ + val jsonResponse = JSONObject(""" { "jsonrpc": "2.0", "result": "d4697066733a2f2f516d516b6d7234576a3772666e4232445a45565a67474d58566b6f4a684d4d6e5a784e336a733565693542514353010453cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196013075000000000000592103e08601000059740ae086010000187900000000000000000000000000000100000001000000010000000000000100010300a10f0432055800821a060000010453cf73c65e36ec0bf3d7539780e83febd2d1b01de0df4f6bb7a95157715f2196000000000000000000000000000000000000000000000000", "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(rpcURL, body, any(), any()) } returns jsonResponse + coEvery { + rpcEngine.request( + body = matchJsonRpcRequest(method, params), + timeout = any(), + ) + } returns jsonResponse val response = runBlocking { - rpc.getJobRegistration(JobIdentifier(MultiOrigin.Acurast(AccountId32(account.hexToBa())), BigInteger.ONE)) + acurastRpc.getJobRegistration(JobIdentifier(MultiOrigin.Acurast(AccountId32(account.hexToBa())), BigInteger.ONE)) } assertEquals(script, response?.script?.toHex()) diff --git a/rpc/src/test/kotlin/acurast/rpc/pallet/ChainTest.kt b/rpc/src/test/kotlin/acurast/rpc/pallet/ChainTest.kt index 75630d3..86aceda 100644 --- a/rpc/src/test/kotlin/acurast/rpc/pallet/ChainTest.kt +++ b/rpc/src/test/kotlin/acurast/rpc/pallet/ChainTest.kt @@ -1,14 +1,13 @@ package acurast.rpc.pallet -import acurast.rpc.RPC -import acurast.rpc.http.IHttpClientProvider -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify +import acurast.rpc.AcurastRpc +import acurast.rpc.engine.RpcEngine +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.unmockkAll import kotlinx.coroutines.runBlocking +import matcher.matchJsonRpcRequest import org.json.JSONArray +import org.json.JSONObject import org.junit.After import org.junit.Before import org.junit.Test @@ -16,15 +15,15 @@ import kotlin.test.assertEquals class ChainTest { @MockK - private lateinit var httpClient : IHttpClientProvider - private lateinit var rpc: RPC + private lateinit var rpcEngine: RpcEngine + + private lateinit var acurastRpc: AcurastRpc - private val rpcURL = "https://example.com" @Before fun setup() { MockKAnnotations.init(this) - rpc = RPC(rpcURL, httpClient) + acurastRpc = AcurastRpc(rpcEngine) } @After @@ -34,27 +33,27 @@ class ChainTest { @Test fun `Get Block Hash`() { - val body = JSONRequest("chain_getBlockHash", JSONArray()).toString() + val method = "chain_getBlockHash" + val params = JSONArray() val expectedResponse = "0x12664e6b6d3eb4dcf6c594a30f5e9e79ab980511a5f7e07ce490e454165caa7d" - - val jsonResponse = """ + val jsonResponse = JSONObject(""" { "jsonrpc": "2.0", "result": "0x12664e6b6d3eb4dcf6c594a30f5e9e79ab980511a5f7e07ce490e454165caa7d", "id": 1 } - """.trimIndent() + """.trimIndent()) - coEvery { httpClient.post(any(), any(), any(), any()) } returns jsonResponse + coEvery { rpcEngine.request(any(), any()) } returns jsonResponse val response = runBlocking { - rpc.chain.getBlockHash() + acurastRpc.chain.getBlockHash() } assertEquals(expectedResponse, response) - coVerify { httpClient.post(rpcURL, body = body, ) } + coVerify { rpcEngine.request(body = matchJsonRpcRequest(method, params), timeout = any()) } } } \ No newline at end of file diff --git a/rpc/src/test/kotlin/matcher/JsonRpc.kt b/rpc/src/test/kotlin/matcher/JsonRpc.kt new file mode 100644 index 0000000..992b7b2 --- /dev/null +++ b/rpc/src/test/kotlin/matcher/JsonRpc.kt @@ -0,0 +1,23 @@ +package matcher + +import io.mockk.Matcher +import io.mockk.MockKMatcherScope +import org.json.JSONArray +import org.json.JSONObject + +fun MockKMatcherScope.matchJsonRpcRequest(method: String, params: JSONArray = JSONArray()): JSONObject = + match(JsonRpcRequestMatcher(method, params)) +data class JsonRpcRequestMatcher(val method: String, val params: JSONArray = JSONArray()) : + Matcher { + override fun match(arg: JSONObject?): Boolean = + arg != null + && arg.getString("jsonrpc") == "2.0" + && arg.has("id") + && arg.getString("method") == method + && arg.getJSONArray("params").similar(params) + + override fun toString(): String = "matchJsonRpcRequest($method, $params)" + + override fun substitute(map: Map): Matcher = + copy(method = map.getOrDefault(method, method) as String, params = map.getOrDefault(params, params) as JSONArray) +} \ No newline at end of file