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
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.
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
)
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())")
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)")
// 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 API works through StorageEntry
helper, which can be created for provided StorageKey
type.
// 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")
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)")
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)")
}
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)")
}
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 SDK provides Signer protocol, which can be implenented for Extrinsic signing.
Signer can be used in two different ways:
- It can be provided directly to the Extrinsic signing calls
- It can be stored into
signer
property of theApi
object. In this case accountPublicKey
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>
}
SDK provides simple in-memory keychain with Secp256k1, Ed25519 and Sr25519 keys support.
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)
In-memory storage for multiple KeyPairs (multi-account support).
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)!
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 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
}
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())")
Substrate.swift is available under the Apache 2.0 license. See the LICENSE file for more information.