Substrate密钥链 0.1.0

Substrate密钥链 0.1.0

Yehor Popovych维护。



 
依赖
Substrate= 0.1.0
UncommonCrypto~> 0.2.0
Bip39.swift~> 0.2.0
Sr25519/Sr25519~> 0.2.0
Sr25519/Ed25519~> 0.2.0
CSecp256k1~> 0.2.0
 

  • Tesseract Systems, Inc.

Substrate.swift

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

基于Substrate网络的Swift SDK

入门指南

安装

包管理器

将以下依赖项添加到您的 Package.swift

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

并将库依赖项添加到您的目标中

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

运行 swift build 并构建您的应用程序。

CocoaPods

将以下内容添加到您的 Podfile

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

然后运行 pod install

示例和文档

此存储库包含 示例项目,它是了解 SDK API 的良好起点。

文档是个不错的学习步骤,它有助于了解如何实现自定义配置和静态类型。

以下是一组带有动态配置的顶级 API 示例,这些示例可以在大多数基于 Substrate 的网络上使用。

初始化

初始化 Api 需要客户端和运行时配置。目前只有一个客户端 - 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())")

运行时调用

对于动态运行时调用,节点应支持元数据版本15。

// 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)")

存储键

存储API通过StorageEntry辅助工具工作,可以为提供的StorageKey类型创建。

为存储创建StorageEntry

// 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")

获取键值

当有条目时,可以获取键值。

// 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键支持迭代。《StorageEntry》为此功能提供了辅助程序。

// 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)")
}

订阅变更

如果API客户端支持订阅,则可以订阅存储变更。

// 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)")
}

自定义RPC调用

当前SDK仅封装了其API所需的RPC调用。对于更多调用,可以使用通用API调用。

// 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签名者

Substrate SDK 提供了 Signer 协议,该协议可以用于实现外部的签名。

Signer 可以有两种不同的使用方式

  1. 可以直接提供给外部签名的调用
  2. 可以存储到 Api 对象的 signer 属性中。在这种情况下,应该提供账户的 PublicKey 以用于签名调用。

第一种方式适合密钥对签名。第二种方式更适合完整的关键链。

Signer 协议

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 提供了支持 Secp256k1、Ed25519 和 Sr25519 密钥的简单内存中的关键链。

密钥对

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

为多个密钥对提供内存中存储(支持多账户)。

初始化和基础 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

可以将钥匙串设置为 Signer 以简化 Api 实例的账户管理和签名。

// 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

钥匙串有一个可以用于选择签名协议公钥的委托对象。它可以用于向用户显示带有账户选择器的UI或实现自定义逻辑。

默认实现了 KeychainDelegateFirstFound,它从钥匙串返回第一个找到的兼容密钥。

协议看起来像这样

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 支持批量调用:[如何批量交易](https://polkadot.js.org/docs/api/cookbook/tx#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遵循Apache 2.0授权。更多信息请参阅授权文件.