Nova Substrate SDK is a native Android library to help developers build native mobile apps for Substrate-based networks, e.g. Polkadot, Kusama & parachains
- Nova Substrate SDK for Android (Kotlin/Java)
- Table of contents
Bip39 is the algorithm which provides an opportunity to use a list of words, called mnemonic, instead of a raw 32 byte seed. Library provides Bip39
class to work with mnemonics:
val bip39 = Bip39()
val newMnemonic = bip39.generateMnemonic(length = MnemonicLength.TWELVE) // twelve words
val entropy = bip39.generateEntropy(newMnemonic)
val theSameMnemonic = bip39.generateMnemonic(entropy)
To generate a seed, a passphrase
is needed. Technically, it is a decoded derivation path (see Junction Decoder)
val seed = bip39.generateSeed(entropy, passphrase)
Library provides support for decoding/encoding account information using JSON format, compatible with Polkadot.js
Using JsonSeedDecoder
you can perform decoding of the imported JSON:
val decoder = JsonSeedDecoder(..)
decoder.extractImportMetaData(myJson) // does not perform full decoding (skips secret decrypting). Faster
decoder.decode(myJson, password) // performs full decoding. Slower
Using JsonSeedEncoder
you can generate JSON out of account information:
val encoder = JsonSeedEncoder(..)
val json = encoder.generate(keypair, seed, password, name, encryptionType, genesis, addressByte)
Library provides several extensions, that implement most common operations.
fun ByteArray.toHexString(withPrefix: Boolean = false): String
fun String.fromHex(): ByteArray
fun String.requirePrefix(prefix: String): String
fun String.requireHexPrefix(): String
fun ByteArray.xxHash128(): ByteArray
fun ByteArray.xxHash64(): ByteArray
fun ByteArray.blake2b512(): ByteArray
fun ByteArray.blake2b256(): ByteArray
fun ByteArray.blake2b128(): ByteArray
fun XXHash64.hash(bytes: ByteArray, seed: Long = 0): ByteArray
fun BCMessageDigest.hashConcat(bytes: ByteArray): ByteArray
fun XXHash64.hashConcat(bytes: ByteArray): ByteArray
There's a support for default Polkadot.js icon generation using IconGenerator
:
val generator = IconGenerator()
val drawable = generator.getSvgImage(accountId, sizeInPixels)
JunctionDecoder
provides support for derivation paths:
val derivationPath: String = ...
val decoder = JunctionDecoder()
val passphrase = decoder.getPassword(derivationPath) // retrieve passphrase to use in entropy -> seed generation
val decodedPath = decoder.decodeDerivationPath(derivationPath)
You can create storage keys easily:
val accountId: ByteArray = ..
val bondedKey = Module.Staking.Bonded.storageKey(bytes)
val accountInfoKey = Module.System.Account.storageKey(bytes)
If you're missing some specific service/module, you can define it by your own:
object Staking : Module("Staking") {
object ActiveEra : Service<Unit>(Staking, "ActiveEra") {
override fun storageKey(storageArgs: Unit): String {
return StorageUtils.createStorageKey(
service = this,
identifier = null
)
}
}
}
Library provides a convenient DSL to deal with scale encoding/decoding. Original codec reference: Link.
object AccountData : Schema<AccountData>() {
val free by uint128()
val reserved by uint128()
val miscFrozen by uint128()
val feeFrozen by uint128()
}
val struct = AccountData { data ->
data[AccountData.free] = BigDecimal("1")
data[AccountData.reserved] = BigInteger("0")
data[AccountData.miscFrozen] = BigInteger("0")
data[AccountData.feeFrozen] = BigInteger("0")
}
val inHex = struct.toHexString() // encode
val asBytes = struct.toByteArray() // or as byte array
val inHex = ...
val struct = AccountData.read(inHex)
val free = struct[AccountData.free]
Library provides the support for the following data types:
- Numbers:
uint8
,uint16
,uint32
,uint64
,uint128
,uint(nBytes)
,compactInt
,byte
,long
- Primitives:
bool
,string
- Arrays:
sizedByteArray(n)
- only content is encoded/decoded), size is thus known in advancebyteArray
- size can vary, so the size is also encoded/decoded alongside with the content
- Compound types:
vector<D>
- List of objects of the some data typeoptional<D>
- Nullable container for other data typepair<D1, D2>
enum(D1, D2, D3...)
- like union in C, stores only one value at once, but this value can have different data typeenum<E : Enum>
- for classical Kotlin enum
If the decoding/encoding cannot be done using standart data types, you can create your own by extending DataType<T>
:
object Delimiter : DataType<Byte>() {
override fun conformsType(value: Any?): Boolean {
return value is Byte && value == 0
}
override fun read(reader: ScaleCodecReader): Byte {
val read = reader.readByte()
if (read != 0.toByte()) throw java.lang.IllegalArgumentException("Delimiter is not 0")
return 0
}
override fun write(writer: ScaleCodecWriter, ignored: Byte) {
writer.writeByte(0)
}
}
And use it in your schema using custom()
keyword:
object CustomTypeTest : Schema<CustomTypeTest>() {
val delimiter by custom(Delimiter)
}
You can supply and default values for each field in the schema:
object DefaultValues : Schema<DefaultValues>() {
val bytes by sizedByteArray(length = 10, default = ByteArray(10))
val text by string(default = "Default")
val bigInteger by uint128(default = BigInteger.TEN)
}
By default, all fields are non null. However, you can use optional()
to change the default behavior:
object Person : Schema<Person>() {
val friendName by string().optional() // friendName now is Field<String?>
}
SS58 is an address format using in substate ecosystem. You can encode/decode address using SS58Encoder
:
val encoder = SS58Encoder()
val address = encoder.encode(publicKey, addressByte)
val accountId = encoder.decode(address)
Library provides an implementation of SocketService
, which simplifies communication with the node: it provides a seamless error recovery, subscription mechanism.
To create a socket service, you need to provide several parameters:
val reconnector = Reconnector(..) // to configure reconnect strategy and scheduling executor
val requestExecutor = RequestExecutor(..) // to configure sending executor
val socketService = SocketService(gson, logger, websocketFactory, reconnector, requestExecutor)
socketService.start(url) // async connect
socketService.stop() // all subscriptions/pending requests are cancelled
socketService.switchUrl(newUrl) // stops current connection and start a new one
// execute single request
socketService.executeRequest(runtimeRequest, deliveryType, object : SocketService.ResponseListener<RpcResponse> {
override fun onNext(response: RpcResponse) {
// success
}
override fun onError(throwable: Throwable) {
// unrecoverable error happened
}
})
// subscribe to changes
socketService.subscribe(runtimeRequest, object : SocketService.ResponseListener<SubscriptionChange> {
override fun onNext(response: SubscriptionChange) {
// change arrived
}
override fun onError(throwable: Throwable) {
// unrecoverable error happened
}
})
During setup of Reconnector
, you can specify a ReconnectStrategy
. There are several of them bundled with library:
ConstantReconnectStrategy
LinearReconnectStrategy
ExponentialReconnectStrategy
. This is a default reconnect strategy.
You can create your own strategy by implementing ReconnectStrategy
interface.
While sending a request, you can specify a DeliveryType
. Currently, there are 3 of them:
AT_LEAST_ONCE
- attempts to send request until succeeded. This is a default delivery type.AT_MOST_ONCE
- send request once, reports error if attempt failed.ON_RECONNECT
- similar toAT_LEAST_ONCE
, but remembers request and sends it on each reconnect. Currently used for subscription initiation.
Library has a out-of-box support for coroutines:
scope.launch {
val response = socketService.executeAsync(request, deliveryType) // suspend function
}
socketService.subscriptionFlow(request).onEach { change ->
// do stuff here
}.launchIn(scope)
The mappers for most common types are provided:
scale
- For scale-encoded valuesscaleCollection
- For list of scale-encoded valuespojo
- for json valuespojoList
- for list of json values
All mappers return a nullable
result by default. You can add nonNull()
modifier to change this behavior. In case of null result, the RpcException
will be thrown.
scale().nonNull().map(response, gson)
// or with coroutines adapter
socketService.executeAsync(request, deliveryType, mapper = scale().nonNull())