Skip to content

Commit

Permalink
Enrich sendable extrinsic (#100)
Browse files Browse the repository at this point in the history
* Add non-encoded extrinsic to SendableExtrinsic

* bump versions

* Remove CallRepresentation from Extrinsic

* Expose Extrinsic encoding without a length

* Code style
  • Loading branch information
valentunn authored Dec 8, 2024
1 parent c1d44d7 commit dbe13fe
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 136 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
buildscript {
ext {
// App version
versionName = '2.3.0'
versionName = '2.4.0'
versionCode = 1

// SDK and tools
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,24 @@ package io.novasama.substrate_sdk_android.runtime.definitions.types.generics
import io.emeraldpay.polkaj.scale.ScaleCodecReader
import io.emeraldpay.polkaj.scale.ScaleCodecWriter
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.Type
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.toByteArray
import io.novasama.substrate_sdk_android.runtime.definitions.types.useScaleWriter
import io.novasama.substrate_sdk_android.scale.dataType.byte
import io.novasama.substrate_sdk_android.scale.dataType.compactInt
import io.novasama.substrate_sdk_android.scale.dataType.toByteArray
import io.novasama.substrate_sdk_android.scale.utils.directWrite

private val SIGNED_MASK = 0b1000_0000.toUByte()

private const val TYPE_ADDRESS = "Address"
private const val TYPE_SIGNATURE = "ExtrinsicSignature"

@OptIn(ExperimentalUnsignedTypes::class)
object Extrinsic : RuntimeType<Extrinsic.EncodingInstance, Extrinsic.DecodedInstance>("ExtrinsicsDecoder") {
object Extrinsic : Type<Extrinsic.Instance>("ExtrinsicsDecoder") {

class EncodingInstance(
val signature: Signature?,
val callRepresentation: CallRepresentation
) {
sealed class CallRepresentation {

class Instance(val call: GenericCall.Instance) : CallRepresentation()

class Bytes(val bytes: ByteArray) : CallRepresentation()
}
}

class DecodedInstance(
class Instance(
val signature: Signature?,
val call: GenericCall.Instance
)
Expand All @@ -56,7 +45,7 @@ object Extrinsic : RuntimeType<Extrinsic.EncodingInstance, Extrinsic.DecodedInst
override fun decode(
scaleCodecReader: ScaleCodecReader,
runtime: RuntimeSnapshot
): DecodedInstance {
): Instance {
val length = compactInt.read(scaleCodecReader)

val extrinsicVersion = byte.read(scaleCodecReader).toUByte()
Expand All @@ -73,30 +62,34 @@ object Extrinsic : RuntimeType<Extrinsic.EncodingInstance, Extrinsic.DecodedInst

val call = GenericCall.decode(scaleCodecReader, runtime)

return DecodedInstance(signature, call)
return Instance(signature, call)
}

override fun encode(
scaleCodecWriter: ScaleCodecWriter,
runtime: RuntimeSnapshot,
value: EncodingInstance
value: Instance
) {
val callBytes = when (value.callRepresentation) {
is EncodingInstance.CallRepresentation.Instance ->
GenericCall.toByteArray(runtime, value.callRepresentation.call)

is EncodingInstance.CallRepresentation.Bytes -> value.callRepresentation.bytes
}
encode(scaleCodecWriter, runtime, value, encodeLength = true)
}

encode(scaleCodecWriter, runtime, value.signature, callBytes)
fun encodeWithoutLength(
scaleCodecWriter: ScaleCodecWriter,
runtime: RuntimeSnapshot,
value: Instance
) {
encode(scaleCodecWriter, runtime, value, encodeLength = false)
}

private fun encode(
scaleCodecWriter: ScaleCodecWriter,
runtime: RuntimeSnapshot,
signature: Signature?,
callBytes: ByteArray
value: Instance,
encodeLength: Boolean,
) {
val signature = value.signature
val callBytes = GenericCall.toByteArray(runtime, value.call)

val isSigned = signature != null

val extrinsicVersion = runtime.metadata.extrinsic.version.toInt().toUByte()
Expand All @@ -116,11 +109,15 @@ object Extrinsic : RuntimeType<Extrinsic.EncodingInstance, Extrinsic.DecodedInst

val extrinsicBodyBytes = byteArrayOf(encodedVersion) + signatureWrapperBytes + callBytes

Bytes.encode(scaleCodecWriter, runtime, extrinsicBodyBytes)
if (encodeLength) {
Bytes.encode(scaleCodecWriter, runtime, extrinsicBodyBytes)
} else {
scaleCodecWriter.directWrite(extrinsicBodyBytes)
}
}

override fun isValidInstance(instance: Any?): Boolean {
return instance is EncodingInstance
return instance is Instance
}

private fun encodedVersion(version: UByte, isSigned: Boolean): UByte {
Expand All @@ -144,3 +141,7 @@ object Extrinsic : RuntimeType<Extrinsic.EncodingInstance, Extrinsic.DecodedInst
throw EncodeDecodeException("Cannot resolve $name type, which is required to work with Extrinsic")
}
}

fun Extrinsic.toBytesWithoutLength(runtime: RuntimeSnapshot, value: Extrinsic.Instance): ByteArray {
return useScaleWriter { encodeWithoutLength(this, runtime, value) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,3 @@ fun multiAddressFromId(addressId: ByteArray): DictEnum.Entry<ByteArray> {
value = addressId
)
}

fun Extrinsic.EncodingInstance(
signature: Extrinsic.Signature?,
call: GenericCall.Instance
): Extrinsic.EncodingInstance {
return Extrinsic.EncodingInstance(
signature,
Extrinsic.EncodingInstance.CallRepresentation.Instance(call)
)
}

fun Extrinsic.EncodingInstance(
signature: Extrinsic.Signature?,
callBytes: ByteArray
): Extrinsic.EncodingInstance {
return Extrinsic.EncodingInstance(
signature,
Extrinsic.EncodingInstance.CallRepresentation.Bytes(callBytes)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,11 @@ import io.novasama.substrate_sdk_android.runtime.definitions.types.RuntimeType
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.DefaultSignedExtensions
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Era
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.ExtrinsicPayloadExtrasInstance
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.new
import io.novasama.substrate_sdk_android.runtime.definitions.types.instances.AddressInstanceConstructor
import io.novasama.substrate_sdk_android.runtime.definitions.types.instances.SignatureInstanceConstructor
import io.novasama.substrate_sdk_android.runtime.definitions.types.toHex
import io.novasama.substrate_sdk_android.runtime.definitions.types.toHexUntyped
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SendableExtrinsic
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignedExtrinsic
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.Signer
Expand Down Expand Up @@ -114,14 +111,7 @@ class ExtrinsicBuilder(
batchMode: BatchMode = BatchMode.BATCH
): SendableExtrinsic {
val call = maybeWrapInBatch(batchMode)
return buildSendableExtrinsic(CallRepresentation.Instance(call))
}

suspend fun buildExtrinsic(
rawCallBytes: ByteArray
): SendableExtrinsic {
requireNotMixingBytesAndInstanceCalls()
return buildSendableExtrinsic(CallRepresentation.Bytes(rawCallBytes))
return buildSendableExtrinsic(call)
}

private fun maybeWrapInBatch(batchMode: BatchMode): GenericCall.Instance {
Expand All @@ -134,13 +124,11 @@ class ExtrinsicBuilder(
}
}

private suspend fun buildSendableExtrinsic(
callRepresentation: CallRepresentation
): SendableExtrinsic {
private suspend fun buildSendableExtrinsic(call: GenericCall.Instance): SendableExtrinsic {
val signerPayload = SignerPayloadExtrinsic(
runtime = runtime,
accountId = accountId,
call = callRepresentation,
call = call,
signedExtras = SignerPayloadExtrinsic.SignedExtras(
includedInExtrinsic = buildIncludedInExtrinsic(),
includedInSignature = buildIncludedInSignature()
Expand All @@ -150,8 +138,25 @@ class ExtrinsicBuilder(
)

val signedExtrinsic = signer.signExtrinsic(signerPayload)
val instance = buildEncodingExtrinsic(signedExtrinsic)

return SendableExtrinsic(runtime, instance)
}

return RealSendableExtrinsic(signedExtrinsic)
private fun buildEncodingExtrinsic(signedExtrinsic: SignedExtrinsic): Extrinsic.Instance {
val address = buildEncodableAddressInstance(signedExtrinsic.payload.accountId)
val multiSignature = signatureConstructor.constructInstance(
runtime.typeRegistry,
signedExtrinsic.signatureWrapper
)
return Extrinsic.Instance(
signature = Extrinsic.Signature.new(
accountIdentifier = address,
signature = multiSignature,
signedExtras = signedExtrinsic.payload.signedExtras.includedInExtrinsic
),
call = signedExtrinsic.payload.call
)
}

private fun buildIncludedInSignature(): Map<String, Any?> {
Expand Down Expand Up @@ -223,41 +228,4 @@ class ExtrinsicBuilder(
"Cannot mix instance and raw bytes calls"
}
}

private inner class RealSendableExtrinsic(
private val signedExtrinsic: SignedExtrinsic
) : SendableExtrinsic {

private val multiSignature = signatureConstructor.constructInstance(
runtime.typeRegistry,
signedExtrinsic.signatureWrapper
)

override val extrinsicHex by lazy {
createExtrinsicHex()
}

override val signatureHex by lazy {
createSignatureHex()
}

private fun createExtrinsicHex(): String {
val address = buildEncodableAddressInstance(signedExtrinsic.payload.accountId)
val extrinsic = Extrinsic.EncodingInstance(
signature = Extrinsic.Signature.new(
accountIdentifier = address,
signature = multiSignature,
signedExtras = signedExtrinsic.payload.signedExtras.includedInExtrinsic
),
callRepresentation = signedExtrinsic.payload.call
)

return Extrinsic.toHex(runtime, extrinsic)
}

private fun createSignatureHex(): String {
val signatureType = Extrinsic.signatureType(runtime)
return signatureType.toHexUntyped(runtime, multiSignature)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,57 @@
package io.novasama.substrate_sdk_android.runtime.extrinsic.signer

import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.toBytesWithoutLength
import io.novasama.substrate_sdk_android.runtime.definitions.types.toHex
import io.novasama.substrate_sdk_android.runtime.definitions.types.toHexUntyped

interface SendableExtrinsic {

val extrinsic: Extrinsic.Instance

val signatureHex: String

val extrinsicHex: String

val bytesWithoutLength: ByteArray
}

fun SendableExtrinsic(
runtime: RuntimeSnapshot,
extrinsic: Extrinsic.Instance
): SendableExtrinsic {
return RealSendableExtrinsic(runtime, extrinsic)
}

private class RealSendableExtrinsic(
private val runtime: RuntimeSnapshot,
override val extrinsic: Extrinsic.Instance
) : SendableExtrinsic {

override val extrinsicHex by lazy {
createExtrinsicHex()
}

override val bytesWithoutLength: ByteArray by lazy {
Extrinsic.toBytesWithoutLength(runtime, extrinsic)
}

override val signatureHex by lazy {
createSignatureHex()
}

private fun createExtrinsicHex(): String {
return Extrinsic.toHex(runtime, extrinsic)
}

private fun createSignatureHex(): String {
requireNotNull(extrinsic.signature) {
"Extrinsic is unsigned"
}

val signatureType = Extrinsic.signatureType(runtime)

return signatureType.toHexUntyped(runtime, extrinsic.signature.signature)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.extensions.fromHex
import io.novasama.substrate_sdk_android.runtime.AccountId
import io.novasama.substrate_sdk_android.runtime.RuntimeSnapshot
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic.EncodingInstance.CallRepresentation
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.extrinsic.Nonce

class SignedExtrinsic(
Expand All @@ -28,7 +28,7 @@ class SignerPayloadRaw(
data class SignerPayloadExtrinsic(
val runtime: RuntimeSnapshot,
val accountId: AccountId,
val call: CallRepresentation,
val call: GenericCall.Instance,
val signedExtras: SignedExtras,
val nonce: Nonce,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import io.novasama.substrate_sdk_android.hash.Hasher.blake2b256
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.DefaultSignedExtensions
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.ExtrasIncludedInExtrinsic
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.ExtrasIncludedInSignature
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
import io.novasama.substrate_sdk_android.runtime.definitions.types.useScaleWriter
import io.novasama.substrate_sdk_android.scale.utils.directWrite

private const val PAYLOAD_HASH_THRESHOLD = 256

Expand All @@ -18,13 +16,7 @@ fun bytesOf(vararg writers: (ScaleCodecWriter) -> Unit) = useScaleWriter {
}

fun SignerPayloadExtrinsic.encodeCallDataTo(writer: ScaleCodecWriter) {
when (call) {
is Extrinsic.EncodingInstance.CallRepresentation.Bytes ->
writer.directWrite(call.bytes)

is Extrinsic.EncodingInstance.CallRepresentation.Instance ->
GenericCall.encode(writer, runtime, call.call)
}
GenericCall.encode(writer, runtime, call)
}

fun SignerPayloadExtrinsic.encodedCallData() = bytesOf(::encodeCallDataTo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import io.novasama.substrate_sdk_android.extensions.toHexString
import io.novasama.substrate_sdk_android.runtime.RealRuntimeProvider
import io.novasama.substrate_sdk_android.runtime.definitions.types.composite.DictEnum
import io.novasama.substrate_sdk_android.runtime.definitions.types.fromHex
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.Extrinsic.EncodingInstance.*
import io.novasama.substrate_sdk_android.runtime.definitions.types.instances.AddressInstanceConstructor
import io.novasama.substrate_sdk_android.runtime.definitions.types.instances.SignatureInstanceConstructor
import io.novasama.substrate_sdk_android.runtime.definitions.types.toHex
Expand Down Expand Up @@ -73,7 +72,7 @@ class ExtrinsicTest {
value = SignatureWrapper.Sr25519(signatureInHex.fromHex())
)

val extrinsic = Extrinsic.EncodingInstance(
val extrinsic = Extrinsic.Instance(
signature = Extrinsic.Signature.new(
accountIdentifier = AddressInstanceConstructor.constructInstance(
typeRegistry = runtime.typeRegistry,
Expand All @@ -82,7 +81,7 @@ class ExtrinsicTest {
signature = signature,
signedExtras = signedExtras
),
callRepresentation = CallRepresentation.Instance(call)
call = call
)

val encoded = Extrinsic.toHex(runtime, extrinsic)
Expand Down
Loading

0 comments on commit dbe13fe

Please sign in to comment.