IcpKit 是一个全面的 iOS 软件包,用于编写与互联网计算机协议 (ICP) 交互的移动应用程序,使用 Swift 编写。IcpKit 旨在促进 iOS 应用程序与 ICP 区块链之间的交互。
有关 ICP 开发的更多信息,我们建议从 https://internetcomputer.org/docs/current/references/ 开始。
此软件包的主要开发者是 Konstantinos Gaitanis。
此软件包由 Bity SA 在 DFinity 基金会开发者资助计划的帮助下构建。
MIT 许可证适用于所有 Swift 代码(参见 LICENSE)。
BLS12381 Rust 库已获得 Levi Feldman 的许可(参见 LICENSE)。
将 IcpKit 作为依赖项添加到您的 Xcode 项目非常容易,只需将其添加到 Package.swift
的 dependencies
值中即可。
...
dependencies: [
.package(url: "https://github.com/kosta-bity/IcpKit.git", .upToNextMajor(from: "0.2.0"))
]
...
IcpKit
软件包定义了两个库 Candid
和 IcpKit
。Candid
实现了 Candid 规范,IcpKit
实现了与容器的通信。要在您的代码中使用
import Candid // not needed when using the CodeGenerator
import IcpKit
IcpKit 将负责与 ICP 通信所需的所有编码、序列化和密码学操作,使开发人员能够专注于应用程序的实际功能并启动其开发周期。
CodeGenerator
命令行工具,用于解析 .did 文件并生成可以直接添加到您的项目中的 Swift 代码。CandidEncoder
和 CandidDecoder
,用于将任何 Encodable
/Decodable
转换为 CandidValue
IcpKit
软件包分为 3 个产品
Candid 是用于与任何 ICP 容器通信的语言。它描述了容器使用的数据类型,以及可以在容器上调用的方法。Candid 本质上是这些类型的文本和二进制表示形式。二进制表示形式是实际发送/接收到/从容器的数据,而文本表示形式用于 Candid 接口定义文件 (.did
) 中,以描述容器的接口。
如果您使用 CodeGenerator
工具,那么您将永远不必直接使用 Candid
与容器交互。
Candid 库的主要类如下
类 | 描述 |
---|---|
CandidValue |
在 Swift 中表示 CandidValue 。例如 .bool(true) |
CandidType |
表示 CandidValue 的类型。例如 .option(.integer) |
CandidSerialiser 和 CandidDeserialiser |
将任何 CandidValue 转换为/从其二进制表示形式转换,该二进制表示形式可以直接发送/接收到/从容器。注意: 不支持序列化递归 candid 值。 |
CandidEncoder 和 CandidDecoder |
将任何 Encodable / Decodable Swift 对象转换为/从 CandidValue 转换。参见 Swift/Candid 编码规则 |
CandidParser |
解析 .did 文件的内容,并根据解析的文件类型返回 CandidInterfaceDefinition 、CandidType 或 CandidValue (参见 Candid 接口定义文件)。CandidParser 主要用于提供的命令行工具,但也可以直接从您的应用程序中调用,用于从在线 .did 文件读取值或类似情况。 |
IcpKit
库构建于 Candid
之上,负责与容器的实际通信。IcpKit
的作用是抽象出调用容器函数时使用的底层数据结构。
IcpKit
库的主要类如下
类 | 描述 |
---|---|
ICPRequestClient |
负责向容器的方法发出 HTTP 请求。它接收方法的参数作为 CandidValue ,并将容器的结果作为 CandidValue 返回。在内部,它将使用 CandidSerialiser 序列化 candid 参数,当提供 ICPSigningPrincipal 时对请求进行签名,最后将所有内容包装在 CBOR 中。当收到响应时,将执行相反的过程以返回 CandidValue 。 |
ICPFunction<T,R> |
在 ICPRequestClient 周围包装 CandidEncoder ,以便可以使用 Swift 对象而不是 CandidValue 调用容器方法。同样,容器的响应使用 CandidDecoder 解码,以返回 Swift 对象。 |
LedgerCanister |
作为示例代码提供,并且是使用 CodeGenerator 从 Ledger.did 文件生成的代码。它允许查询任何 ICP 帐户的 ICP 余额,并获取任何 ICP 区块的详细信息。 |
ICPSigningPrincipal |
您的应用程序必须实现的协议,以便对请求进行签名。该库提供了使用私钥 ICPCryptography.ellipticSign() 对任意数据进行签名的功能,但处理私钥是您的应用程序的责任。实现具体的 ICPSigningPrincipal 需要您获取所需的私钥,然后调用 ellipticSign 并返回结果。参见 如何创建 Principal |
ICPRequestClient 类是该库的通信主力,允许向任何容器发出方法调用。
Ledger Canister 作为示例实现提供,也允许轻松创建 ICP 钱包应用程序。
要使用 CodeGenerator
,您必须首先克隆项目
git clone https://github.com/kosta-bity/IcpKit
cd IcpKit
然后,您可以使用 Swift 编译和运行,或者自己构建可执行文件。
从 IcpKit
根文件夹
./compile_code_generator.sh
您将在文件夹 .build/release/CodeGenerator
中找到可执行文件。运行不带任何参数的 CodeGenerator
将显示帮助信息。
或者,您可以使用 swift 从 IcpKit
根文件夹运行它
swift run CodeGenerator
容器由用于与其交互的所有类型和方法定义。大多数容器以 此处 定义的 .did
格式发布其定义。
IcpKit 可以解析这些文件并生成 Swift 代码,该代码可以包含在您的 iOS/Mac 项目中以与容器交互。这有效地抽象化了有关通信的所有技术细节。
.did
文件可以是以下两种类型之一
.did
文件。.did
文件。假设您有一个 .did
服务接口定义文件,与容器交互的步骤如下
CodeGeneratorTool
为 .did
文件生成 Swift 代码。UnnamedType<n>
,可以重命名为更易读的名称。这是一个简单的接口 MyDid.did
type MyVector = vec opt bool;
type FooResult = record { name: text; count: int8 };
service: {
foo: (input: MyVector; sorted: bool) -> (FooResult) query
};
使用命令
CodeGenerator -n SimpleExample MyDid.did
我们得到了生成的 SimpleExample.swift
文件,其内容如下
import IcpKit
enum SimpleExample {
typealias MyVector = [Bool?]
struct FooResult: Codable {
let name: String
let count: Int8
}
class Service: ICPService {
func foo(input: MyVector, sorted: Bool) async throws -> FooResult {
let caller = ICPQuery<CandidTuple2<MyVector, Bool>, FooResult>(canister, "foo")
let response = try await caller.callMethod(.init(input, sorted), client, sender: sender)
return response
}
}
}
然后,我们可以将此文件添加到我们的项目中,并在我们的代码中的任何位置使用它,如下所示
let client = ICPRequestClient()
let myService = SimpleExample.Service("aaaaa-aa", client)
let myVector: SimpleExample.MyVector = [true, false, nil]
let record = try await myService.foo(input: myVector, sorted: false)
print(record.name)
print(record.count)
生成的代码是完全类型安全的,并避免了常见的错误,例如错误的数字类型等...
值 .did
文件的示例是以下 phone_book.did
(
vec {
record { name = "MyName"; phone = 1234 : nat; };
record { name = "AnotherName"; phone = 4321 : nat; };
}
)
可以使用以下命令解析这些类型的文件
./CodeGenerator value -n PhoneBook phone_book.did
这将生成 PhoneBook.swift
,其内容如下
enum PhoneBook {
struct UnnamedType0: Codable {
let name: String
let phone: BigUInt
}
let cValue: [UnnamedType0] = [
.init(name: "MyName", phone: 1234),
.init(name: "AnotherName", phone: 4321),
]
}
您的应用程序可能需要下载 .did
文件,解析它并从中提取一些值。这可以使用 CandidParser
完成。例如,如果我们想下载并读取上面的 phone_book.did
let phoneBookDidContents: String = try await downloadDid() // download phone_book.did file and read it
let phoneBookCandidValue: CandidValue = try CandidParser().parseValue(phoneBookContents)
我们可以更进一步,将解析的 CandidValue
解码为 Swift struct
struct PhoneBookEntry: Decodable {
let name: String
let phone: BigUInt
}
let phoneBook: [PhoneBookEntry] = try CandidDecoder().decode(phoneBookCandidValue)
Candid | Swift | 注释 |
---|---|---|
bool |
Bool |
|
text |
String |
utf8 编码 |
int |
BigInt |
|
nat |
BigUInt |
|
int<n> |
Int<n> |
n = 8, 16, 32, 64 ,Int 对应于 int64 |
nat<n> |
UInt<n> |
n = 8, 16, 32, 64 ,UInt 对应于 nat64 |
blob |
Data |
|
opt <candid_type> |
<swift_type>? |
由于 Swift 编译器的限制,具有 nil 值的可选结构体被编码为 opt empty 。CandidEncoder().encode(MyStruct?.none) // .option(.empty) 无法确定 nil 结构体的类型) |
vec <candid_type> |
[<swift_type>] |
|
record |
struct <name>: Codable { ... } |
每个 record 都被编码/解码为 Swift Codable struct 。每个 record 项对应于具有相同名称的 struct 成员值。如果 candid 项仅按数字键入,则名称为 _<number> 。struct MyStruct: Codable { let a: Bool; let b: String? } 被编码为 record { a: bool; b: opt text; } ,而 record { bool; text; } 解码为 struct MyStruct2: Codable { let _0: Bool; let _1: String } |
variant |
enum <name>: Codable { ... } |
每个 variant 都被编码/解码为 Swift Codable enum 。每个 variant 情况对应于具有相同名称的 enum 情况。关联值使用其名称附加到每个情况(如果可用)。variant { winter, summer } 编码为 enum Season: Codable { case winter, summer } variant { status: int; error: bool; } 编码为 enum Status { case status(Int); case error(Bool) } |
function |
CandidFunctionProtocol |
不支持将 Swift 函数自动编码为 Candid Value,因为我们无法在没有值的情况下从 Swift 类型推断函数签名。这是由于 Swift 的类型系统限制。但是,允许解码。 |
service |
CandidServiceProtocol |
由于与函数相同的原因,不支持编码。 |
principal |
CandidPrincipal |
|
null |
nil |
|
empty |
nil |
|
reserved |
nil |
当从 CandidValue
解码 null
、reserved
或 empty
到 Swift
时,它们始终在 Swift
中解码为 nil
。
当从 Swift
编码 nil
到 CandidValue
时,它始终编码为 opt <sub_type>
。
Candid 二进制格式不支持 record、variant、function 等的键的文本表示形式...而是使用 此处 定义的简单哈希方法
hash(id) = ( Sum_(i=0..k) utf8(id)[i] * 223^(k-i) ) mod 2^32 where k = |utf8(id)|-1
这意味着当从二进制格式反序列化 CandidValue 时,我们无法访问生成哈希的原始键。
CandidDecoder
会自动处理所有 Swift structs
的此问题,方法是哈希 swift 名称,然后比较哈希值。
但是,由于 Swift 解码系统的工作方式,我们无法对 Swift enums
执行相同的操作。这意味着为了从反序列化的 CandidValue(基本上是通过 ICPRequestClient
接收的每个值)解码 Swift enum,我们需要定义 CodingKeys 如下
如果我们从具有命名值的 variant 解码
type NamedVariant = variant { one: int8; two: int16; three: record { a: int8; b: int16 } };
我们使用 CandidCodingKey
执行哈希处理
enum NamedVariant {
case one(Int8)
case two(Int16)
case three(a: Int8, b: Int16)
enum CodingKeys: String, CandidCodingKey {
case one, two, three
}
enum ThreeCodingKeys: String, CandidCodingKey {
case a, b
}
}
如果我们从具有未命名值的 variant 解码
type UnnamedVariant = variant { int8; int16; record { int8, int16} };
我们使用 Int, CodingKey
将整数值分配给键
enum UnnamedVariant {
case anyName(Int8)
case really(Int16)
case weCanChoose(here: Int8, too: Int16)
enum CodingKeys: Int, CodingKey {
case anyName, really, weCanChoose // as long as we keep the order here
}
enum WeCanChooseCodingKeys: Int, CodingKey {
case here, too // and here
}
}
有关更深入的理解,请参阅 Swift 文档中关于编码、解码和 Coding Keys 的内容。
有几种方法可以执行请求。取决于它是简单的查询还是请求实际更改了区块链的状态。
let method = ICPMethod(
canister: ICPSystemCanisters.ledger,
methodName: "account_balance",
arg: .record([
"account": .blob(account.accountId)
])
)
let response = try await client.query(method, effectiveCanister: ICPSystemCanisters.ledger)
等效地,我们也可以这样做
let response = try await client.query(.uncertified, method, effectiveCanister: ICPSystemCanisters.ledger)
let requestId = try await client.call(method, effectiveCanister: ICPSystemCanisters.ledger)
let response = try await client.pollRequestStatus(requestId: requestId, effectiveCanister: ICPSystemCanisters.ledger)
等效地,我们也可以这样做
let response = try await client.callAndPoll(requestId: requestId, effectiveCanister: ICPSystemCanisters.ledger)
甚至
let response = try await client.query(.certified, method, effectiveCanister: ICPSystemCanisters.ledger)
所有这些都具有完全相同的结果。
可以使用以下代码实现相同的目的,只是这次使用 ICPFunction
。这允许我们直接将 Encodable
馈送到函数,并接收回 Decodable
。ICPFunction
将确保输入从输入 Swift 类型正确编码为 CandidValue
,并且输出从 CandidValue
解码为输出 Swift 类型。
struct Result: Decodable {
let name: String
let count: Int?
}
// equivalent Candid definition:
// function (nat) -> ( record { name: text; count: opt int64; } )
let function = ICPFunction<BigUInt, Result>(canister, "foo")
let result = try await function.callMethod(12345)
我们建议使用 HorizontalSystems 的 HdWalletKit.Swift,以便从种子派生公钥/私钥对。ICP 派生路径为 m/44'/223'/0'/0/0
公钥必须采用未压缩形式。
ICPCryptography.selfAuthenticatingPrincipal(uncompressedPublicKey:)
创建 ICPPrincipal
实例。ICPSigningPrincipal
。ICPAccount.mainAccount(of:)
创建此 principal 的主帐户。这是一个最小的 ICPSigningPrincipal
实现。这仅作为示例给出。请勿在生产环境中使用此代码,因为将私钥保存在内存中通常不是一个好习惯。
sign
方法特意设置为 async
,以允许任何类型的延迟获取私钥(例如,远程访问或访问 iOS Secure Enclave)。
class SimpleSigningPrincipal: ICPSigningPrincipal {
private let privateKey: Data
let rawPublicKey: Data
let principal: ICPPrincipal
init(publicKey: Data, privateKey: Data) throws {
self.privateKey = privateKey
self.rawPublicKey = publicKey
self.principal = try ICPCryptography.selfAuthenticatingPrincipal(uncompressedPublicKey: publicKey)
}
func sign(_ message: Data, domain: ICPDomainSeparator) async throws -> Data {
return try ICPCryptography.ellipticSign(message, domain: domain, with: privateKey)
}
static func fromMnemonic(_ words: [String]) throws -> SimplePrincipal {
let seed = HdWalletKit.Mnemonic.seed(mnemonic: words)!
let xPrivKey = HDExtendedKeyVersion.xprv.rawValue
let privateKey = try HDPrivateKey(seed: seed, xPrivKey: xPrivKey)
.derived(at: 44, hardened: true)
.derived(at: 223, hardened: true)
.derived(at: 0, hardened: true)
.derived(at: 0, hardened: false)
.derived(at: 0, hardened: false)
let publicKey = privateKey.publicKey(compressed: false)
return try SimplePrincipal(privateKey: privateKey.raw, uncompressedPublicKey: publicKey.raw)
}
}
CandidFunctionProtocol
,因为没有简单的方法来推断函数参数和结果中使用的 CandidTypes
。这意味着当调用期望另一个函数作为输入的容器方法时,自动代码生成将失败。CandidServiceProtocol
,因为函数无法编码。Structs
的编码无法推断结构体的成员。这些被编码为 opt empty
,根据 Candid 规范,所有容器都应该接受它。