├── substrate-client ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── nodle │ │ │ └── substratesdk │ │ │ ├── utils │ │ │ ├── Debug.kt │ │ │ ├── Hex.kt │ │ │ └── xxHash.kt │ │ │ ├── rpc │ │ │ ├── ISubstrateRpc.kt │ │ │ ├── JsonObjectBuilder.kt │ │ │ ├── RpcMethod.kt │ │ │ ├── SubstrateRpc.kt │ │ │ ├── SubstrateProvider.kt │ │ │ ├── HttpRpc.kt │ │ │ └── WebSocketRpc.kt │ │ │ ├── account │ │ │ ├── Signer.kt │ │ │ ├── SS58.kt │ │ │ ├── Account.kt │ │ │ └── Mnemonic.kt │ │ │ ├── scale │ │ │ ├── Call+Encoder.kt │ │ │ ├── v2 │ │ │ │ ├── Call+Encoder.kt │ │ │ │ ├── AccountInfo+Parser.kt │ │ │ │ └── Extrinsic+Encoder.kt │ │ │ ├── MultiAddress+Encoder.kt │ │ │ ├── v1 │ │ │ │ └── AccountInfo+Parser.kt │ │ │ ├── AccountInfo+Parser.kt │ │ │ ├── MultiAddress+Parser.kt │ │ │ ├── Extrinsic+Encoder.kt │ │ │ ├── BasicType+Encoder.kt │ │ │ ├── BasicType+Parser.kt │ │ │ └── RuntimeMetadata+Parser.kt │ │ │ ├── types │ │ │ ├── ExtrinsicCall.kt │ │ │ ├── AccountInfo.kt │ │ │ ├── Extrinsic.kt │ │ │ ├── MultiAddress.kt │ │ │ └── RuntimeMetadata.kt │ │ │ └── SubstrateApi.kt │ └── test │ │ ├── resources │ │ └── simplelogger.properties │ │ └── java │ │ └── io │ │ └── nodle │ │ └── substratesdk │ │ ├── TestEd25519.kt │ │ ├── TestRpc.kt │ │ └── TestMnemonic.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── .gitignore ├── gradlew.bat ├── README.md ├── ATTRIBUTION.yml ├── gradlew └── LICENSE.txt /substrate-client/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | rpc-test.config 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='substrate-client-kotlin' 2 | include ':substrate-client' 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NodleCode/substrate-client-kotlin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/utils/Debug.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.utils 2 | 3 | /** 4 | * @author Lucien Loiseau on 14/07/20. 5 | */ 6 | fun onDebugOnly(job: () -> Unit) { 7 | job() 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 26 15:39:59 SGT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip 7 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/rpc/ISubstrateRpc.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.rpc 2 | 3 | import io.reactivex.rxjava3.core.Single 4 | 5 | /** 6 | * @author Lucien Loiseau on 12/05/21. 7 | */ 8 | 9 | class NullJsonObjectException : Exception() 10 | 11 | interface ISubstrateRpc { 12 | 13 | fun send(method: RpcMethod, defaultValue: T? = null): Single 14 | 15 | fun url() : String 16 | 17 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/account/Signer.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.account 2 | 3 | import org.bouncycastle.crypto.Signer 4 | import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters 5 | import org.bouncycastle.crypto.signers.Ed25519Signer 6 | 7 | /** 8 | * @author Lucien Loiseau on 30/06/20. 9 | */ 10 | fun Ed25519PrivateKeyParameters.signMsg(msg: ByteArray) : ByteArray { 11 | val signer: Signer = Ed25519Signer() 12 | signer.init(true, this) 13 | signer.update(msg, 0, msg.size) 14 | return signer.generateSignature() 15 | } 16 | 17 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/Call+Encoder.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale 2 | 3 | import io.nodle.substratesdk.types.ExtrinsicCall 4 | import io.nodle.substratesdk.types.TransferCall 5 | 6 | /** 7 | * @author Lucien Loiseau on 30/07/20. 8 | */ 9 | fun ExtrinsicCall.toU8a(): ByteArray { 10 | return (this as? TransferCall) 11 | ?.toU8a() 12 | ?: byteArrayOf() 13 | } 14 | 15 | fun TransferCall.toU8a(): ByteArray { 16 | return byteArrayOf(moduleIndex.toByte(), callIndex.toByte()) + 17 | destAccount.toMultiAddress().toU8a() + 18 | amount.toCompactU8a() 19 | } 20 | 21 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/utils/Hex.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.utils 2 | 3 | /** 4 | * @author Lucien Loiseau on 13/08/20. 5 | */ 6 | /** 7 | * @author Lucien Loiseau on 30/07/20. 8 | */ 9 | fun ByteArray.toHex() = 10 | this.joinToString(separator = "") { it.toInt().and(0xff).toString(16).padStart(2, '0') } 11 | 12 | fun String.hexToBa() = removePrefix("0x").pureHexToBa() 13 | 14 | fun String.pureHexToBa() = ByteArray(this.length / 2) { this.substring(it * 2, it * 2 + 2).toInt(16).toByte() } 15 | 16 | fun ByteArray.trimTrailingZeros() : ByteArray = this.dropLastWhile { it == 0x00.toByte() }.toByteArray() 17 | 18 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/v2/Call+Encoder.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale.v2 2 | 3 | import io.nodle.substratesdk.scale.toCompactU8a 4 | import io.nodle.substratesdk.types.* 5 | 6 | /** 7 | * @author Lucien Loiseau on 30/07/20. 8 | */ 9 | 10 | fun ExtrinsicCall.toU8aV2(): ByteArray { 11 | return (this as? TransferCall) 12 | ?.toU8aV2() 13 | ?: byteArrayOf() 14 | } 15 | 16 | fun TransferCall.toU8aV2(): ByteArray { 17 | return byteArrayOf(moduleIndex.toByte(), callIndex.toByte()) + 18 | byteArrayOf(0xff.toByte()) + 19 | destAccount.toU8a() + 20 | amount.toCompactU8a() 21 | } 22 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/types/ExtrinsicCall.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.types 2 | 3 | import io.nodle.substratesdk.account.Account 4 | import io.nodle.substratesdk.rpc.SubstrateProvider 5 | import io.reactivex.rxjava3.core.Single 6 | import java.lang.Exception 7 | import java.math.BigInteger 8 | 9 | /** 10 | * @author Lucien Loiseau on 29/06/20. 11 | */ 12 | 13 | abstract class ExtrinsicCall 14 | 15 | data class TransferCall( 16 | val moduleIndex: Int, 17 | val callIndex: Int, 18 | val destAccount: Account, 19 | val amount: BigInteger 20 | ) : ExtrinsicCall() 21 | 22 | class CallNotFoundException(msg: String?) : Exception(msg) -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/rpc/JsonObjectBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.rpc 2 | 3 | import org.json.JSONObject 4 | import java.util.* 5 | 6 | /** 7 | * @author Lucien Loiseau on 14/07/20. 8 | */ 9 | class JsonObjectBuilder { 10 | private val deque: Deque = LinkedList() 11 | 12 | fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { 13 | deque.push(JSONObject()) 14 | this.build() 15 | return deque.pop() 16 | } 17 | 18 | infix fun String.to(value: T) { 19 | deque.peek().put(this, value) 20 | } 21 | } 22 | 23 | public fun json(build: JsonObjectBuilder.() -> Unit): JSONObject { 24 | return JsonObjectBuilder().json(build) 25 | } 26 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/MultiAddress+Encoder.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale 2 | 3 | import io.nodle.substratesdk.types.* 4 | 5 | /** 6 | * @author Lucien Loiseau on 09/04/21. 7 | */ 8 | 9 | fun MultiAddress.toU8a(): ByteArray { 10 | return type.id.toByte().toU8a() + 11 | when (type) { 12 | AddressType.AccountID -> (this as AccountIDAddress).pubkey 13 | AddressType.Raw -> (this as RawAddress).bytes.toU8a() 14 | AddressType.Address20 -> (this as Address20).bytes 15 | AddressType.Address32 -> (this as Address32).bytes 16 | else -> throw ScaleParserException("unsupported multiaddress type: ${type.name}") 17 | } 18 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/types/AccountInfo.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.types 2 | 3 | import java.math.BigInteger 4 | 5 | /** 6 | * @author Lucien Loiseau on 14/07/20. 7 | */ 8 | data class AccountInfo( 9 | var nonce: UInt, 10 | var consumers: UInt, 11 | var providers: UInt, 12 | var data: AccountData 13 | ) 14 | 15 | data class AccountData( 16 | var free: BigInteger, 17 | var reserved: BigInteger, 18 | var miscFrozen: BigInteger, 19 | var feeFrozen: BigInteger 20 | ) 21 | 22 | val nullAccountInfo = AccountInfo( 23 | 0.toUInt(), 24 | 0.toUInt(), 25 | 0.toUInt(), 26 | AccountData( 27 | 0.toBigInteger(), 28 | 0.toBigInteger(), 29 | 0.toBigInteger(), 30 | 0.toBigInteger() 31 | ) 32 | ) -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/v1/AccountInfo+Parser.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale.v1 2 | 3 | import io.nodle.substratesdk.scale.readU128 4 | import io.nodle.substratesdk.scale.readU32 5 | import io.nodle.substratesdk.scale.readU8 6 | import io.nodle.substratesdk.types.AccountData 7 | import io.nodle.substratesdk.types.AccountInfo 8 | import java.nio.ByteBuffer 9 | import java.nio.ByteOrder 10 | 11 | /** 12 | * @author Lucien Loiseau on 09/04/21. 13 | */ 14 | 15 | fun ByteBuffer.readAccountInfoV1(): AccountInfo { 16 | return AccountInfo( 17 | readU32(), 18 | readU8().toUInt(), 19 | 0.toUInt(), 20 | AccountData( 21 | readU128(), 22 | readU128(), 23 | readU128(), 24 | readU128() 25 | ) 26 | ) 27 | } -------------------------------------------------------------------------------- /substrate-client/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/AccountInfo+Parser.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale 2 | 3 | import io.nodle.substratesdk.types.AccountData 4 | import io.nodle.substratesdk.types.AccountInfo 5 | import java.nio.ByteBuffer 6 | import java.nio.ByteOrder 7 | 8 | /** 9 | * @author Lucien Loiseau on 30/07/20. 10 | */ 11 | 12 | class ScaleEncoderException(msg: String?) : Exception(msg) 13 | 14 | // with the release of Substrate 3.0, the refCount has been changed into two UInt32 15 | // consumers and producers 16 | fun ByteBuffer.readAccountInfoSub3(): AccountInfo { 17 | return AccountInfo( 18 | readU32(), 19 | readU32(), 20 | readU32(), 21 | AccountData( 22 | readU128(), 23 | readU128(), 24 | readU128(), 25 | readU128() 26 | ) 27 | ) 28 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/MultiAddress+Parser.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale 2 | 3 | import io.nodle.substratesdk.types.* 4 | import java.nio.ByteBuffer 5 | 6 | /** 7 | * @author Lucien Loiseau on 09/04/21. 8 | */ 9 | 10 | @ExperimentalUnsignedTypes 11 | fun ByteBuffer.readMultiAddress(): MultiAddress = 12 | when (val type = this.readU8().toInt()) { 13 | AddressType.AccountID.id -> { 14 | val ba = ByteArray(32) 15 | get(ba) 16 | AccountIDAddress(ba) 17 | } 18 | AddressType.Raw.id -> RawAddress(readByteArray()) 19 | AddressType.Address20.id -> { 20 | val ba = ByteArray(20) 21 | get(ba) 22 | Address20(ba) 23 | } 24 | AddressType.Address32.id -> { 25 | val ba = ByteArray(32) 26 | get(ba) 27 | Address32(ba) 28 | } 29 | else -> throw ScaleParserException("unsupported multiaddress type: ${type}") 30 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/rpc/RpcMethod.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.rpc 2 | 3 | import org.json.JSONArray 4 | 5 | /** 6 | * @author Lucien Loiseau on 14/07/20. 7 | */ 8 | open class RpcMethod(val method: String, val params: JSONArray) 9 | 10 | class ChainGetBlockHash(blockNumber: Int) : 11 | RpcMethod("chain_getBlockHash", JSONArray(arrayOf(blockNumber))) 12 | 13 | class StateGetKeys(keyPrefix: String) : 14 | RpcMethod("state_getKeys", JSONArray(arrayOf(keyPrefix))) 15 | 16 | class StateGetMetadata : 17 | RpcMethod("state_getMetadata", JSONArray()) 18 | 19 | class StateGetRuntimeVersion : 20 | RpcMethod("state_getRuntimeVersion", JSONArray()) 21 | 22 | class StateGetStorage(key: String) : 23 | RpcMethod("state_getStorage", JSONArray(arrayOf(key))) 24 | 25 | class AuthorSubmitExtrinsic(extrinsic: String) : 26 | RpcMethod("author_submitExtrinsic", JSONArray(arrayOf(extrinsic))) 27 | 28 | class PaymentQueryInfo(extrinsic: String) : 29 | RpcMethod("payment_queryInfo", JSONArray(arrayOf(extrinsic))) 30 | 31 | 32 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/v2/AccountInfo+Parser.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale.v2 2 | 3 | import io.nodle.substratesdk.scale.readU128 4 | import io.nodle.substratesdk.scale.readU32 5 | import io.nodle.substratesdk.types.AccountData 6 | import io.nodle.substratesdk.types.AccountInfo 7 | import java.nio.ByteBuffer 8 | import java.nio.ByteOrder 9 | 10 | /** 11 | * @author Lucien Loiseau on 30/07/20. 12 | */ 13 | 14 | class ScaleEncoderException(msg: String?) : Exception(msg) 15 | 16 | // with the release of Substrate 2.0, the refCount is represented as u32 instead of u8 17 | // Substrate v2.0.0 https://github.com/paritytech/substrate/releases/tag/v2.0.0 18 | // Refcounts are now u32 (#7164) https://github.com/paritytech/substrate/pull/7164 19 | fun ByteBuffer.readAccountInfoV12(): AccountInfo { 20 | return AccountInfo( 21 | readU32(), 22 | readU32(), 23 | 0.toUInt(), 24 | AccountData( 25 | readU128(), 26 | readU128(), 27 | readU128(), 28 | readU128() 29 | ) 30 | ) 31 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/types/Extrinsic.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.types 2 | 3 | import java.math.BigInteger 4 | 5 | /** 6 | * @author Lucien Loiseau on 26/06/20. 7 | */ 8 | data class Extrinsic( 9 | val signature: ExtrinsicSignature, 10 | val method: ExtrinsicCall 11 | ) 12 | 13 | data class ExtrinsicSignature( 14 | val signer: MultiAddress, 15 | val signature: ExtrinsicEd25519Signature, 16 | val era: ExtrinsicEra, 17 | val nonce: Long, 18 | val tip: BigInteger 19 | ) 20 | 21 | data class ExtrinsicEd25519Signature(val signature: ByteArray) 22 | 23 | abstract class ExtrinsicEra 24 | 25 | data class ImmortalEra(val value: Long = 0x00) : ExtrinsicEra() 26 | 27 | data class MortalEra(val period: Long, val phase: Long) : ExtrinsicEra() 28 | 29 | data class ExtrinsicPayload( 30 | val blockHash: ByteArray, 31 | val era: ExtrinsicEra, 32 | val genesisHash: ByteArray, 33 | val method: ExtrinsicCall, 34 | val nonce: Long, 35 | val specVersion: Long, 36 | val tip: BigInteger, 37 | val transactionVersion: Long 38 | ) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/account/SS58.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.account 2 | 3 | import org.bouncycastle.crypto.digests.Blake2bDigest 4 | import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters 5 | import org.komputing.kbase58.decodeBase58 6 | import org.komputing.kbase58.encodeToBase58String 7 | import java.lang.Exception 8 | 9 | /** 10 | * @author Lucien Loiseau on 28/05/20. 11 | * REFERENCE: https://github.com/paritytech/substrate/wiki/External-Address-Format-(SS58) 12 | */ 13 | private val contextPrefix = "SS58PRE".toByteArray() 14 | 15 | fun ByteArray.toSS58(substrateID: Byte = 0x2a): String { 16 | val body = byteArrayOf(substrateID) + this 17 | 18 | // documentation says blake2b-256 but reference implementation use 512 bits 19 | // https://github.com/paritytech/substrate/blob/master/primitives/core/src/crypto.rs 20 | val b2b = Blake2bDigest(512) 21 | val checksum = ByteArray(b2b.digestSize) 22 | b2b.update(contextPrefix, 0, contextPrefix.size) 23 | b2b.update(body, 0, body.size) 24 | b2b.doFinal(checksum, 0) 25 | 26 | return (body + checksum.slice(0..1)).encodeToBase58String() 27 | } 28 | 29 | fun String.ss58ToBa(): ByteArray = decodeBase58().drop(1).dropLast(2).toByteArray() -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/v2/Extrinsic+Encoder.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale.v2 2 | 3 | import io.nodle.substratesdk.scale.ScaleEncoderException 4 | import io.nodle.substratesdk.scale.toCompactU8a 5 | import io.nodle.substratesdk.scale.toU8a 6 | import io.nodle.substratesdk.types.* 7 | 8 | fun Extrinsic.toU8aV2(): ByteArray { 9 | val payload = byteArrayOf(0x84.toByte()) + signature.toU8aV2() + method.toU8aV2() 10 | return payload.size.toCompactU8a() + payload 11 | } 12 | 13 | fun ExtrinsicSignature.toU8aV2(): ByteArray { 14 | if (signer.type != AddressType.AccountID) { 15 | throw ScaleEncoderException("address type not supported") 16 | } 17 | 18 | return byteArrayOf(0xff.toByte()) + 19 | (signer as AccountIDAddress).pubkey + 20 | signature.toU8a() + 21 | era.toU8a() + 22 | nonce.toCompactU8a() + 23 | tip.toCompactU8a() 24 | } 25 | 26 | fun ExtrinsicPayload.toU8aV2(): ByteArray { 27 | return method.toU8aV2() + 28 | era.toU8a() + 29 | nonce.toCompactU8a() + 30 | tip.toCompactU8a() + 31 | specVersion.toInt().toU8a() + 32 | transactionVersion.toInt().toU8a() + 33 | genesisHash + 34 | blockHash 35 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/types/MultiAddress.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.types 2 | 3 | import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters 4 | 5 | /** 6 | * @author Lucien Loiseau on 09/04/21. 7 | */ 8 | 9 | /** 10 | * Represents the MultiAddress enum defined in Substrate 11 | * 12 | * See: 13 | * - https://github.com/paritytech/substrate/blob/master/primitives/runtime/src/multiaddress.rs 14 | * - https://github.com/paritytech/substrate/pull/7380 15 | */ 16 | enum class AddressType(val id: Int) { 17 | AccountID(0), AccountIndex(1), Raw(2), Address32(3), Address20(4) 18 | } 19 | 20 | abstract class MultiAddress(val type: AddressType) 21 | 22 | // public key 23 | class AccountIDAddress(val pubkey: ByteArray) : 24 | MultiAddress(AddressType.AccountID) { 25 | constructor(ed: Ed25519PublicKeyParameters) : this(ed.encoded) 26 | } 27 | 28 | // encoded string 29 | class RawAddress(val bytes: ByteArray) : MultiAddress(AddressType.Raw) 30 | 31 | // eth 32 | class Address20(val bytes: ByteArray) : 33 | MultiAddress(AddressType.Address20) { 34 | init { 35 | require(bytes.size == 20) 36 | } 37 | } 38 | 39 | // like a 256 bits hash 40 | class Address32(val bytes: ByteArray) : 41 | MultiAddress(AddressType.Address32) { 42 | init { 43 | require(bytes.size == 32) 44 | } 45 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store 3 | .idea/ 4 | release/ 5 | mapping.txt 6 | GVersion.kt 7 | # Android Studio 8 | .project 9 | .settings/ 10 | .classpath 11 | 12 | # Visual Studio Code 13 | .vscode/ 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | 23 | # Gradle files 24 | .gradle/ 25 | build/ 26 | 27 | # Local configuration file (sdk path, etc) 28 | local.properties 29 | 30 | # Proguard folder generated by Eclipse 31 | proguard/ 32 | 33 | # Log Files 34 | *.log 35 | 36 | # Android Studio Navigation editor temp files 37 | .navigation/ 38 | 39 | # Android Studio captures folder 40 | captures/ 41 | 42 | ### Intellij Patch ### 43 | *.iml 44 | modules.xml 45 | .idea/misc.xml 46 | *.ipr 47 | 48 | ### Linux ### 49 | *~ 50 | 51 | # KDE directory preferences 52 | .directory 53 | 54 | # Linux trash folder which might appear on any partition or disk 55 | .Trash-* 56 | 57 | # .nfs files are created when an open file is removed but is still being accessed 58 | .nfs* 59 | 60 | ### macOS ### 61 | *.DS_Store 62 | .AppleDouble 63 | .LSOverride 64 | 65 | # Icon must end with two \r 66 | Icon 67 | 68 | 69 | # Thumbnails 70 | ._* 71 | 72 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 73 | !gradle-wrapper.jar 74 | 75 | 76 | # Created by https://www.gitignore.io/api/android 77 | # Edit at https://www.gitignore.io/?templates=android 78 | 79 | ### Android ### 80 | # Built application files 81 | *.apk 82 | *.ap_ 83 | *.aab 84 | 85 | # Files for the ART/Dalvik VM 86 | *.dex 87 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/rpc/SubstrateRpc.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.rpc 2 | 3 | import io.nodle.substratesdk.utils.onDebugOnly 4 | import io.reactivex.rxjava3.core.Single 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory 7 | 8 | /** 9 | * @author Lucien Loiseau on 12/05/21. 10 | */ 11 | class SubstrateRpc(substrateUrls: Array) { 12 | 13 | private val log: Logger = LoggerFactory.getLogger(SubstrateRpc::class.java) 14 | 15 | private val rpcEndpoints = substrateUrls.mapNotNull { url -> 16 | when { 17 | url.startsWith("wss") -> WebSocketRpc(url) 18 | url.startsWith("http") -> HttpRpc(url) 19 | else -> null 20 | } 21 | } 22 | 23 | /** 24 | * TODO: would be nice to not do any blockingget in there, 25 | * feels a bit like cheating, not really rxjava-ly 26 | */ 27 | fun send(method: RpcMethod, defaultValue: T? = null): Single { 28 | return Single.just(rpcEndpoints) 29 | .map { 30 | it.forEach { rpc -> 31 | try { 32 | return@map rpc.send(method, defaultValue).blockingGet() 33 | } catch (e: Exception) { 34 | onDebugOnly { log.debug("rpc error (${rpc.url()}) !! ${e.message}") } 35 | // ignore 36 | } 37 | } 38 | onDebugOnly { log.debug("rpc error !! no endpoint left to try") } 39 | throw Exception() 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/utils/xxHash.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.utils 2 | 3 | import net.jpountz.xxhash.XXHashFactory 4 | import org.bouncycastle.crypto.digests.Blake2bDigest 5 | import java.nio.ByteBuffer 6 | import java.nio.ByteOrder 7 | 8 | /** 9 | * @author Lucien Loiseau on 14/07/20. 10 | */ 11 | 12 | /** 13 | * 128-bits xxhash 14 | * 15 | * ------------------------------------------------------- 16 | * Excerpt from: https://www.shawntabrizi.com/substrate/querying-substrate-storage-via-rpc/ 17 | * 18 | * ``` 19 | * Note: Note that we use XXHash to output a 128 bit hash. 20 | * However, XXHash only supports 32 bit and 64 bit outputs. 21 | * To correctly generate the 128 bit hash, we need to hash 22 | * the same phrase twice, with seed 0 and seed 1, and concatenate them. 23 | * ``` 24 | * ------------------------------------------------------- 25 | */ 26 | fun ByteArray.xxHash128() : ByteArray { 27 | val factory = XXHashFactory.fastestInstance() 28 | val buf = ByteBuffer.wrap(this) 29 | 30 | val ret = ByteBuffer.allocate(16) 31 | ret.order(ByteOrder.LITTLE_ENDIAN) 32 | ret.putLong(factory.hash64().hash(buf, 0)) 33 | buf.rewind() 34 | ret.putLong(factory.hash64().hash(buf, 1)) 35 | return ret.array() 36 | } 37 | 38 | fun String.xxHash128() : ByteArray = toByteArray().xxHash128() 39 | 40 | fun ByteArray.blake128() : ByteArray { 41 | val b2b = Blake2bDigest(128) 42 | val checksum = ByteArray(b2b.digestSize) 43 | b2b.update(this, 0, size) 44 | b2b.doFinal(checksum, 0) 45 | return checksum 46 | } 47 | 48 | fun String.blake128() : ByteArray = toByteArray().blake128() 49 | -------------------------------------------------------------------------------- /substrate-client/src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # SLF4J's SimpleLogger configuration file 2 | # Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. 3 | 4 | # Default logging detail level for all instances of SimpleLogger. 5 | # Must be one of ("trace", "debug", "info", "warn", or "error"). 6 | # If not specified, defaults to "info". 7 | org.slf4j.simpleLogger.defaultLogLevel=trace 8 | 9 | # Logging detail level for a SimpleLogger instance named "xxxxx". 10 | # Must be one of ("trace", "debug", "info", "warn", or "error"). 11 | # If not specified, the default logging detail level is used. 12 | #org.slf4j.simpleLogger.log.xxxxx= 13 | 14 | # Set to true if you want the current date and time to be included in output messages. 15 | # Default is false, and will output the number of milliseconds elapsed since startup. 16 | #org.slf4j.simpleLogger.showDateTime=false 17 | 18 | # The date and time format to be used in the output messages. 19 | # The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. 20 | # If the format is not specified or is invalid, the default format is used. 21 | # The default format is yyyy-MM-dd HH:mm:ss:SSS Z. 22 | #org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z 23 | 24 | # Set to true if you want to output the current thread name. 25 | # Defaults to true. 26 | #org.slf4j.simpleLogger.showThreadName=true 27 | 28 | # Set to true if you want the Logger instance name to be included in output messages. 29 | # Defaults to true. 30 | #org.slf4j.simpleLogger.showLogName=true 31 | 32 | # Set to true if you want the last component of the name to be included in output messages. 33 | # Defaults to false. 34 | #org.slf4j.simpleLogger.showShortLogName=false% -------------------------------------------------------------------------------- /substrate-client/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' 3 | id 'com.cookpad.android.plugin.license-tools' 4 | id 'maven-publish' 5 | id 'com.peterabeles.gversion' 6 | } 7 | 8 | gversion { 9 | srcDir = "src/main/java/" // path is relative to the sub-project by default 10 | classPackage = "io.nodle.substratesdk" 11 | debug = false // optional. print out extra debug information 12 | language = "kotlin" // optional. Can be Java, Kotlin, YAML, or Properties. Case insensitive. 13 | } 14 | 15 | afterEvaluate { 16 | publishing { 17 | publications { 18 | libraryJar(MavenPublication) { 19 | from components.kotlin 20 | groupId = 'com.github.NodleCode' 21 | artifactId = 'substrate-client' 22 | version = '0.1' 23 | } 24 | } 25 | } 26 | } 27 | 28 | project.compileKotlin.dependsOn(createVersionFile) 29 | 30 | dependencies { 31 | implementation fileTree(dir: 'libs', include: ['*.jar']) 32 | api group: 'org.slf4j', name:'slf4j-api', version: '1.7.30' 33 | 34 | // websocket 35 | implementation 'com.neovisionaries:nv-websocket-client:2.9' 36 | // json 37 | implementation 'org.json:json:20200518' 38 | // crypto 39 | api group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.65' 40 | // xxhash 41 | api group: 'org.lz4', name: 'lz4-java', version: '1.7.1' 42 | 43 | // BIP-39: key generation from 12 words mnemonic 44 | // not a fan of this dependency because it comes with spongy castle which we don't need 45 | implementation 'com.github.NodleCode:BIP39:484f9d5d588' 46 | implementation 'com.github.komputing:KBase58:0.1' 47 | 48 | // kotlin 49 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutine" 50 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 51 | implementation("io.reactivex.rxjava3:rxkotlin:3.0.0") 52 | 53 | // test only dependencies 54 | testImplementation 'junit:junit:4.12' 55 | testImplementation 'pl.pragmatists:JUnitParams:1.1.1' 56 | testImplementation 'com.github.doyaaaaaken:kotlin-csv-jvm:0.7.3' 57 | testImplementation 'org.mock-server:mockserver-netty:5.3.0' 58 | //testImplementation group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.30' 59 | } 60 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /substrate-client/src/test/java/io/nodle/substratesdk/TestEd25519.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk 2 | 3 | import io.nodle.substratesdk.account.Wallet 4 | import io.nodle.substratesdk.account.signMsg 5 | import io.nodle.substratesdk.utils.hexToBa 6 | import io.nodle.substratesdk.utils.toHex 7 | import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters 8 | import org.hamcrest.CoreMatchers 9 | import org.junit.Assert 10 | import org.junit.FixMethodOrder 11 | import org.junit.Test 12 | import org.junit.runners.MethodSorters 13 | 14 | /** 15 | * @author Lucien Loiseau on 25/03/21. 16 | */ 17 | 18 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 19 | class TestEd25519 { 20 | @Test 21 | fun stage0_testSigner() { 22 | val wallet = Wallet("void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold") 23 | val signature = wallet.privateKey.signMsg("Hello World!".toByteArray()) 24 | Assert.assertThat(signature.toHex(), CoreMatchers.equalTo("4e69456ddb02d48b6aec237ca844d83ad39be9e0778ac88472c7dd036a360c31ca17acf19197427c97ce58f9ba6311b409425d4e5c785e81edb733ca9208ad02")) 25 | } 26 | 27 | @Test 28 | fun stage1_testSigner() { 29 | val wallet = Wallet(Ed25519PrivateKeyParameters("e0dab57d0f865f5add6121cc0d4351ac5eae7f66d7f371bf39d30b7ae913d2c4".hexToBa(), 0)) 30 | val signature = wallet.privateKey.signMsg("0300ffe432904cb94fdf27c6fdaad7985391f7bbe578e2df5ae1a6788bb07b5d6f15cf280048002d00000003000000f18113a0061d72db857d3b3e2f261e1c214989d191fa57380e633cc5c1a2ad38f18113a0061d72db857d3b3e2f261e1c214989d191fa57380e633cc5c1a2ad38".hexToBa()) 31 | Assert.assertThat(signature.toHex(), CoreMatchers.equalTo("2f0fada4259e8faf06806d0d56e72a0fb592f8923407f46637f4edd3e13b80082e75b22615059707d9523baf7ed706249c89ef518c0ff06912b654101385990f")) 32 | } 33 | 34 | @Test 35 | fun stage2_testSS58() { 36 | val wallet1 = Wallet("void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold") 37 | val ss58Substrate = wallet1.toSS58() 38 | Assert.assertThat(ss58Substrate, CoreMatchers.equalTo("5F3qnP81P18AzbX5dTRUGmHZf19dF3qDc877SGHDpK8RfFLX")) 39 | 40 | val ss58SubstrateGeneric = wallet1.toSS58(0x2a) 41 | Assert.assertThat(ss58SubstrateGeneric, CoreMatchers.equalTo("5F3qnP81P18AzbX5dTRUGmHZf19dF3qDc877SGHDpK8RfFLX")) 42 | 43 | val ss58SubstrateNodle = wallet1.toSS58(0x25) 44 | Assert.assertThat(ss58SubstrateNodle, CoreMatchers.equalTo("4k7YVRQzTYkYDJVm6o2MPFxSRaS9y8gcfRLTMr4iW39zFk2F")) 45 | } 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Substrate Client 2 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 3 | 4 | substrate-client-kotlin is client library to interact with a substrate-based chain. 5 | It uses the API available from the RPC endpoint only (no sidecar). 6 | As of today it provides the following functionality: 7 | 8 | * compatible with substrate 2.0 9 | * ed25519 wallet creation 10 | * get account info (balance) 11 | * sign extrinsic and send (immortal era) 12 | * estimate fee 13 | 14 | what is currently **not** supported 15 | 16 | * sr25519 wallet 17 | * mortal era extrinsic 18 | 19 | ## Using the library 20 | 21 | 1. Add the JitPack repository. 22 | ``` 23 | allprojects { 24 | repositories { 25 | ... 26 | maven { url 'https://jitpack.io' } 27 | } 28 | } 29 | ``` 30 | 31 | 2. Add the substrate-client dependency 32 | ``` 33 | dependencies { 34 | implementation 'com.github.NodleCode:substrate-client-kotlin:1.0' 35 | } 36 | ``` 37 | 38 | 39 | ## Running the tests over custom rpc endpoints 40 | 41 | The library comes with a set of JUnit Test to ensure that the api 42 | provided by this library works properly 43 | 44 | In order to test that this library works over your own substrate rpc 45 | endpoints, you need to provide the chains configuration that will be 46 | used by the tests. 47 | 48 | Simply create a file in 49 | 50 | ``` 51 | substrate-client/src/test/resources/rpc-test.config 52 | ``` 53 | 54 | This file must be a CSV (without header) and must contain 3 columns per lines: 55 | * rpc endpoint (e.g. wss://rpc.polkadot.io) 56 | * alice wallet mnemonic 57 | * bob wallet mnemonic 58 | 59 | Alice and Bob wallet must be provisioned with enough token to be able 60 | to perform the test and pay the fees. When testing sending transaction, 61 | the test will always send token from the wallet with the most token. 62 | 63 | Here is an example of such file (wallets and rpc doesn't exists): 64 | ``` 65 | wss://mainnet.substrate-chain.io,smoke key grief belt gather absurd open attend keep flip hollow popular,total arch interest inmate book cigar primary long mixture party practice old 66 | wss://testnet.substrate-chain.io,pumpkin poem tuition scatter elder moral hockey valley health head joke stem,pupil opinion unhappy nerve adult lunch dolphin famous angry draw soap like 67 | ``` 68 | 69 | You can then run the tests with gradle: 70 | 71 | ``` 72 | ./gradlew test 73 | ``` 74 | 75 | The test report will be generated in `substrate-client/build/reports/test/index.html` 76 | 77 | ## Additional Notes 78 | 79 | This is a work in progress and comes with no warranty. 80 | contribution are welcome. If you have any question, ideas or if you found a bug, please open an issue! -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/account/Account.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.account 2 | 3 | import io.nodle.substratesdk.types.AccountIDAddress 4 | import io.nodle.substratesdk.types.MultiAddress 5 | import io.nodle.substratesdk.utils.hexToBa 6 | import io.nodle.substratesdk.utils.toHex 7 | import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters 8 | import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters 9 | import java.lang.Exception 10 | 11 | class InvalidAccount : Exception("invalid account") 12 | 13 | /** 14 | * @author Lucien Loiseau on 29/07/20. 15 | */ 16 | open class Account() { 17 | lateinit var ss58: String 18 | 19 | constructor(pub: Ed25519PublicKeyParameters) : this() { 20 | ss58 = pub.encoded.toSS58() 21 | } 22 | 23 | @Throws(InvalidAccount::class) 24 | constructor(accountId: String) : this() { 25 | val accountBa = if (accountId.startsWith("0x")) { 26 | accountId.hexToBa() 27 | } else { // we assume it is ss58 28 | try { 29 | accountId.ss58ToBa() 30 | } catch (e: Exception) { 31 | throw InvalidAccount() 32 | } 33 | } 34 | 35 | if (accountBa.size != 32) { 36 | throw InvalidAccount() 37 | } 38 | ss58 = accountBa.toSS58() 39 | } 40 | 41 | @Throws(InvalidAccount::class) 42 | constructor(accountBa: ByteArray) : this() { 43 | if (accountBa.size != 32) { 44 | throw InvalidAccount() 45 | } 46 | ss58 = accountBa.toSS58() 47 | } 48 | 49 | fun toMultiAddress(): MultiAddress { 50 | return AccountIDAddress(ss58.ss58ToBa()) 51 | } 52 | 53 | fun toSS58(): String { 54 | return ss58 55 | } 56 | 57 | fun toSS58(substrateID: Byte): String { 58 | return ss58.ss58ToBa().toSS58(substrateID) 59 | } 60 | 61 | fun toHex(): String { 62 | return "0x" + ss58.ss58ToBa().toHex() 63 | } 64 | 65 | fun toU8a(): ByteArray { 66 | return ss58.ss58ToBa() 67 | } 68 | } 69 | 70 | class Wallet : Account { 71 | 72 | var privateKey: Ed25519PrivateKeyParameters 73 | 74 | constructor(privateKeyParameters: Ed25519PrivateKeyParameters) { 75 | this.privateKey = privateKeyParameters 76 | this.ss58 = privateKey.generatePublicKey().encoded.toSS58() 77 | } 78 | 79 | @Throws(InvalidAccount::class) 80 | constructor(mnemonic: String) { 81 | try { 82 | this.privateKey = Mnemonic.generateEd25519FromMnemonic(mnemonic.split(" "), "") 83 | this.ss58 = privateKey.generatePublicKey().encoded.toSS58() 84 | } catch (e: Exception) { 85 | throw InvalidAccount() 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/rpc/SubstrateProvider.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.rpc 2 | 3 | import io.nodle.substratesdk.account.Account 4 | import io.nodle.substratesdk.scale.readMetadata 5 | import io.nodle.substratesdk.types.CallNotFoundException 6 | import io.nodle.substratesdk.types.RuntimeMetadata 7 | import io.nodle.substratesdk.types.TransferCall 8 | import io.nodle.substratesdk.types.findCall 9 | import io.nodle.substratesdk.utils.hexToBa 10 | import io.nodle.substratesdk.utils.onDebugOnly 11 | import io.reactivex.rxjava3.core.Single 12 | import org.json.JSONObject 13 | import java.math.BigInteger 14 | import java.nio.ByteBuffer 15 | 16 | /** 17 | * @author Lucien Loiseau on 29/07/20. 18 | */ 19 | class SubstrateProvider(vararg substrateRpcUrl: String) { 20 | val rpc = SubstrateRpc(substrateRpcUrl) 21 | 22 | var genesisHash: String? = null 23 | var metadata: RuntimeMetadata? = null 24 | var runtimeVersion: JSONObject? = null 25 | 26 | fun getGenesisHash(): Single { 27 | if (genesisHash != null) 28 | return Single.just(genesisHash) 29 | 30 | return rpc.send(ChainGetBlockHash(0)) 31 | .map { it.removePrefix("0x") } 32 | .doOnSuccess { genesisHash = it } 33 | } 34 | 35 | @ExperimentalUnsignedTypes 36 | fun getMetadata(): Single { 37 | if (metadata != null) 38 | return Single.just(metadata) 39 | 40 | return rpc.send(StateGetMetadata()) 41 | .map { it.hexToBa() } 42 | .map { ByteBuffer.wrap(it).readMetadata() } 43 | .doOnSuccess { metadata = it } 44 | } 45 | 46 | @ExperimentalUnsignedTypes 47 | fun transferCall(destAccount: Account, amount: BigInteger): Single { 48 | return this.getMetadata().map { 49 | val call = it.findCall("Balances", "transfer") 50 | ?: throw CallNotFoundException("Balance transfer call is not supported on this blockchain") 51 | TransferCall( 52 | call.moduleIndex, 53 | call.callIndex, 54 | destAccount, 55 | amount 56 | ) 57 | } 58 | } 59 | 60 | fun getRuntimeVersion(): Single { 61 | if (runtimeVersion != null) 62 | return Single.just(runtimeVersion) 63 | 64 | return rpc.send(StateGetRuntimeVersion()) 65 | .doOnSuccess { runtimeVersion = it } 66 | } 67 | 68 | fun getSpecVersion(): Single { 69 | return this.getRuntimeVersion().map { 70 | it.getLong("specVersion") 71 | } 72 | } 73 | 74 | fun getTransactionVersion(): Single { 75 | return this.getRuntimeVersion().map { 76 | it.getLong("transactionVersion") 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/Extrinsic+Encoder.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale 2 | 3 | import io.nodle.substratesdk.rpc.SubstrateProvider 4 | import io.nodle.substratesdk.scale.v2.toU8aV2 5 | import io.nodle.substratesdk.types.* 6 | 7 | /** 8 | * @author Lucien Loiseau on 30/07/20. 9 | */ 10 | fun ExtrinsicEra.toU8a(): ByteArray { 11 | return (this as? ImmortalEra)?.toU8a() 12 | ?: (this as? MortalEra)?.toU8a() 13 | ?: byteArrayOf() 14 | } 15 | 16 | fun ImmortalEra.toU8a(): ByteArray { 17 | return byteArrayOf(0x00) 18 | } 19 | 20 | fun MortalEra.toU8a(): ByteArray { 21 | val quantizeFactor = (period shr 12).coerceAtLeast(1) 22 | val trailingZeros = java.lang.Long.numberOfTrailingZeros(phase) 23 | val encoded = 24 | (trailingZeros - 1).coerceAtLeast(1).coerceAtMost(15) + (((phase / quantizeFactor) shl 4)) 25 | val first = encoded shr 8 26 | val second = encoded and 0xff 27 | return byteArrayOf(second.toByte(), first.toByte()) 28 | } 29 | 30 | fun ExtrinsicPayload.toU8a(): ByteArray { 31 | return method.toU8a() + 32 | era.toU8a() + 33 | nonce.toCompactU8a() + 34 | tip.toCompactU8a() + 35 | specVersion.toInt().toU8a() + 36 | transactionVersion.toInt().toU8a() + 37 | genesisHash + 38 | blockHash 39 | } 40 | 41 | fun ExtrinsicEd25519Signature.toU8a(): ByteArray { 42 | return byteArrayOf(0x00) + 43 | this.signature 44 | } 45 | 46 | fun ExtrinsicSignature.toU8a(): ByteArray { 47 | return signer.toU8a() + 48 | signature.toU8a() + 49 | era.toU8a() + 50 | nonce.toCompactU8a() + 51 | tip.toCompactU8a() 52 | } 53 | 54 | fun Extrinsic.toU8a(): ByteArray { 55 | val payload = byteArrayOf(0x84.toByte()) + signature.toU8a() + method.toU8a() 56 | return payload.size.toCompactU8a() + payload 57 | } 58 | 59 | /** 60 | * BACKPORT to V2 61 | * -------------- 62 | * temporary fix. Since substrate version 3, extrinsic are encoded slightly differently 63 | * however there is no easy and deterministic way to know which runtime version is 64 | * actually running. Here we use the nodle spec version but it means that this code 65 | * is nodle-specific and it will not work with substrate based chain using substrate 3 but 66 | * a specversion < 45. This is obviously a big problem which cannot be solved until the 67 | * chain clearly lists all its types over the RPC. 68 | */ 69 | fun Extrinsic.toU8a(provider: SubstrateProvider): ByteArray = 70 | provider.getSpecVersion().filter { it > 45 } 71 | ?.blockingGet()?.let { this.toU8a() } 72 | ?: this.toU8aV2() 73 | 74 | fun ExtrinsicPayload.toU8a(provider: SubstrateProvider): ByteArray = 75 | provider.getSpecVersion().filter { it > 45 } 76 | ?.blockingGet()?.let { this.toU8a() } 77 | ?: this.toU8aV2() 78 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/rpc/HttpRpc.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.rpc 2 | 3 | import io.nodle.substratesdk.utils.onDebugOnly 4 | import io.reactivex.rxjava3.core.Single 5 | import org.json.JSONObject 6 | import org.slf4j.Logger 7 | import org.slf4j.LoggerFactory 8 | import java.net.HttpURLConnection 9 | import java.net.URL 10 | 11 | /** 12 | * @author Lucien Loiseau on 12/05/21. 13 | */ 14 | class HttpRpc(private val url: String) : ISubstrateRpc { 15 | private val log: Logger = LoggerFactory.getLogger(HttpRpc::class.java) 16 | 17 | override fun send(method: RpcMethod, defaultValue: T?): Single { 18 | return Single.just(url) 19 | .map { url -> 20 | val connection = (URL(url).openConnection() as HttpURLConnection) 21 | val json = json { 22 | "id" to 1 23 | "jsonrpc" to "2.0" 24 | "method" to method.method 25 | "params" to method.params 26 | } 27 | onDebugOnly { log.debug("rpc ($url) > $json") } 28 | 29 | connection.requestMethod = "POST" 30 | connection.doInput = true 31 | connection.doOutput = true 32 | connection.connectTimeout = 10000 33 | connection.readTimeout = 10000 34 | connection.setRequestProperty( 35 | "User-Agent", 36 | "Mozilla/5.0" 37 | ) // Java/* is blocked by cloudflare 38 | connection.setRequestProperty("Content-Type", "application/json") 39 | connection.instanceFollowRedirects = true 40 | connection.connect() 41 | val out = connection.outputStream 42 | out.write(json.toString().toByteArray()) 43 | 44 | if (connection.responseCode == HttpURLConnection.HTTP_ACCEPTED || 45 | connection.responseCode == HttpURLConnection.HTTP_OK 46 | ) { 47 | val response = String(connection.inputStream.readBytes()) 48 | onDebugOnly { log.debug("rpc ($url) < $response") } 49 | JSONObject(response) 50 | } else { 51 | throw Exception() 52 | } 53 | } 54 | .map { 55 | if (it.has("error")) { 56 | throw Exception(it.getJSONObject("error").toString()) 57 | } 58 | 59 | it.opt("result")?.let { result -> 60 | @Suppress("UNCHECKED_CAST") // if it fails it throws an exception 61 | if (!JSONObject.NULL.equals(result)) result as T 62 | else defaultValue ?: throw NullJsonObjectException() 63 | } ?: throw Exception("result not available") 64 | } 65 | } 66 | 67 | override fun url(): String { 68 | return url 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/BasicType+Encoder.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale 2 | 3 | import io.nodle.substratesdk.utils.trimTrailingZeros 4 | import java.math.BigInteger 5 | import java.nio.ByteBuffer 6 | import java.nio.ByteOrder 7 | import java.nio.charset.Charset 8 | import kotlin.math.pow 9 | 10 | /** 11 | * @author Lucien Loiseau on 14/08/20. 12 | */ 13 | 14 | fun Byte.toU8a(): ByteArray = ByteBuffer.allocate(1) 15 | .order(ByteOrder.LITTLE_ENDIAN) 16 | .put(this) 17 | .array() 18 | 19 | fun Short.toU8a(): ByteArray = ByteBuffer.allocate(2) 20 | .order(ByteOrder.LITTLE_ENDIAN) 21 | .putShort(this) 22 | .array() 23 | 24 | fun Int.toU8a(): ByteArray = ByteBuffer.allocate(4) 25 | .order(ByteOrder.LITTLE_ENDIAN) 26 | .putInt(this) 27 | .array() 28 | 29 | fun Long.toU8a(): ByteArray = ByteBuffer.allocate(8) 30 | .order(ByteOrder.LITTLE_ENDIAN) 31 | .putLong(this) 32 | .array() 33 | 34 | fun Byte.toCompactU8a(): ByteArray = toLong().toCompactU8a() 35 | fun Short.toCompactU8a(): ByteArray = toLong().toCompactU8a() 36 | fun Int.toCompactU8a(): ByteArray = toLong().toCompactU8a() 37 | fun Long.toCompactU8a(): ByteArray { 38 | val x = this 39 | val ret: ByteBuffer 40 | when (x) { 41 | in 0 until 64 -> { 42 | ret = ByteBuffer.allocate(1) 43 | ret.order(ByteOrder.LITTLE_ENDIAN) 44 | ret.put(((x shl 2) + 0x00).toByte()) 45 | } 46 | in 64 until 2.0.pow(14.0).minus(1).toLong() -> { 47 | ret = ByteBuffer.allocate(2) 48 | ret.order(ByteOrder.LITTLE_ENDIAN) 49 | ret.putShort(((x shl 2) + 0x01).toShort()) 50 | } 51 | in 64 until 2.0.pow(30.0).minus(1).toLong() -> { 52 | ret = ByteBuffer.allocate(4) 53 | ret.order(ByteOrder.LITTLE_ENDIAN) 54 | ret.putInt(((x shl 2) + 0x02).toInt()) 55 | } 56 | else -> { 57 | return this.toBigInteger().toCompactU8a() 58 | } 59 | } 60 | return ret.array() 61 | } 62 | 63 | fun BigInteger.toCompactU8a(): ByteArray { 64 | if (compareTo(2.0.pow(30.0).minus(1).toLong().toBigInteger()) < 0) { 65 | return toLong().toCompactU8a() 66 | } 67 | val arr = toByteArray().reversedArray().trimTrailingZeros() 68 | 69 | return ByteBuffer.allocate(1 + arr.size) 70 | .order(ByteOrder.LITTLE_ENDIAN) 71 | .put((((arr.size - 4) shl 2) + 0x03).toByte()) 72 | .put(arr) 73 | .array() 74 | } 75 | 76 | fun String.toU8a(): ByteArray { 77 | val buf = this.toByteArray(charset = Charset.defaultCharset()) 78 | return buf.size.toLong().toCompactU8a() + buf 79 | } 80 | 81 | fun ByteArray.toU8a(): ByteArray { 82 | return this.size.toLong().toCompactU8a() + this 83 | } 84 | 85 | fun List.toU8a(toU8a: T.() -> ByteArray): ByteArray { 86 | var buf = ByteArray(0) 87 | forEach { 88 | buf += it.toU8a() 89 | } 90 | return this.size.toLong().toCompactU8a() + buf 91 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/account/Mnemonic.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.account 2 | 3 | import io.github.novacrypto.bip39.MnemonicGenerator 4 | import io.github.novacrypto.bip39.MnemonicValidator 5 | import io.github.novacrypto.bip39.Validation.InvalidChecksumException 6 | import io.github.novacrypto.bip39.Validation.InvalidWordCountException 7 | import io.github.novacrypto.bip39.Validation.UnexpectedWhiteSpaceException 8 | import io.github.novacrypto.bip39.Validation.WordNotFoundException 9 | import io.github.novacrypto.bip39.Words 10 | import io.github.novacrypto.bip39.wordlists.English 11 | import org.bouncycastle.crypto.digests.SHA512Digest 12 | import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator 13 | import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters 14 | import org.bouncycastle.crypto.params.KeyParameter 15 | import java.security.SecureRandom 16 | import java.util.* 17 | 18 | /** 19 | * @author Lucien Loiseau on 28/05/20. 20 | */ 21 | object Mnemonic { 22 | 23 | fun newTwelveWords(): List { 24 | /* 25 | val ret = mutableListOf() 26 | val entropy = ByteArray(Words.TWELVE.byteLength()) 27 | SecureRandom().nextBytes(entropy) 28 | MnemonicGenerator(English.INSTANCE) 29 | .createMnemonic(entropy) { 30 | s: CharSequence? -> ret.add(s.toString()) 31 | } 32 | return ret 33 | */ 34 | val sb = StringBuilder() 35 | val entropy = ByteArray(Words.TWELVE.byteLength()) 36 | SecureRandom().nextBytes(entropy) 37 | MnemonicGenerator(English.INSTANCE) 38 | .createMnemonic( 39 | entropy 40 | ) { s: CharSequence? -> sb.append(s) } 41 | return sb.split(" ") 42 | } 43 | 44 | fun seedFromMnemonic(mnemonic: List): ByteArray { 45 | val entropyWithChecksum = MnemonicValidator 46 | .ofWordList(English.INSTANCE) 47 | .entropyWithChecksum(mnemonic) 48 | val entropy = Arrays.copyOf(entropyWithChecksum, entropyWithChecksum.size - 1); 49 | return entropy 50 | } 51 | 52 | @Throws( 53 | InvalidChecksumException::class, 54 | InvalidWordCountException::class, 55 | WordNotFoundException::class, 56 | UnexpectedWhiteSpaceException::class) 57 | fun generateEd25519FromMnemonic(mnemonic: List, password: String): Ed25519PrivateKeyParameters { 58 | MnemonicValidator 59 | .ofWordList(English.INSTANCE) 60 | .validate(mnemonic) 61 | 62 | // get entropy from mnemonic phrase 63 | val entropyWithChecksum = MnemonicValidator 64 | .ofWordList(English.INSTANCE) 65 | .entropyWithChecksum(mnemonic) 66 | val entropy = Arrays.copyOf(entropyWithChecksum, entropyWithChecksum.size - 1); 67 | 68 | // generate the seed 69 | val salt = "mnemonic$password" 70 | val gen = PKCS5S2ParametersGenerator(SHA512Digest()) 71 | gen.init(entropy, salt.toByteArray(), 2048) 72 | val encoded = (gen.generateDerivedParameters(512) as KeyParameter).key 73 | 74 | // generate private key 75 | return Ed25519PrivateKeyParameters(encoded, 0) 76 | } 77 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/types/RuntimeMetadata.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.types 2 | 3 | /** 4 | * @author Lucien Loiseau on 30/07/20. 5 | */ 6 | 7 | const val META_MAGIC_NUMBER = 1635018093 8 | 9 | data class RuntimeMetadata( 10 | var version: Int = 0, 11 | var magicNumber: Int = META_MAGIC_NUMBER, 12 | var modules: List = ArrayList(), 13 | var extrinsic: Extrinsic = Extrinsic() 14 | ) { 15 | data class Module( 16 | var name: String = "", 17 | var storage: Storage? = Storage(), 18 | var calls: List? = ArrayList(), 19 | var events: List? = ArrayList(), 20 | var constants: List = ArrayList(), 21 | var errors: List = ArrayList(), 22 | var index: Int = 0 23 | ) 24 | 25 | data class Storage( 26 | var prefix: String = "", 27 | var entries: List = ArrayList() 28 | ) { 29 | data class Entry( 30 | var name: String = "", 31 | var modifier: Modifier = Modifier.OPTIONAL, 32 | var type: Pair, 33 | var default: ByteArray = ByteArray(0), 34 | var documentation: List = ArrayList() 35 | ) { 36 | enum class Modifier { 37 | OPTIONAL, DEFAULT, REQUIRED 38 | } 39 | 40 | enum class Hasher { 41 | BLAKE2_128, BLAKE2_256, BLAKE2_256_CONCAT, TWOX_128, TWOX_256, TWOX_64_CONCAT, IDENTITY 42 | } 43 | } 44 | 45 | data class TypePlain( 46 | var value: String 47 | ) 48 | 49 | data class TypeMap( 50 | var hasher: Entry.Hasher = Entry.Hasher.BLAKE2_128, 51 | var key: String = "", 52 | var type: String = "", 53 | var iterable: Boolean = false 54 | ) 55 | 56 | data class TypeDoubleMap( 57 | var firstHasher: Entry.Hasher = Entry.Hasher.BLAKE2_128, 58 | var firstKey: String = "", 59 | var secondHasher: Entry.Hasher = Entry.Hasher.BLAKE2_128, 60 | var secondKey: String = "", 61 | var type: String = "" 62 | ) 63 | } 64 | 65 | data class Call( 66 | var moduleIndex: Int = 0, 67 | var callIndex: Int = 0, 68 | var name: String = "", 69 | var arguments: List = ArrayList(), 70 | var documentation: List = ArrayList() 71 | ) { 72 | data class Argument( 73 | var name: String = "", 74 | var type: String = "" 75 | ) 76 | } 77 | 78 | data class Event( 79 | var name: String = "", 80 | var arguments: List = ArrayList(), 81 | var documentation: List = ArrayList() 82 | ) { 83 | data class Argument( 84 | var name: String = "" 85 | ) 86 | } 87 | 88 | data class Constant( 89 | var name: String = "", 90 | var type: String = "", 91 | var values: ByteArray = ByteArray(0), 92 | var documentation: List = ArrayList() 93 | ) 94 | 95 | data class Error( 96 | var name: String = "", 97 | var documentation: List = ArrayList() 98 | ) 99 | 100 | data class Extrinsic( 101 | var version: Int = 4, 102 | var signedExtensions: List = ArrayList() 103 | ) 104 | } 105 | 106 | fun RuntimeMetadata.findCall(module: String, call: String) : RuntimeMetadata.Call? { 107 | return this.modules 108 | .find { it.name == module }?.calls 109 | ?.find { it.name == call } 110 | } -------------------------------------------------------------------------------- /ATTRIBUTION.yml: -------------------------------------------------------------------------------- 1 | - artifact: com.github.NodleCode:BIP39:+ 2 | name: BIP39 3 | license: GNU General Public License v3.0 4 | licenseUrl: https://api.github.com/licenses/gpl-3.0 5 | url: https://novacrypto.github.io/BIP39 6 | - artifact: com.github.komputing.khash:sha256:+ 7 | name: sha256 8 | license: MIT License 9 | licenseUrl: https://api.github.com/licenses/mit 10 | url: https://github.com/komputing/KHash 11 | - artifact: com.github.komputing:KBase58:+ 12 | name: KBase58 13 | license: MIT License 14 | licenseUrl: https://api.github.com/licenses/mit 15 | - artifact: com.madgag.spongycastle:core:+ 16 | name: core 17 | license: Bouncy Castle Licence 18 | licenseUrl: http://www.bouncycastle.org/licence.html 19 | url: http://rtyley.github.io/spongycastle/ 20 | - artifact: com.neovisionaries:nv-websocket-client:+ 21 | name: nv-websocket-client 22 | license: The Apache Software License, Version 2.0 23 | licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt 24 | url: https://github.com/TakahikoKawasaki/nv-websocket-client 25 | - artifact: io.github.novacrypto:SHA256:+ 26 | name: SHA256 27 | license: GNU General Public License v3.0 28 | licenseUrl: https://api.github.com/licenses/gpl-3.0 29 | - artifact: io.github.novacrypto:ToRuntime:+ 30 | name: ToRuntime 31 | license: GNU General Public License v3.0 32 | licenseUrl: https://api.github.com/licenses/gpl-3.0 33 | - artifact: io.reactivex.rxjava3:rxjava:+ 34 | name: rxjava 35 | license: The Apache Software License, Version 2.0 36 | licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt 37 | url: https://github.com/ReactiveX/RxJava 38 | - artifact: io.reactivex.rxjava3:rxkotlin:+ 39 | name: rxkotlin 40 | license: The Apache License, Version 2.0 41 | licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt 42 | url: https://github.com/ReactiveX/RxKotlin 43 | - artifact: org.bouncycastle:bcprov-jdk15on:+ 44 | name: bcprov-jdk15on 45 | license: Bouncy Castle Licence 46 | licenseUrl: http://www.bouncycastle.org/licence.html 47 | url: http://www.bouncycastle.org/java.html 48 | - artifact: org.jetbrains.kotlin:kotlin-stdlib-common:+ 49 | name: kotlin-stdlib-common 50 | license: The Apache License, Version 2.0 51 | licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt 52 | url: https://kotlinlang.org/ 53 | - artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk7:+ 54 | name: kotlin-stdlib-jdk7 55 | license: The Apache License, Version 2.0 56 | licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt 57 | url: https://kotlinlang.org/ 58 | - artifact: org.jetbrains.kotlin:kotlin-stdlib:+ 59 | name: kotlin-stdlib 60 | license: The Apache License, Version 2.0 61 | licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt 62 | url: https://kotlinlang.org/ 63 | - artifact: org.jetbrains:annotations:+ 64 | name: annotations 65 | license: The Apache Software License, Version 2.0 66 | licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt 67 | url: http://www.jetbrains.org 68 | - artifact: org.json:json:+ 69 | name: json 70 | license: The JSON License 71 | licenseUrl: http://json.org/license.html 72 | url: https://github.com/douglascrockford/JSON-java 73 | - artifact: org.lz4:lz4-java:+ 74 | name: lz4-java 75 | license: The Apache Software License, Version 2.0 76 | licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt 77 | url: https://github.com/lz4/lz4-java 78 | - artifact: org.reactivestreams:reactive-streams:+ 79 | name: reactive-streams 80 | license: CC0 81 | licenseUrl: http://creativecommons.org/publicdomain/zero/1.0/ 82 | url: http://www.reactive-streams.org/ 83 | - artifact: org.slf4j:slf4j-api:+ 84 | name: slf4j-api 85 | license: MIT License 86 | licenseUrl: https://api.github.com/licenses/mit 87 | url: http://www.slf4j.org 88 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/SubstrateApi.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk 2 | 3 | import io.nodle.substratesdk.account.Account 4 | import io.nodle.substratesdk.account.Wallet 5 | import io.nodle.substratesdk.account.signMsg 6 | import io.nodle.substratesdk.rpc.* 7 | import io.nodle.substratesdk.scale.readAccountInfoSub3 8 | import io.nodle.substratesdk.scale.toU8a 9 | import io.nodle.substratesdk.scale.v1.readAccountInfoV1 10 | import io.nodle.substratesdk.types.* 11 | import io.nodle.substratesdk.utils.blake128 12 | import io.nodle.substratesdk.utils.hexToBa 13 | import io.nodle.substratesdk.utils.toHex 14 | import io.nodle.substratesdk.utils.xxHash128 15 | import io.reactivex.rxjava3.core.Single 16 | import io.reactivex.rxjava3.functions.Function5 17 | import org.json.JSONObject 18 | import java.math.BigInteger 19 | import java.nio.ByteBuffer 20 | 21 | /** 22 | * @author Lucien Loiseau on 14/07/20. 23 | */ 24 | @ExperimentalUnsignedTypes 25 | fun Account.getAccountInfo(provider: SubstrateProvider): Single { 26 | return provider.getMetadata() 27 | .flatMap { metadata -> 28 | val ba = toU8a() 29 | val key = "System".xxHash128() + "Account".xxHash128() + ba.blake128() + ba 30 | provider.rpc.send(StateGetStorage("0x" + key.toHex()), "") 31 | .map { scale -> 32 | if (scale == "") { 33 | nullAccountInfo 34 | } else { 35 | when (metadata.version) { 36 | in 0..11 -> ByteBuffer.wrap(scale.hexToBa()).readAccountInfoV1() 37 | else -> ByteBuffer.wrap(scale.hexToBa()).readAccountInfoSub3() 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | @ExperimentalUnsignedTypes 45 | fun Account.getBalance(provider: SubstrateProvider): Single { 46 | return getAccountInfo(provider) 47 | .map { it.data.free } 48 | } 49 | 50 | @ExperimentalUnsignedTypes 51 | fun Wallet.signTx( 52 | provider: SubstrateProvider, 53 | destAccount: Account, 54 | amount: BigInteger, 55 | era: ExtrinsicEra = ImmortalEra() 56 | ): Single { 57 | return Single.zip( 58 | provider.transferCall(destAccount, amount), 59 | provider.getGenesisHash(), 60 | provider.getSpecVersion(), 61 | provider.getTransactionVersion(), 62 | getAccountInfo(provider), 63 | Function5 { call, genesisHash, specVersion, transactionVersion, sourceWalletInfo -> 64 | val payload = ExtrinsicPayload( 65 | genesisHash.hexToBa(), 66 | era, 67 | genesisHash.hexToBa(), 68 | call, 69 | sourceWalletInfo.nonce.toLong(), 70 | specVersion, 71 | BigInteger.ZERO, 72 | transactionVersion 73 | ) 74 | val signature = privateKey.signMsg(payload.toU8a(provider)) 75 | val extrinsicSignature = ExtrinsicSignature( 76 | AccountIDAddress(privateKey.generatePublicKey()), 77 | ExtrinsicEd25519Signature(signature), 78 | era, 79 | sourceWalletInfo.nonce.toLong(), 80 | BigInteger.ZERO 81 | ) 82 | Extrinsic( 83 | extrinsicSignature, 84 | call 85 | ) 86 | } 87 | ) 88 | } 89 | 90 | fun Extrinsic.estimateFee(provider: SubstrateProvider): Single { 91 | return provider.rpc 92 | .send(PaymentQueryInfo("0x" + toU8a(provider).toHex())) 93 | .map { it.getString("partialFee").toBigInteger() } 94 | } 95 | 96 | fun Extrinsic.send(provider: SubstrateProvider): Single { 97 | return provider.rpc.send(AuthorSubmitExtrinsic("0x" + toU8a(provider).toHex())) 98 | } 99 | 100 | @ExperimentalUnsignedTypes 101 | fun Wallet.signAndSend( 102 | provider: SubstrateProvider, 103 | destAccount: Account, 104 | amount: BigInteger, 105 | era: ExtrinsicEra = ImmortalEra() 106 | ): Single { 107 | return this 108 | .signTx(provider, destAccount, amount, era) 109 | .flatMap { it.send(provider) } 110 | } 111 | 112 | fun getVersion(): String = GIT_SHA -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/BasicType+Parser.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale 2 | 3 | import java.math.BigInteger 4 | import java.nio.ByteBuffer 5 | import java.nio.ByteOrder 6 | import java.nio.charset.Charset 7 | 8 | class ScaleParserException(msg: String?) : Exception(msg) 9 | 10 | inline fun parserRequire(value: Boolean, lazyMessage: () -> Any): Unit { 11 | if (!value) { 12 | val message = lazyMessage() 13 | throw ScaleParserException(message.toString()) 14 | } 15 | } 16 | 17 | inline fun catchAll(parser: () -> T): T { 18 | try { 19 | return parser() 20 | } catch (e: Exception) { 21 | throw ScaleParserException(e.message) 22 | } 23 | } 24 | 25 | 26 | @ExperimentalUnsignedTypes 27 | fun ByteBuffer.readU8(): UByte = catchAll { order(ByteOrder.LITTLE_ENDIAN).get().toUByte() } 28 | 29 | 30 | @ExperimentalUnsignedTypes 31 | fun ByteBuffer.readU16(): UShort = catchAll { order(ByteOrder.LITTLE_ENDIAN).short.toUShort() } 32 | 33 | 34 | @ExperimentalUnsignedTypes 35 | fun ByteBuffer.readU32(): UInt = catchAll { order(ByteOrder.LITTLE_ENDIAN).int.toUInt() } 36 | 37 | @ExperimentalUnsignedTypes 38 | fun ByteBuffer.readU64(): ULong = catchAll { order(ByteOrder.LITTLE_ENDIAN).long.toULong() } 39 | 40 | fun ByteBuffer.readU128(): BigInteger = catchAll { 41 | val ba = ByteArray(16) 42 | get(ba) 43 | BigInteger(ba.reversedArray()) 44 | } 45 | 46 | fun ByteBuffer.readI8(): Byte = catchAll { order(ByteOrder.LITTLE_ENDIAN).get() } 47 | 48 | fun ByteBuffer.readI16(): Short = catchAll { order(ByteOrder.LITTLE_ENDIAN).short } 49 | 50 | fun ByteBuffer.readI32(): Int = catchAll { order(ByteOrder.LITTLE_ENDIAN).int } 51 | 52 | fun ByteBuffer.readI64(): Long = catchAll { order(ByteOrder.LITTLE_ENDIAN).long } 53 | 54 | fun ByteBuffer.readCompactInteger(): Int = catchAll { 55 | this.order(ByteOrder.LITTLE_ENDIAN) 56 | val byte = this.get().toInt() and 0xff 57 | if (byte and 0b11 == 0x00) { 58 | return (byte shr 2) 59 | } 60 | if (byte and 0b11 == 0x01) { 61 | this.position(position() - 1) 62 | return ((short.toInt() and 0xffff) shr 2) 63 | } 64 | if (byte and 0b11 == 0x02) { 65 | this.position(position() - 1) 66 | return (int shr 2) 67 | } 68 | throw ScaleParserException("compact mode not supported") 69 | } 70 | 71 | fun ByteBuffer.readCompactBigInt(): BigInteger = catchAll { 72 | this.order(ByteOrder.LITTLE_ENDIAN) 73 | val byte = this.get().toInt() and 0xff 74 | if (byte and 0b11 == 0x00) { 75 | return (byte shr 2).toBigInteger() 76 | } 77 | if (byte and 0b11 == 0x01) { 78 | this.position(position() - 1) 79 | return ((short.toInt() and 0xffff) shr 2).toBigInteger() 80 | } 81 | if (byte and 0b11 == 0x02) { 82 | this.position(position() - 1) 83 | return (int shr 2).toBigInteger() 84 | } 85 | 86 | val len = byte shr 2 87 | val ba = ByteArray(len) 88 | get(ba) 89 | return BigInteger(ba.reversedArray()) 90 | } 91 | 92 | fun ByteBuffer.readString(): String = catchAll { 93 | this.order(ByteOrder.LITTLE_ENDIAN) 94 | val size = readCompactInteger() 95 | val ba = ByteArray(size) 96 | get(ba) 97 | return ba.toString(charset = Charset.defaultCharset()) 98 | } 99 | 100 | fun ByteBuffer.readList(elementParser: ByteBuffer.() -> T): List = catchAll { 101 | val size = readCompactInteger() 102 | val list = ArrayList(size) 103 | for (i in 0 until size) { 104 | list.add(this.elementParser()) 105 | } 106 | return list 107 | } 108 | 109 | fun ByteBuffer.readListWithCounter(elementParser: ByteBuffer.(i: Int) -> T): List = catchAll { 110 | val size = readCompactInteger() 111 | val list = ArrayList(size) 112 | for (i in 0 until size) { 113 | list.add(this.elementParser(i)) 114 | } 115 | return list 116 | } 117 | 118 | fun ByteBuffer.readByteArray(): ByteArray = catchAll { 119 | this.order(ByteOrder.LITTLE_ENDIAN) 120 | val size = readCompactInteger() 121 | val ba = ByteArray(size) 122 | get(ba) 123 | return ba 124 | } 125 | 126 | fun ByteBuffer.readBoolean(): Boolean = catchAll { this.get() == 0x01.toByte() } 127 | 128 | fun ByteBuffer.readOptionalBoolean(): Boolean? = catchAll { 129 | if (this.get() == 0x00.toByte()) { 130 | null 131 | } else { 132 | this.get() == 0x02.toByte() 133 | } 134 | } 135 | 136 | inline fun ByteBuffer.readOptional(optionalParser: ByteBuffer.() -> T): T? = catchAll { 137 | return when (T::class) { 138 | Boolean::class -> readOptionalBoolean() as T? 139 | else -> { 140 | when (readBoolean()) { 141 | true -> this.optionalParser() 142 | else -> null 143 | } 144 | } 145 | } 146 | } 147 | 148 | @ExperimentalUnsignedTypes 149 | fun > ByteBuffer.readEnum(values: Array): T = catchAll { 150 | val id = this.readU8().toInt() 151 | for (t in values) { 152 | if (t.ordinal == id) { 153 | return t 154 | } 155 | } 156 | throw ScaleParserException("Unknown enum value: $id") 157 | } 158 | 159 | @ExperimentalUnsignedTypes 160 | fun ByteBuffer.readUnion(vararg parsers: ByteBuffer.() -> Any): Pair = catchAll { 161 | val index = this.readU8().toInt() 162 | parserRequire(index <= 255) { "Union can have max 255 values. Index: $index" } 163 | parserRequire(index < parsers.size) { "Index unknown for this union: $index" } 164 | val parse = parsers[index] 165 | return Pair(index, this.parse()) 166 | } -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/rpc/WebSocketRpc.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.rpc 2 | 3 | import com.neovisionaries.ws.client.* 4 | import io.nodle.substratesdk.utils.onDebugOnly 5 | import io.reactivex.rxjava3.core.Single 6 | import io.reactivex.rxjava3.subjects.BehaviorSubject 7 | import kotlinx.coroutines.* 8 | import kotlinx.coroutines.sync.Mutex 9 | import kotlinx.coroutines.sync.withLock 10 | import org.json.JSONException 11 | import org.json.JSONObject 12 | import org.slf4j.Logger 13 | import org.slf4j.LoggerFactory 14 | import java.io.IOException 15 | import java.util.concurrent.locks.ReentrantLock 16 | import kotlin.concurrent.withLock 17 | 18 | /** 19 | * @author Lucien Loiseau on 28/05/20. 20 | */ 21 | class WebSocketRpc(private val url: String) : ISubstrateRpc { 22 | private val log: Logger = LoggerFactory.getLogger(WebSocketRpc::class.java) 23 | private val lock: ReentrantLock = ReentrantLock() 24 | 25 | private var ws: WebSocket? = null 26 | private var cmdId: Int = 1 27 | private var recvChannel = BehaviorSubject.create() 28 | 29 | private val webSocketListener: WebSocketListener = object : WebSocketAdapter() { 30 | private var timeout: Job? = null 31 | private val mutex = Mutex() 32 | 33 | private fun resetTimeout() { 34 | runBlocking { 35 | mutex.withLock { 36 | timeout?.cancel() 37 | timeout = CoroutineScope(Dispatchers.Default).launch { 38 | delay(20000) 39 | if (isActive) { 40 | onDebugOnly { log.debug("rpc ($url) -- timeout fired, closing websocket") } 41 | timeout = null 42 | close() 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | override fun onConnected( 50 | websocket: WebSocket?, 51 | headers: MutableMap>? 52 | ) { 53 | super.onConnected(websocket, headers) 54 | resetTimeout() 55 | } 56 | 57 | override fun onFrameSent(websocket: WebSocket?, frame: WebSocketFrame?) { 58 | super.onFrameSent(websocket, frame) 59 | resetTimeout() 60 | } 61 | 62 | override fun onFrame(websocket: WebSocket?, frame: WebSocketFrame?) { 63 | super.onFrame(websocket, frame) 64 | resetTimeout() 65 | } 66 | 67 | override fun onDisconnected( 68 | websocket: WebSocket?, 69 | serverCloseFrame: WebSocketFrame?, 70 | clientCloseFrame: WebSocketFrame?, 71 | closedByServer: Boolean 72 | ) { 73 | close() 74 | } 75 | 76 | override fun onTextMessage(websocket: WebSocket?, text: String?) { 77 | try { 78 | val json = JSONObject(text!!) 79 | onDebugOnly { log.debug("rpc ($url) < $json -- from thread ${Thread.currentThread().name} )") } 80 | recvChannel.onNext(json) 81 | } catch (e: JSONException) { 82 | // ignore 83 | } 84 | } 85 | } 86 | 87 | @Throws(WebSocketException::class) 88 | private fun checkOpen() { 89 | lock.withLock { 90 | if ((ws == null) || !ws!!.isOpen) { 91 | open() 92 | } 93 | } 94 | } 95 | 96 | @Throws(Exception::class) 97 | private fun open() { 98 | close() 99 | ws = WebSocketFactory() 100 | .setConnectionTimeout(1000) 101 | .createSocket(url) 102 | ws?.pingInterval = 0 103 | ws?.addExtension(WebSocketExtension.PERMESSAGE_DEFLATE) 104 | ws?.connect() 105 | 106 | cmdId = 1 107 | recvChannel = BehaviorSubject.create() 108 | ws?.addListener(webSocketListener) 109 | return 110 | } 111 | 112 | private fun close() { 113 | try { 114 | ws?.removeListener(webSocketListener) 115 | ws?.disconnect() 116 | ws = null 117 | if (recvChannel?.hasObservers() == true) { 118 | recvChannel?.onError(IOException("rpc ($url) -- websocket disconnected")) 119 | } 120 | } catch (e: Exception) { 121 | // ignore 122 | } 123 | } 124 | 125 | private fun getResponse(queryId: Int, defaultValue: T?): Single { 126 | return recvChannel 127 | .filter { it.getInt("id") == queryId } 128 | .map { 129 | if (it.has("error")) { 130 | throw Exception(it.getJSONObject("error").toString()) 131 | } 132 | it.opt("result")?.let { result -> 133 | @Suppress("UNCHECKED_CAST") // if it fails it throws an exception 134 | if (!JSONObject.NULL.equals(result)) result as T 135 | else defaultValue ?: throw NullJsonObjectException() 136 | } ?: throw Exception("query result not available") 137 | } 138 | .firstOrError() 139 | } 140 | 141 | override fun send(method: RpcMethod, defaultValue: T?): Single { 142 | return Single 143 | .fromCallable { checkOpen() } 144 | .map { cmdId++ } 145 | .map { 146 | val json = json { 147 | "id" to it 148 | "jsonrpc" to "2.0" 149 | "method" to method.method 150 | "params" to method.params 151 | } 152 | onDebugOnly { log.debug("rpc ($url)> $json") } 153 | ws?.sendText(json.toString()) 154 | it 155 | } 156 | .flatMap { 157 | getResponse(it, defaultValue) 158 | } 159 | } 160 | 161 | override fun url(): String { 162 | return url 163 | } 164 | } 165 | 166 | 167 | -------------------------------------------------------------------------------- /substrate-client/src/main/java/io/nodle/substratesdk/scale/RuntimeMetadata+Parser.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk.scale 2 | 3 | import io.nodle.substratesdk.types.RuntimeMetadata 4 | import io.nodle.substratesdk.types.RuntimeMetadata.* 5 | import io.nodle.substratesdk.types.RuntimeMetadata.Storage.Entry 6 | import io.nodle.substratesdk.utils.onDebugOnly 7 | import java.nio.ByteBuffer 8 | import java.nio.ByteOrder 9 | 10 | /** 11 | * @author Lucien Loiseau on 30/07/20. 12 | */ 13 | @ExperimentalUnsignedTypes 14 | @Throws(ScaleParserException::class) 15 | fun ByteBuffer.readMetadata(): RuntimeMetadata { 16 | order(ByteOrder.LITTLE_ENDIAN) 17 | val metadata = RuntimeMetadata() 18 | metadata.magicNumber = readI32() 19 | metadata.version = readU8().toInt() 20 | when (metadata.version) { 21 | 11 -> metadata.modules = readListWithCounter { i -> readMetadataModuleV11(i) } 22 | 12 -> metadata.modules = readListWithCounter { i -> readMetadataModuleV12(i) } 23 | else -> throw ScaleParserException("unsupported metadata version: ${metadata.version}") 24 | } 25 | return metadata 26 | } 27 | 28 | @ExperimentalUnsignedTypes 29 | @Throws(ScaleParserException::class) 30 | fun ByteBuffer.readMetadataModuleV11(i: Int): Module { 31 | return Module( 32 | name = readString(), 33 | storage = readOptional { readModuleStorage() }, 34 | calls = readOptional { readListWithCounter { j -> readModuleCall(i, j) } }, 35 | events = readOptional { readList { readModuleEvent() } }, 36 | constants = readList { readModuleConstant() }, 37 | errors = readList { readModuleError() }, 38 | index = i 39 | ) 40 | } 41 | 42 | @ExperimentalUnsignedTypes 43 | @Throws(ScaleParserException::class) 44 | fun ByteBuffer.readMetadataModuleV12(i: Int): Module { 45 | val moduleV12 = Module( 46 | name = readString(), 47 | storage = readOptional { readModuleStorage() }, 48 | calls = readOptional { readListWithCounter { j -> readModuleCall(i, j) } }, 49 | events = readOptional { readList { readModuleEvent() } }, 50 | constants = readList { readModuleConstant() }, 51 | errors = readList { readModuleError() }, 52 | index = readU8().toInt() 53 | ) 54 | // in MetadataV12, module index is no longer the index of the variant in the module enum 55 | // but is instead set as a field. Thus, for each call of the module, 56 | // we need to update the module index with the correct one 57 | // for more details: https://github.com/paritytech/substrate/pull/6969 58 | moduleV12.calls?.forEach { it.moduleIndex = moduleV12.index } 59 | return moduleV12 60 | } 61 | 62 | @ExperimentalUnsignedTypes 63 | @Throws(ScaleParserException::class) 64 | fun ByteBuffer.readModuleStorage(): Storage { 65 | return Storage( 66 | prefix = readString(), 67 | entries = readList { readModuleStorageEntry() }) 68 | } 69 | 70 | @ExperimentalUnsignedTypes 71 | @Throws(ScaleParserException::class) 72 | fun ByteBuffer.readModuleStorageEntry(): Entry { 73 | return Entry( 74 | name = readString(), 75 | modifier = readEnum(Entry.Modifier.values()), 76 | type = readUnion( 77 | { readModuleStorageTypePlain() }, 78 | { readModuleStorageTypeMap() }, 79 | { readModuleStorageTypeDoubleMap() } 80 | ), 81 | default = readByteArray(), 82 | documentation = readList { readString() }) 83 | } 84 | 85 | @Throws(ScaleParserException::class) 86 | fun ByteBuffer.readModuleStorageTypePlain(): Storage.TypePlain { 87 | return Storage.TypePlain( 88 | value = readString() 89 | ) 90 | } 91 | 92 | @ExperimentalUnsignedTypes 93 | @Throws(ScaleParserException::class) 94 | fun ByteBuffer.readModuleStorageTypeMap(): Storage.TypeMap { 95 | return Storage.TypeMap( 96 | hasher = readEnum(Entry.Hasher.values()), 97 | key = readString(), 98 | type = readString(), 99 | iterable = readBoolean() 100 | ) 101 | } 102 | 103 | @ExperimentalUnsignedTypes 104 | @Throws(ScaleParserException::class) 105 | fun ByteBuffer.readModuleStorageTypeDoubleMap(): Storage.TypeDoubleMap { 106 | return Storage.TypeDoubleMap( 107 | firstHasher = readEnum(Entry.Hasher.values()), 108 | firstKey = readString(), 109 | secondKey = readString(), 110 | type = readString(), 111 | secondHasher = readEnum(Entry.Hasher.values()) 112 | ) 113 | } 114 | 115 | 116 | @Throws(ScaleParserException::class) 117 | fun ByteBuffer.readModuleCall(moduleIndex: Int, callIndex: Int): Call { 118 | return Call( 119 | moduleIndex = moduleIndex, 120 | callIndex = callIndex, 121 | name = readString(), 122 | arguments = readList { readCallArgument() }, 123 | documentation = readList { readString() }) 124 | } 125 | 126 | @Throws(ScaleParserException::class) 127 | fun ByteBuffer.readCallArgument(): Call.Argument { 128 | return Call.Argument( 129 | name = readString(), 130 | type = readString() 131 | ) 132 | } 133 | 134 | @Throws(ScaleParserException::class) 135 | fun ByteBuffer.readModuleEvent(): Event { 136 | return Event( 137 | name = readString(), 138 | arguments = readList { readEventArgument() }, 139 | documentation = readList { readString() }) 140 | } 141 | 142 | @Throws(ScaleParserException::class) 143 | fun ByteBuffer.readEventArgument(): Event.Argument { 144 | return Event.Argument( 145 | name = readString() 146 | ) 147 | } 148 | 149 | @Throws(ScaleParserException::class) 150 | fun ByteBuffer.readModuleConstant(): Constant { 151 | return Constant( 152 | name = readString(), 153 | type = readString(), 154 | values = readByteArray(), 155 | documentation = readList { readString() }) 156 | } 157 | 158 | @Throws(ScaleParserException::class) 159 | fun ByteBuffer.readModuleError(): Error { 160 | return Error( 161 | name = readString(), 162 | documentation = readList { readString() } 163 | ) 164 | } 165 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /substrate-client/src/test/java/io/nodle/substratesdk/TestRpc.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk 2 | 3 | import com.github.doyaaaaaken.kotlincsv.dsl.csvReader 4 | import io.nodle.substratesdk.account.Wallet 5 | import io.nodle.substratesdk.rpc.SubstrateProvider 6 | import io.reactivex.rxjava3.kotlin.subscribeBy 7 | import junitparams.JUnitParamsRunner 8 | import junitparams.Parameters 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.runBlocking 11 | import org.hamcrest.CoreMatchers 12 | import org.junit.Assert 13 | import org.junit.FixMethodOrder 14 | import org.junit.Test 15 | import org.junit.runner.RunWith 16 | import org.junit.runners.MethodSorters 17 | import org.mockserver.client.server.MockServerClient 18 | 19 | /** 20 | * @author Lucien Loiseau on 31/05/20. 21 | */ 22 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 23 | @RunWith(JUnitParamsRunner::class) 24 | class TestRpc { 25 | 26 | private fun getResourceAsText(path: String): String? { 27 | return this.javaClass.classLoader.getResource(path)?.readText() 28 | } 29 | 30 | fun testChainConfig(): Array? { 31 | val config = getResourceAsText("rpc-test.config") 32 | return config 33 | ?.let { 34 | csvReader().readAll(config) 35 | .map { 36 | it.toTypedArray() 37 | }.toTypedArray() 38 | } ?: arrayOf() 39 | } 40 | 41 | @ExperimentalUnsignedTypes 42 | @Test 43 | @Parameters(method = "testChainConfig") 44 | fun stage0_testMetadata(rpcUrl: String, aliceMnemonic: String, bobMnemonic: String, carlaMnemonic: String) { 45 | val provider = SubstrateProvider(rpcUrl) 46 | 47 | val meta = provider.getMetadata().blockingGet() 48 | Assert.assertThat( 49 | meta, 50 | CoreMatchers.notNullValue() 51 | ) 52 | } 53 | 54 | @Test 55 | @Parameters(method = "testChainConfig") 56 | fun stage0_testGenesisHash(rpcUrl: String, aliceMnemonic: String, bobMnemonic: String, carlaMnemonic: String) { 57 | val provider = SubstrateProvider(rpcUrl) 58 | val hash = provider.getGenesisHash().blockingGet() 59 | Assert.assertThat( 60 | hash, 61 | CoreMatchers.notNullValue() 62 | ) 63 | } 64 | 65 | @Test 66 | @Parameters(method = "testChainConfig") 67 | fun stage0_testSpecVersion(rpcUrl: String, aliceMnemonic: String, bobMnemonic: String, carlaMnemonic: String) { 68 | val provider = SubstrateProvider(rpcUrl) 69 | val specVersion = provider.getSpecVersion().blockingGet() 70 | Assert.assertThat( 71 | specVersion, 72 | CoreMatchers.notNullValue() 73 | ) 74 | } 75 | 76 | @Test 77 | @Parameters(method = "testChainConfig") 78 | fun stage0_testTransactionVersion(rpcUrl: String, aliceMnemonic: String, bobMnemonic: String, carlaMnemonic: String) { 79 | val provider = SubstrateProvider(rpcUrl) 80 | val specVersion = provider.getTransactionVersion().blockingGet() 81 | Assert.assertThat( 82 | specVersion, 83 | CoreMatchers.notNullValue() 84 | ) 85 | } 86 | 87 | @Test 88 | @Parameters(method = "testChainConfig") 89 | fun stage1_testBalance(rpcUrl: String, aliceMnemonic: String, bobMnemonic: String, carlaMnemonic: String) { 90 | val provider = SubstrateProvider(rpcUrl) 91 | 92 | val wallet1 = 93 | Wallet(carlaMnemonic) 94 | val balance1 = wallet1.getAccountInfo(provider).blockingGet() 95 | Assert.assertThat(balance1.data.free.toLong(), CoreMatchers.equalTo(1000000000000)) 96 | 97 | runBlocking { delay(12000) } 98 | 99 | val wallet2 = 100 | Wallet(aliceMnemonic) 101 | val balance2 = wallet2.getAccountInfo(provider).blockingGet() 102 | Assert.assertThat(balance2.data.free.toLong(), CoreMatchers.notNullValue()) 103 | 104 | val wallet3 = 105 | Wallet(bobMnemonic) 106 | val balance3 = wallet3.getAccountInfo(provider).blockingGet() 107 | Assert.assertThat(balance3.data.free.toLong(), CoreMatchers.notNullValue()) 108 | 109 | val wallet4 = 110 | Wallet("cherry royal innocent naive motor album pride humble deliver leaf trick series") 111 | val balance4 = wallet4.getAccountInfo(provider).blockingGet() 112 | Assert.assertThat(balance4.data.free.toLong(), CoreMatchers.equalTo(0)) 113 | } 114 | 115 | 116 | @ExperimentalUnsignedTypes 117 | @Test 118 | @Parameters(method = "testChainConfig") 119 | fun stage2_testSignTx(rpcUrl: String, aliceMnemonic: String, bobMnemonic: String, carlaMnemonic: String) { 120 | val provider = SubstrateProvider(rpcUrl) 121 | val src = Wallet(aliceMnemonic) 122 | val destWallet = Wallet(bobMnemonic) 123 | val txhash = src.signTx(provider, destWallet, 10.toBigInteger()).blockingGet() 124 | Assert.assertThat(txhash, CoreMatchers.notNullValue()) 125 | } 126 | 127 | @ExperimentalUnsignedTypes 128 | @Test 129 | @Parameters(method = "testChainConfig") 130 | fun stage3_testTransfer(rpcUrl: String, aliceMnemonic: String, bobMnemonic: String, carlaMnemonic: String) { 131 | val provider = SubstrateProvider(rpcUrl) 132 | 133 | val wallet1 = Wallet(aliceMnemonic) 134 | val balance1 = wallet1.getAccountInfo(provider).blockingGet().data.free.toLong() 135 | Assert.assertThat(balance1, CoreMatchers.notNullValue()) 136 | 137 | val wallet2 = Wallet(bobMnemonic) 138 | val balance2 = wallet2.getAccountInfo(provider).blockingGet().data.free.toLong() 139 | Assert.assertThat(balance2, CoreMatchers.notNullValue()) 140 | 141 | val balancesrc : Long 142 | val walletsrc = if (balance1 < balance2) { 143 | balancesrc = balance2 144 | wallet2 145 | } else { 146 | balancesrc = balance1 147 | wallet1 148 | } 149 | 150 | val balancedst : Long 151 | val walletdst = if (balance1 < balance2) { 152 | balancedst = balance1 153 | wallet1 154 | } else { 155 | balancedst = balance2 156 | wallet2 157 | } 158 | 159 | // estimate fee 160 | val tx1 = walletsrc.signTx(provider, walletdst, 1000000.toBigInteger()).blockingGet() 161 | val fee1 = tx1.estimateFee(provider).blockingGet() 162 | 163 | // send fund 164 | val txhash1 = walletsrc.signAndSend(provider, walletdst, 1000000.toBigInteger()).blockingGet() 165 | Assert.assertThat(txhash1, CoreMatchers.notNullValue()) 166 | 167 | // wait for transaction to be validated 168 | runBlocking { 169 | delay(15000) 170 | } 171 | 172 | // check that balance as changed and amount is coherent 173 | val balanceSrcAfter = walletsrc.getAccountInfo(provider).blockingGet().data.free.toLong() 174 | Assert.assertThat(balanceSrcAfter < balancesrc, CoreMatchers.equalTo(true)) 175 | val expected = (fee1.toLong() + 1000000) 176 | val actual = (balancesrc - balanceSrcAfter) 177 | val diff = expected - actual 178 | val ratio = diff*100/expected 179 | 180 | // we shouldn't expect more than a 10% slip from expected price 181 | Assert.assertThat(ratio < 10, CoreMatchers.equalTo(true)) 182 | 183 | 184 | val balanceDstAfter = walletdst.getAccountInfo(provider).blockingGet().data.free.toLong() 185 | Assert.assertThat(balanceDstAfter > balancedst, CoreMatchers.equalTo(true)) 186 | 187 | // destination should receive exactly what's expected 188 | Assert.assertThat(balanceDstAfter - balancedst, CoreMatchers.equalTo(1000000)) 189 | 190 | /* todo: mortal era does not work! 191 | val txhash2 = walletsrc.signAndSend(provider, walletdst, 10.toBigInteger(), MortalEra(64,38)).blockingGet() 192 | Assert.assertThat(txhash2, CoreMatchers.notNullValue()) 193 | */ 194 | } 195 | 196 | @ExperimentalUnsignedTypes 197 | @Test 198 | @Parameters(method = "testChainConfig") 199 | fun stage4_testEstimateFee(rpcUrl: String, aliceMnemonic: String, bobMnemonic: String, carlaMnemonic: String) { 200 | val provider = SubstrateProvider(rpcUrl) 201 | val src = Wallet(aliceMnemonic) 202 | val destWallet = Wallet(bobMnemonic) 203 | 204 | val tx1 = src.signTx(provider, destWallet, 1000000.toBigInteger()).blockingGet() 205 | val fee1 = tx1.estimateFee(provider).blockingGet() 206 | Assert.assertThat(fee1, CoreMatchers.not(0.toBigInteger())) 207 | 208 | val tx2 = src.signTx(provider, destWallet, 100000000.toBigInteger()).blockingGet() 209 | val fee2 = tx2.estimateFee(provider).blockingGet() 210 | Assert.assertThat(fee2, CoreMatchers.not(0.toBigInteger())) 211 | 212 | val tx3 = src.signTx(provider, destWallet, 1000000000.toBigInteger()).blockingGet() 213 | val fee3 = tx3.estimateFee(provider).blockingGet() 214 | Assert.assertThat(fee3, CoreMatchers.not(0.toBigInteger())) 215 | 216 | val tx4 = src.signTx(provider, destWallet, 1000000000000.toBigInteger()).blockingGet() 217 | val fee4 = tx4.estimateFee(provider).blockingGet() 218 | Assert.assertThat(fee4, CoreMatchers.not(0.toBigInteger())) 219 | } 220 | 221 | @Test 222 | fun stage5_testUrlCannotConnect() { 223 | val rpcUrl = "wss://ThisSubstrateUrlDoesNotExist.com" 224 | val provider = SubstrateProvider(rpcUrl) 225 | 226 | provider.getMetadata() 227 | .subscribeBy( 228 | onError = { 229 | }, 230 | onSuccess = { 231 | Assert.fail() 232 | }) 233 | 234 | var mockServer: MockServerClient = MockServerClient("localhost", 5568) 235 | val url = "http://localhost:5568" 236 | } 237 | 238 | @Test 239 | fun stage5_testUrlCannotWebSocket() { 240 | val rpcUrl = "ws://google.com/" 241 | val provider = SubstrateProvider(rpcUrl) 242 | 243 | provider.getMetadata() 244 | .subscribeBy( 245 | onError = { 246 | }, 247 | onSuccess = { 248 | Assert.fail() 249 | }) 250 | } 251 | } -------------------------------------------------------------------------------- /substrate-client/src/test/java/io/nodle/substratesdk/TestMnemonic.kt: -------------------------------------------------------------------------------- 1 | package io.nodle.substratesdk 2 | 3 | import io.nodle.substratesdk.account.Mnemonic 4 | import io.nodle.substratesdk.account.toSS58 5 | import io.nodle.substratesdk.utils.toHex 6 | import org.hamcrest.CoreMatchers 7 | import org.junit.Assert 8 | import org.junit.Assert.assertThat 9 | import org.junit.FixMethodOrder 10 | import org.junit.Test 11 | import org.junit.runners.MethodSorters 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * See [testing documentation](http://d.android.com/tools/testing). 17 | */ 18 | @FixMethodOrder(MethodSorters.NAME_ASCENDING) 19 | class TestMnemonic { 20 | val substrateVectors = listOf( 21 | arrayOf( 22 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", 23 | "00000000000000000000000000000000", 24 | "44e9d125f037ac1d51f0a7d3649689d422c2af8b1ec8e00d71db4d7bf6d127e33f50c3d5c84fa3e5399c72d6cbbbbc4a49bf76f76d952f479d74655a2ef2d453" 25 | ), 26 | arrayOf( 27 | "legal winner thank year wave sausage worth useful legal winner thank yellow", 28 | "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", 29 | "4313249608fe8ac10fd5886c92c4579007272cb77c21551ee5b8d60b780416850f1e26c1f4b8d88ece681cb058ab66d6182bc2ce5a03181f7b74c27576b5c8bf" 30 | ), 31 | arrayOf( 32 | "letter advice cage absurd amount doctor acoustic avoid letter advice cage above", 33 | "80808080808080808080808080808080", 34 | "27f3eb595928c60d5bc91a4d747da40ed236328183046892ed6cd5aa9ae38122acd1183adf09a89839acb1e6eaa7fb563cc958a3f9161248d5a036e0d0af533d" 35 | ), 36 | arrayOf( 37 | "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", 38 | "ffffffffffffffffffffffffffffffff", 39 | "227d6256fd4f9ccaf06c45eaa4b2345969640462bbb00c5f51f43cb43418c7a753265f9b1e0c0822c155a9cabc769413ecc14553e135fe140fc50b6722c6b9df" 40 | ), 41 | arrayOf( 42 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", 43 | "000000000000000000000000000000000000000000000000", 44 | "44e9d125f037ac1d51f0a7d3649689d422c2af8b1ec8e00d71db4d7bf6d127e33f50c3d5c84fa3e5399c72d6cbbbbc4a49bf76f76d952f479d74655a2ef2d453" 45 | ), 46 | arrayOf( 47 | "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", 48 | "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", 49 | "cb1d50e14101024a88905a098feb1553d4306d072d7460e167a60ccb3439a6817a0afc59060f45d999ddebc05308714733c9e1e84f30feccddd4ad6f95c8a445" 50 | ), 51 | arrayOf( 52 | "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always", 53 | "808080808080808080808080808080808080808080808080", 54 | "9ddecf32ce6bee77f867f3c4bb842d1f0151826a145cb4489598fe71ac29e3551b724f01052d1bc3f6d9514d6df6aa6d0291cfdf997a5afdb7b6a614c88ab36a" 55 | ), 56 | arrayOf( 57 | "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when", 58 | "ffffffffffffffffffffffffffffffffffffffffffffffff", 59 | "8971cb290e7117c64b63379c97ed3b5c6da488841bd9f95cdc2a5651ac89571e2c64d391d46e2475e8b043911885457cd23e99a28b5a18535fe53294dc8e1693" 60 | ), 61 | arrayOf( 62 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", 63 | "0000000000000000000000000000000000000000000000000000000000000000", 64 | "44e9d125f037ac1d51f0a7d3649689d422c2af8b1ec8e00d71db4d7bf6d127e33f50c3d5c84fa3e5399c72d6cbbbbc4a49bf76f76d952f479d74655a2ef2d453" 65 | ), 66 | arrayOf( 67 | "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", 68 | "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", 69 | "3037276a5d05fcd7edf51869eb841bdde27c574dae01ac8cfb1ea476f6bea6ef57ab9afe14aea1df8a48f97ae25b37d7c8326e49289efb25af92ba5a25d09ed3" 70 | ), 71 | arrayOf( 72 | "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", 73 | "8080808080808080808080808080808080808080808080808080808080808080", 74 | "2c9c6144a06ae5a855453d98c3dea470e2a8ffb78179c2e9eb15208ccca7d831c97ddafe844ab933131e6eb895f675ede2f4e39837bb5769d4e2bc11df58ac42" 75 | ), 76 | arrayOf( 77 | "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", 78 | "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 79 | "047e89ef7739cbfe30da0ad32eb1720d8f62441dd4f139b981b8e2d0bd412ed4eb14b89b5098c49db2301d4e7df4e89c21e53f345138e56a5e7d63fae21c5939" 80 | ), 81 | arrayOf( 82 | "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic", 83 | "9e885d952ad362caeb4efe34a8e91bd2", 84 | "f4956be6960bc145cdab782e649a5056598fd07cd3f32ceb73421c3da27833241324dc2c8b0a4d847eee457e6d4c5429f5e625ece22abaa6a976e82f1ec5531d" 85 | ), 86 | arrayOf( 87 | "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog", 88 | "6610b25967cdcca9d59875f5cb50b0ea75433311869e930b", 89 | "fbcc5229ade0c0ff018cb7a329c5459f91876e4dde2a97ddf03c832eab7f26124366a543f1485479c31a9db0d421bda82d7e1fe562e57f3533cb1733b001d84d" 90 | ), 91 | arrayOf( 92 | "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length", 93 | "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c", 94 | "7c60c555126c297deddddd59f8cdcdc9e3608944455824dd604897984b5cc369cad749803bb36eb8b786b570c9cdc8db275dbe841486676a6adf389f3be3f076" 95 | ), 96 | arrayOf( 97 | "scheme spot photo card baby mountain device kick cradle pact join borrow", 98 | "c0ba5a8e914111210f2bd131f3d5e08d", 99 | "c12157bf2506526c4bd1b79a056453b071361538e9e2c19c28ba2cfa39b5f23034b974e0164a1e8acd30f5b4c4de7d424fdb52c0116bfc6a965ba8205e6cc121" 100 | ), 101 | arrayOf( 102 | "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave", 103 | "6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3", 104 | "23766723e970e6b79dec4d5e4fdd627fd27d1ee026eb898feb9f653af01ad22080c6f306d1061656d01c4fe9a14c05f991d2c7d8af8730780de4f94cd99bd819" 105 | ), 106 | arrayOf( 107 | "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", 108 | "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863", 109 | "f4c83c86617cb014d35cd87d38b5ef1c5d5c3d58a73ab779114438a7b358f457e0462c92bddab5a406fe0e6b97c71905cf19f925f356bc673ceb0e49792f4340" 110 | ), 111 | arrayOf( 112 | "cat swing flag economy stadium alone churn speed unique patch report train", 113 | "23db8160a31d3e0dca3688ed941adbf3", 114 | "719d4d4de0638a1705bf5237262458983da76933e718b2d64eb592c470f3c5d222e345cc795337bb3da393b94375ff4a56cfcd68d5ea25b577ee9384d35f4246" 115 | ), 116 | arrayOf( 117 | "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access", 118 | "8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0", 119 | "7ae1291db32d16457c248567f2b101e62c5549d2a64cd2b7605d503ec876d58707a8d663641e99663bc4f6cc9746f4852e75e7e54de5bc1bd3c299c9a113409e" 120 | ), 121 | arrayOf( 122 | "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", 123 | "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad", 124 | "a911a5f4db0940b17ecb79c4dcf9392bf47dd18acaebdd4ef48799909ebb49672947cc15f4ef7e8ef47103a1a91a6732b821bda2c667e5b1d491c54788c69391" 125 | ), 126 | arrayOf( 127 | "vessel ladder alter error federal sibling chat ability sun glass valve picture", 128 | "f30f8c1da665478f49b001d94c5fc452", 129 | "4e2314ca7d9eebac6fe5a05a5a8d3546bc891785414d82207ac987926380411e559c885190d641ff7e686ace8c57db6f6e4333c1081e3d88d7141a74cf339c8f" 130 | ), 131 | arrayOf( 132 | "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump", 133 | "c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05", 134 | "7a83851102849edc5d2a3ca9d8044d0d4f00e5c4a292753ed3952e40808593251b0af1dd3c9ed9932d46e8608eb0b928216a6160bd4fc775a6e6fbd493d7c6b2" 135 | ), 136 | arrayOf( 137 | "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", 138 | "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f", 139 | "938ba18c3f521f19bd4a399c8425b02c716844325b1a65106b9d1593fbafe5e0b85448f523f91c48e331995ff24ae406757cff47d11f240847352b348ff436ed" 140 | ) 141 | ) 142 | 143 | @Test 144 | fun stage1_generateFromSubstrateTestVector() { 145 | for (vector in substrateVectors) { 146 | val seed = Mnemonic.seedFromMnemonic(vector[0].split(" ")) 147 | assertThat(seed.toHex(), CoreMatchers.equalTo(vector[1])) 148 | 149 | val key = Mnemonic.generateEd25519FromMnemonic(vector[0].split(" "), "Substrate") 150 | val privateKey = key.encoded.toHex() 151 | assertThat(privateKey, CoreMatchers.equalTo(vector[2].substring(0, 64))) 152 | } 153 | } 154 | 155 | @Test 156 | fun stage2_generateNewMnemonic() { 157 | val twelve1 = Mnemonic.newTwelveWords() 158 | assertThat(twelve1.size, CoreMatchers.equalTo(12)) 159 | 160 | val twelve2 = Mnemonic.newTwelveWords() 161 | assertThat(twelve2.size, CoreMatchers.equalTo(12)) 162 | 163 | assertThat(twelve1 == twelve2, CoreMatchers.equalTo(false)) 164 | } 165 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. --------------------------------------------------------------------------------