Skip to content

tesseract-one/Substrate.swift

Repository files navigation

Substrate.swift

🐧 linux: ready GitHub license Build Status GitHub release SPM compatible CocoaPods version Platform OS X | iOS | tvOS | watchOS | Linux

Swift SDK for Substrate based networks

Getting started

Installation

Add the following dependency to your Package.swift:

.package(url: "https://github.com/tesseract-one/Substrate.swift.git", from: "0.0.1")

And add library dependencies to your target

// Main and RPC
.product(name: "Substrate", package: "Substrate.swift"),
.product(name: "SubstrateRPC", package: "Substrate.swift"),
// Keychain
.product(name: "SubstrateKeychain", package: "Substrate.swift"),

Run swift build and build your app.

Add the following to your Podfile:

# Main and RPC
pod 'Substrate', '~> 0.0.1'
# RPC
pod 'Substrate-RPC', '~> 0.0.1'
# Keychain
pod 'Substrate-Keychain', '~> 0.0.1'

Then run pod install

Examples and Documentation

This repository contains Examples project, which provides good starting point to get acquainted with SDK API.

Documentation is a good next step, it helps to understand how to implement own custom Configs and static types.

Below is a set of top-level API examples with Dynamic Config, which can be used on the most of the Substrate-based networks.

Initialization

For initialization Api needs client and runtime config. For now there is only one client - JsonRPC.

import Substrate
import SubstrateRPC

let nodeUrl = URL(string: "wss://westend-rpc.polkadot.io")!

// Dynamic Config should work for almost all Substrate based networks.
// It's not most eficient though because uses a lot of dynamic types
let substrate = try await Api(
    rpc: JsonRpcClient(.ws(url: nodeUrl)),
    config: .dynamicBlake2
)

Submit Extrinsic

import SubstrateKeychain

// Create KeyPair for signing
let mnemonic = "your key 12 words"
let from = try Sr25519KeyPair(parsing: mnemonic + "//Key1") // hard key derivation

// Create recipient address from ss58 string
let to = try substrate.runtime.address(ss58: "recipient s58 address")

// Dynamic Call type with Map parameters.
// any ValueRepresentable type can be used as parameter
let call = AnyCall(name: "transfer",
                   pallet: "Balances",
                   params: ["dest": to, "value": 15483812850])

// Create Submittable (transaction) from the call
let tx = try await substrate.tx.new(call)

// We are using direct signer API here
// Or we can set Keychain as signer in Api and provide `account` parameter
// `waitForFinalized()` will wait for block finalization
// `waitForInBlock()` will wait for inBlock status
// `success()` will search for failed event and throw in case of failure
let events = try await tx.signSendAndWatch(signer: from)
        .waitForFinalized()
        .success()

// `parsed()` will dynamically parse all extrinsic events.
// Check `ExtrinsicEvents` struct for more efficient search methods.
print("Events: \(try events.parsed())")

Runtime Calls

For dynamic runtime calls node should support Metadata v15.

// We can check does node have needed runtime call
guard substrate.call.has(method: "versions", api: "Metadata") else {
  fatalError("Node doesn't have needed call")
}

// Array<UInt32> is a return value for the call
// AnyValueRuntimeCall can be used for dynamic return parsing
let call = AnyRuntimeCall<[UInt32]>(api: "Metadata",
                                    method: "versions")

// Will parse vall result to Array<UInt32>
let versions = try await substrate.call.execute(call: call)

print("Supported metadata versions: \(versions)")

Constants

// It will throw if constant is not found or type is wrong
let deposit = try substrate.constants.get(UInt128.self, name: "ExistentialDeposit", pallet: "Balances")

// This will parse constant to dynamic Value<RuntimeType.Id>
let dynDeposit = try substrate.constants.dynamic(name: "ExistentialDeposit", pallet: "Balances")

print("Existential deposit: \(deposit), \(dynDeposit)")

Storage Keys

Storage API works through StorageEntry helper, which can be created for provided StorageKey type.

Create StorageEntry for storage

// dynamic storage key wirh typed Value
let entry = try substrate.query.entry(UInt128.self, name: "NominatorSlashInEra", pallet: "Stacking")

// dynamic storage key with dynamic Value
let dynEntry = substrate.query.dynamic(name: "NominatorSlashInEra", pallet: "Stacking")

Fetch key value

When we have entry we can fetch key values.

// We want values for this account.
let accountId = try substrate.runtime.account(ss58: "EoukLS2Rzh6dZvMQSkqFy4zGvqeo14ron28Ue3yopVc8e3Q")

// NominatorSlashInEra storage is Double Map (EraIndex, AccountId).
// We have to provide 2 keys to get value.

// optional value
let optSlash = try await entry.value([652, accountId])
print("Value is: \(optSlash ?? 0)")

// default value used when nil
let slash = try await entry.valueOrDefault([652, accountId])
print("Value is: \(slash)")

Key Iterators

Map keys support iteration. StorageEntry has helpers for this functionality.

// We can iterate over Key/Value pairs.
for try await (key, value) in entry.entries() {
  print("Key: \(key), value: \(value)")
}

// or only over keys
for try await key in entry.keys() {
  print("Key: \(key)")
}

// For maps where N > 1 we can filter iterator by first N-1 keys
// This will set EraIndex value to 652
let filtered = entry.filter(652)

// now we can iterate over filtered Key/Value pairs.
for try await (key, value) in filtered.entries() {
  print("Key: \(key), value: \(value)")
}

// or only over keys
for try await key in filtered.keys() {
  print("Key: \(key)")
}

Subscribe for changes

If Api Client support subscription we can subscribe for storage changes.

// Some account we want to watch
let ALICE = try substrate.runtime.account(ss58: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY")

// Dynamic entry for System.Account storage key
let entry = try substrate.query.dynamic(name: "Account", pallet: "System")

// It's a Map parameter so we should pass key to watch
for try await account in entry.watch([ALICE]) {
  print("Account updated: \(account)")
}

Custom RPC calls

Current SDK wraps only RPC calls needed for its API. For more calls common call API can be used.

// Simple call
let blockHash: Data = try await substrate.rpc.call(method: "chain_getBlockHash", params: Params(0))

// Subscription
let stream = try await substrate.rpc.subscribe(
  method: "chain_subscribeNewHeads",
  params: Params(),
  unsubscribe: "chain_unsubscribeNewHeads",
  DynamicConfig.TBlock.THeader.self
)

Substrate Signer

Substrate SDK provides Signer protocol, which can be implenented for Extrinsic signing.

Signer can be used in two different ways:

  1. It can be provided directly to the Extrinsic signing calls
  2. It can be stored into signer property of the Api object. In this case account PublicKey should be provided for signing calls.

First way is good for KeyPair signing. Second is better for full Keychain.

Signer protocol:

protocol Signer {
    // get account with proper type
    func account(type: KeyTypeId, algos: [CryptoTypeId]) async -> Result<any PublicKey, SignerError>
    
    // Sign extrinsic payload
    func sign<RC: Config, C: Call>(
        payload: SigningPayload<C, RC.TExtrinsicManager>,
        with account: any PublicKey,
        runtime: ExtendedRuntime<RC>
    ) async -> Result<RC.TSignature, SignerError>
}

Keychain API

SDK provides simple in-memory keychain with Secp256k1, Ed25519 and Sr25519 keys support.

Key Pairs

import SubstrateKeychain

// Initializers
// New with random key (OS secure random is used)
let random = EcdsaKeyPair() // or Ed25519KeyPair / Sr25519KeyPair
// From Bip39 mnemonic
let mnemonic = Sr25519KeyPair(phrase: "your words")
// JS compatible path based parsing
let path = try Ed25519KeyPair(parsing: "//Alice")

// Key derivation (JS compatible)
let derived = try mnemonic.derive(path: [PathComponent(string: "//Alice")])

// Sign and verify
let data = Data(repeating: 1, count: 20)
let signature = derived.sign(message: data)
let isSigned = derived.verify(message: data, signature: signature)

Keychain

In-memory storage for multiple KeyPairs (multi-account support).

Initialization and base API
import SubstrateKeychain

// Create empty keychain object with default delegate
let keychain = Keychain()

// Will be returned for account request
keychain.add(derived, for: .account)
// Key for ImOnline
keychain.add(random, for: .imOnline)
// Key for all types of requests
keychain.add(path)

// Search for PubKey registered for 'account' requests
let pubKey = keychain.publicKeys(for: .account).first!
// get KeyPair for this PubKey
let keyPair = keychain.keyPair(for: pubKey)!
Api Signer integration

Keychain can be set as Signer for Api instance for simpler account management and signing.

// Set substrate signer to created keychain instance
substrate.signer = keychain

// Fetch account from Keychain (it will call Keychain delegate for selection)
// Can be stored and reused as needed (active account)
let from = try await substrate.tx.account()

// Signer will return PublicKey from Keychain
// which should be converted to account or address for extrinsics
print("Account: \(try from.account(in: substrate))")

// We can provide this PublicKey as `account` to calls for signing.
// Signing call will be sent to Keychain through Signer protocol
let events = try await tx.signSendAndWatch(account: from)
        .waitForFinalized()
        .success()

// print parsed events
print("Events: \(try events.parsed())")
Keychain Delegate

Keychain has delegate object which can select public keys for the Signer protocol. It can be used to show UI with account selecter to the user or implement custom logic.

There is default KeychainDelegateFirstFound implementation which returns first found compatible key from the Keychain.

Protocol looks like this:

enum KeychainDelegateResponse {
    case cancelled
    case noAccount
    case account(any PublicKey)
}

protocol KeychainDelegate: AnyObject {
    func account(in keychain: Keychain,
                 for type: KeyTypeId,
                 algorithms algos: [CryptoTypeId]) async -> KeychainDelegateResponse
}

Batch Extrinsics

SDK supports batch calls: #how-can-i-batch-transactions

// Fetch account from Keychain (should be set in substrate as signer)
let from = try await substrate.tx.account()

// Create recipient addresses from ss58 string
let to1 = try substrate.runtime.address(ss58: "recipient1 s58 address")
let to2 = try substrate.runtime.address(ss58: "recipient2 s58 address")

// Create 2 calls
let call1 = AnyCall(name: "transfer",
                    pallet: "Balances",
                    params: ["dest": to1, "value": 15483812850])
let call2 = AnyCall(name: "transfer",
                    pallet: "Balances",
                    params: ["dest": to2, "value": 21234567890])

// Create batch transaction from the calls (batch or batchAll methods)
let tx = try await substrate.tx.batchAll([call1, call2])

// Sign, send and watch for finalization
let events = try await tx.signSendAndWatch(account: from)
        .waitForFinalized()
        .success()

// Parsed events
print("Events: \(try events.parsed())")

Author

License

Substrate.swift is available under the Apache 2.0 license. See the LICENSE file for more information.

About

Swift APIs for Polkadot and any Substrate-based chain.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages