Skip to content

Commit

Permalink
Support automatic runtime apis detection for metadata v15 and above
Browse files Browse the repository at this point in the history
  • Loading branch information
valentunn committed Nov 19, 2024
1 parent d08d350 commit 323610d
Show file tree
Hide file tree
Showing 16 changed files with 367 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.novasama.substrate_sdk_android.runtime.definitions.types.Type
import io.novasama.substrate_sdk_android.runtime.definitions.types.TypeReference
import io.novasama.substrate_sdk_android.runtime.definitions.types.resolvedOrNull
import io.novasama.substrate_sdk_android.runtime.definitions.types.skipAliases
import java.math.BigInteger

interface RequestPreprocessor {

Expand All @@ -25,6 +26,10 @@ class TypeRegistry(
return typeRef?.value
}

operator fun get(typeIndex: BigInteger): Type<*>? {
return get(typeIndex.toString())
}

inline operator fun <reified R> get(key: String): R? {
return get(key)?.let { it as? R }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.novasama.substrate_sdk_android.runtime.metadata
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Null
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import io.novasama.substrate_sdk_android.runtime.metadata.module.RuntimeApi
import java.math.BigInteger

interface WithName {
Expand All @@ -11,10 +12,12 @@ interface WithName {

fun <T : WithName> List<T>.groupByName() = associateBy(WithName::name).toMap()

class RuntimeMetadata(
data class RuntimeMetadata(
val metadataVersion: Int,
val modules: Map<String, Module>,
val extrinsic: ExtrinsicMetadata
val extrinsic: ExtrinsicMetadata,
// Present in v15+ metadata. null otherwise
val apis: List<RuntimeApi>?
)

class ExtrinsicMetadata(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
import io.novasama.substrate_sdk_android.runtime.definitions.types.bytes
import io.novasama.substrate_sdk_android.runtime.definitions.types.errors.EncodeDecodeException
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
import io.novasama.substrate_sdk_android.runtime.definitions.types.useScaleWriter
import io.novasama.substrate_sdk_android.runtime.metadata.module.ErrorMetadata
import io.novasama.substrate_sdk_android.runtime.metadata.module.Event
import io.novasama.substrate_sdk_android.runtime.metadata.module.MetadataFunction
import io.novasama.substrate_sdk_android.runtime.metadata.module.Module
import io.novasama.substrate_sdk_android.runtime.metadata.module.RuntimeApi
import io.novasama.substrate_sdk_android.runtime.metadata.module.RuntimeApiMethod
import io.novasama.substrate_sdk_android.runtime.metadata.module.StorageEntry
import io.novasama.substrate_sdk_android.runtime.metadata.module.StorageEntryType
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.RuntimeRequest
import io.novasama.substrate_sdk_android.wsrpc.request.runtime.state.StateCallRequest
import java.io.ByteArrayOutputStream

/**
Expand Down Expand Up @@ -138,7 +144,7 @@ fun StorageEntry.storageKey(runtime: RuntimeSnapshot, vararg arguments: Any?): S
val argumentType = argumentsTypes[index]
val argumentHasher = argumentsHashers[index]

val keyEncoded = argumentType?.bytes(runtime, key) ?: typeNotResolved(fullName)
val keyEncoded = argumentType?.bytes(runtime, key) ?: storageTypeNotResolved(fullName)

keyOutputStream.write(argumentHasher.hashingFunction(keyEncoded))
}
Expand All @@ -154,7 +160,10 @@ fun StorageEntry.storageKey(runtime: RuntimeSnapshot, vararg arguments: Any?): S
* @throws IllegalStateException if some of types used for encoding cannot be resolved
* @throws EncodeDecodeException if error happened during encoding
*/
fun StorageEntry.storageKeys(runtime: RuntimeSnapshot, keysArguments: List<List<Any?>>): List<String> {
fun StorageEntry.storageKeys(
runtime: RuntimeSnapshot,
keysArguments: List<List<Any?>>
): List<String> {
val argumentsTypes = this.keys
val argumentsHashers = this.hashers

Expand All @@ -173,7 +182,7 @@ fun StorageEntry.storageKeys(runtime: RuntimeSnapshot, keysArguments: List<List<
val argumentType = argumentsTypes[index]
val argumentHasher = argumentsHashers[index]

val keyEncoded = argumentType?.bytes(runtime, key) ?: typeNotResolved(fullName)
val keyEncoded = argumentType?.bytes(runtime, key) ?: storageTypeNotResolved(fullName)

keyOutputStream.write(argumentHasher.hashingFunction(keyEncoded))
}
Expand Down Expand Up @@ -232,7 +241,68 @@ fun Module.fullNameOf(withName: WithName): String {
val StorageEntry.fullName
get() = "$moduleName.$name"

private fun typeNotResolved(entryName: String): Nothing =
fun RuntimeMetadata.runtimeApi(name: String): RuntimeApi {
return runtimeApiOrNull(name) ?: error("Runtime Api $name is not found")
}

fun RuntimeMetadata.runtimeApiOrNull(name: String): RuntimeApi? {
if (apis == null) {
error("This version of metadata does not support auto-detection of runtime apis")
}

return apis.find { it.name == name }
}

fun RuntimeApi.methodOrNull(name: String): RuntimeApiMethod? {
return methods.find { it.name == name }
}

fun RuntimeApi.method(name: String): RuntimeApiMethod {
return methodOrNull(name) ?: error("Method $name is not found in runtime api ${this.name}")
}

fun RuntimeApiMethod.createRequest(
runtime: RuntimeSnapshot,
inputValues: Map<String, Any?>
): StateCallRequest {
return StateCallRequest(
runtimeRpcName = "${apiName}_${name}",
encodedArguments = encodeInputs(runtime, inputValues)
)
}

fun RuntimeApiMethod.encodeInputs(
runtime: RuntimeSnapshot,
inputValues: Map<String, Any?>
): String {
return useScaleWriter {
inputs.forEach { methodParam ->
val inputValue = inputValues.getValue(methodParam.name)

val type = methodParam.type ?: runtimeMethodInputNotResolved(name, methodParam.name)
type.encodeUnsafe(this, runtime, inputValue)
}
}.toHexString(withPrefix = true)
}

fun RuntimeApiMethod.decodeOutput(
runtime: RuntimeSnapshot,
outputEncoded: String
): Any? {
val outputType = output ?: runtimeMethodOutputNotResolved(name)

return outputType.fromHex(runtime, outputEncoded)
}

private fun runtimeMethodInputNotResolved(methodName: String, argumentName: String): Nothing {
error("Cannot resolve type for input $argumentName of $methodName method")
}

private fun runtimeMethodOutputNotResolved(methodName: String): Nothing {
error("Cannot resolve type for output of $methodName method")
}

private fun storageTypeNotResolved(entryName: String): Nothing =
throw IllegalStateException("Cannot resolve key or value type for storage entry `$entryName`")

private fun wrongEntryType(): Nothing =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package io.novasama.substrate_sdk_android.runtime.metadata
import io.emeraldpay.polkaj.scale.ScaleCodecReader
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.runtime.metadata.v14.PostV14MetadataSchema
import io.novasama.substrate_sdk_android.runtime.metadata.v14.PostV14PalletMetadataSchema
import io.novasama.substrate_sdk_android.runtime.metadata.v14.RuntimeMetadataSchemaV14
import io.novasama.substrate_sdk_android.runtime.metadata.v14.RuntimeMetadataSchemaV15
import io.novasama.substrate_sdk_android.runtime.metadata.v15.RuntimeMetadataSchemaV15
import io.novasama.substrate_sdk_android.scale.EncodableStruct
import io.novasama.substrate_sdk_android.scale.Schema
import io.novasama.substrate_sdk_android.scale.uint32
Expand All @@ -15,12 +16,12 @@ object Magic : Schema<Magic>() {
val runtimeVersion by uint8()
}

@Suppress("UNCHECKED_CAST")
class RuntimeMetadataReader private constructor(
val metadataVersion: Int,
val metadata: EncodableStruct<*>
) {

@Suppress("UNCHECKED_CAST")
val metadataPostV14: EncodableStruct<PostV14MetadataSchema<*>>
get() {
require(metadata.schema is PostV14MetadataSchema<*>) {
Expand All @@ -30,6 +31,15 @@ class RuntimeMetadataReader private constructor(
return metadata as EncodableStruct<PostV14MetadataSchema<*>>
}

val metadataV15: EncodableStruct<RuntimeMetadataSchemaV15>
get() {
require(metadata.schema is RuntimeMetadataSchemaV15) {
"Metadata is not v15"
}

return metadata as EncodableStruct<RuntimeMetadataSchemaV15>
}

companion object {

fun read(metadaScale: String): RuntimeMetadataReader {
Expand Down Expand Up @@ -57,7 +67,6 @@ class RuntimeMetadataReader private constructor(
return read(scaleCoderReader)
}

@OptIn(ExperimentalUnsignedTypes::class)
private fun read(reader: ScaleCodecReader): RuntimeMetadataReader {
val runtimeVersion = Magic.read(reader)[Magic.runtimeVersion].toInt()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface RuntimeBuilder {
fun buildMetadata(
reader: RuntimeMetadataReader,
typeRegistry: TypeRegistry,
knownSignedExtensions: List<SignedExtensionMetadata> = DefaultSignedExtensions.ALL,
fallbackSignedExtensions: List<SignedExtensionMetadata> = DefaultSignedExtensions.ALL,
): RuntimeMetadata
}

Expand All @@ -20,11 +20,12 @@ object VersionedRuntimeBuilder : RuntimeBuilder {
override fun buildMetadata(
reader: RuntimeMetadataReader,
typeRegistry: TypeRegistry,
knownSignedExtensions: List<SignedExtensionMetadata>,
fallbackSignedExtensions: List<SignedExtensionMetadata>,
): RuntimeMetadata {
return when (reader.metadataVersion) {
14, 15 -> PostV14RuntimeBuilder.buildMetadata(reader, typeRegistry, knownSignedExtensions)
else -> V13RuntimeBuilder.buildMetadata(reader, typeRegistry, knownSignedExtensions)
15 -> V15RuntimeBuilder.buildMetadata(reader, typeRegistry, fallbackSignedExtensions)
14 -> V14RuntimeBuilder.buildMetadata(reader, typeRegistry, fallbackSignedExtensions)
else -> V13RuntimeBuilder.buildMetadata(reader, typeRegistry, fallbackSignedExtensions)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ import io.novasama.substrate_sdk_android.runtime.metadata.module.StorageEntry
import io.novasama.substrate_sdk_android.runtime.metadata.module.StorageEntryType
import io.novasama.substrate_sdk_android.scale.EncodableStruct

@OptIn(ExperimentalUnsignedTypes::class)
internal object V13RuntimeBuilder : RuntimeBuilder {

override fun buildMetadata(
reader: RuntimeMetadataReader,
typeRegistry: TypeRegistry,
knownSignedExtensions: List<SignedExtensionMetadata>
fallbackSignedExtensions: List<SignedExtensionMetadata>
): RuntimeMetadata {
val metadataStruct = reader.metadata

Expand All @@ -45,10 +44,11 @@ internal object V13RuntimeBuilder : RuntimeBuilder {
return RuntimeMetadata(
extrinsic = buildExtrinsic(
struct = metadataStruct[RuntimeMetadataSchema.extrinsic],
knownSignedExtensions = knownSignedExtensions
knownSignedExtensions = fallbackSignedExtensions
),
modules = buildModules(metadataStruct[RuntimeMetadataSchema.modules], typeRegistry),
metadataVersion = reader.metadataVersion
metadataVersion = reader.metadataVersion,
apis = null
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,12 @@ import io.novasama.substrate_sdk_android.runtime.metadata.v14.StorageMetadataV14
import io.novasama.substrate_sdk_android.scale.EncodableStruct
import java.math.BigInteger

@OptIn(ExperimentalUnsignedTypes::class)
object PostV14RuntimeBuilder : RuntimeBuilder {
object V14RuntimeBuilder : RuntimeBuilder {

override fun buildMetadata(
reader: RuntimeMetadataReader,
typeRegistry: TypeRegistry,
knownSignedExtensions: List<SignedExtensionMetadata>,
fallbackSignedExtensions: List<SignedExtensionMetadata>,
): RuntimeMetadata {
val metadataStruct = reader.metadata

Expand All @@ -53,7 +52,8 @@ object PostV14RuntimeBuilder : RuntimeBuilder {
typeRegistry
),
modules = buildModules(metadataStruct[schema.pallets], typeRegistry),
metadataVersion = reader.metadataVersion
metadataVersion = reader.metadataVersion,
apis = null
)
}

Expand Down Expand Up @@ -122,7 +122,7 @@ object PostV14RuntimeBuilder : RuntimeBuilder {
moduleIndex: Int,
): Map<String, MetadataFunction> {

val type = typeRegistry[callsRaw[PalletCallMetadataV14.type].toString()]
val type = typeRegistry[callsRaw[PalletCallMetadataV14.type]]

if (type !is DictEnum) return emptyMap()

Expand All @@ -144,7 +144,7 @@ object PostV14RuntimeBuilder : RuntimeBuilder {
moduleIndex: Int,
): Map<String, Event> {

val type = typeRegistry[eventsRaw[PalletEventMetadataV14.type].toString()]
val type = typeRegistry[eventsRaw[PalletEventMetadataV14.type]]

if (type !is DictEnum) return emptyMap()

Expand Down Expand Up @@ -180,7 +180,7 @@ object PostV14RuntimeBuilder : RuntimeBuilder {
): Map<String, Constant> {

return constantsRaw.map { constantStruct ->
val typeIndex = constantStruct[PalletConstantMetadataV14.type].toString()
val typeIndex = constantStruct[PalletConstantMetadataV14.type]

Constant(
name = constantStruct[PalletConstantMetadataV14.name],
Expand All @@ -196,7 +196,7 @@ object PostV14RuntimeBuilder : RuntimeBuilder {
errorsRaw: EncodableStruct<PalletErrorMetadataV14>,
): Map<Int, ErrorMetadata> {

val type = typeRegistry[errorsRaw[PalletErrorMetadataV14.type].toString()]
val type = typeRegistry[errorsRaw[PalletErrorMetadataV14.type]]

if (type !is DictEnum) return emptyMap()

Expand All @@ -215,16 +215,17 @@ object PostV14RuntimeBuilder : RuntimeBuilder {
): StorageEntryType {
return when (enumValue) {
is BigInteger -> {
StorageEntryType.Plain(typeRegistry[enumValue.toString()])
StorageEntryType.Plain(typeRegistry[enumValue])
}

is EncodableStruct<*> -> {
requireOrException(enumValue.schema is MapTypeV14) {
cannotConstructStorageEntry(enumValue)
}

val hashers = enumValue[MapTypeV14.hashers]

val type = typeRegistry[enumValue[MapTypeV14.key].toString()]
val type = typeRegistry[enumValue[MapTypeV14.key]]
?: cannotConstructStorageEntry(enumValue)

val keys = if (hashers.size == 1) {
Expand All @@ -244,9 +245,10 @@ object PostV14RuntimeBuilder : RuntimeBuilder {
StorageEntryType.NMap(
keys,
hashers,
typeRegistry[enumValue[MapTypeV14.value].toString()]
typeRegistry[enumValue[MapTypeV14.value]]
)
}

else -> cannotConstructStorageEntry(enumValue)
}
}
Expand All @@ -262,8 +264,8 @@ object PostV14RuntimeBuilder : RuntimeBuilder {
signedExtensions = struct[schema.signedExtensions].map {
SignedExtensionMetadata(
id = it[SignedExtensionMetadataV14.identifier],
includedInExtrinsic = typeRegistry[it[SignedExtensionMetadataV14.type].toString()],
includedInSignature = typeRegistry[it[SignedExtensionMetadataV14.additionalSigned].toString()]
includedInExtrinsic = typeRegistry[it[SignedExtensionMetadataV14.type]],
includedInSignature = typeRegistry[it[SignedExtensionMetadataV14.additionalSigned]]
)
}
)
Expand Down
Loading

0 comments on commit 323610d

Please sign in to comment.