The Flow Swift SDK provides Swift developers to build decentralized apps on Apple devices that interact with the Flow blockchain.
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '13.0'
use_frameworks!
target 'ExampleApp' do
pod 'FlowSDK', '~> 0.7.1'
end
- File > Swift Packages > Add Package Dependency
- Add
https://github.com/portto/flow-swift-sdk.git
- Select "Up to Next Major" with "0.7.0"
Before sending out any transactions, please install flow-cli and start emulator first.
Check out Flow Access API Specification for all apis.
Flow blockchain uses ECDSA with SHA2-256 or SHA3-256 to grant access to control user accounts.
Create a random private key for P256 and secp256k1 curve:
import FlowSDK
let privateKey1 = try PrivateKey(signatureAlgorithm: .ecdsaP256)
let privateKey2 = try PrivateKey(signatureAlgorithm: .ecdsaSecp256k1)
A private key has an accompanying public key:
let publicKey = privateKey.publicKey
You must start emulator to send this transaction. Once you have a key pair, you can create a new account using:
import FlowSDK
import BigInt
// Generate a new private key
let privateKey = try PrivateKey(signatureAlgorithm: .ecdsaSecp256k1)
// Get the public key
let publicKey = privateKey.publicKey
// Get flow grpc client
let client = Client(network: .emulator)
// Define creating account script
let script = """
import Crypto
transaction(publicKey: PublicKey, hashAlgorithm: HashAlgorithm, weight: UFix64) {
prepare(signer: AuthAccount) {
let account = AuthAccount(payer: signer)
// add a key to the account
account.keys.add(publicKey: publicKey, hashAlgorithm: hashAlgorithm, weight: weight)
}
}
"""
// Get service account info
let (payerAccount, payerAccountKey, payerSigner) = try await serviceAccount(client: client)
// Get latest reference block id
let referenceBlockId = try await client.getLatestBlock(isSealed: true)?.id
// Define creating account transaction
var transaction = try Transaction(
script: script.data(using: .utf8)!,
arguments: [
publicKey.cadenceArugment,
HashAlgorithm.sha3_256.cadenceArugment,
.ufix64(1000)
],
referenceBlockId: referenceBlockId!,
gasLimit: 100,
proposalKey: .init(
address: payerAccount.address,
keyIndex: payerAccountKey.index,
sequenceNumber: payerAccountKey.sequenceNumber),
payer: payerAccount.address,
authorizers: [payerAccount.address])
// Sign transaction
try transaction.signEnvelope(
address: payerAccount.address,
keyIndex: payerAccountKey.index,
signer: payerSigner)
// Send out transaction
let txId = try await client.sendTransaction(transaction: transaction)
// Get transaction result
var result: TransactionResult?
while result?.status != .sealed {
result = try await client.getTransactionResult(id: txId)
sleep(3)
}
debugPrint(result)
private func serviceAccount(client: Client) async throws -> (account: Account, accountKey: AccountKey, signer: InMemorySigner) {
let serviceAddress = Address(hexString: "f8d6e0586b0a20c7")
let serviceAccount = try await client.getAccountAtLatestBlock(address: serviceAddress)!
let servicePrivateKey = try PrivateKey(
data: Data(hex: "7aac2988c5c3df3325d8cd679563cc974271f9505245da53e887fa3cc36c064f"),
signatureAlgorithm: .ecdsaP256)
let servicePublicKey = servicePrivateKey.publicKey
let serviceAccountKeyIndex = serviceAccount.keys.firstIndex(where: { $0.publicKey == servicePublicKey })!
let serviceAccountKey = serviceAccount.keys[serviceAccountKeyIndex]
let signer = InMemorySigner(privateKey: servicePrivateKey, hashAlgorithm: .sha3_256)
return (account: serviceAccount, accountKey: serviceAccountKey, signer: signer)
}
Below is a simple transaction of printing "Hello World!"
import FlowSDK
let myAddress: Address
let myAccountKey: AccountKey
let myPrivateKey: PrivateKey
// Get flow grpc client
let client = Client(network: .emulator)
// Get latest reference block id
let referenceBlockId = try await client.getLatestBlock(isSealed: true)!.id
var transaction = Transaction(
script: "transaction { execute { log(\"Hello, World!\") } }".data(using: .utf8)!,
referenceBlockId: referenceBlockId,
gasLimit: 100,
proposalKey: .init(
address: myAddress,
keyIndex: myAccountKey.index,
sequenceNumber: myAccountKey.sequenceNumber),
payer: myAddress)
Transaction signing is done through the Signer protocol. The simplest (and least secure) implementation of Signer is InMemorySigner.
// create a signer from your private key and configured hash algorithm
let mySigner = InMemorySigner(privateKey: myPrivateKey, hashAlgorithm: myAccountKey.hashAlgorithm)
try transaction.signEnvelope(
address: myAddress,
keyIndex: myAccountKey.index,
signer: mySigner)
Flow introduces new concepts that allow for more flexibility when creating and signing transactions. Before trying the examples below, we recommend that you read through the transaction signature documentation.
- Proposer, payer and authorizer are the same account (
0x01
). - Only the envelope must be signed.
- Proposal key must have full signing weight.
Account | Key ID | Weight |
---|---|---|
0x01 |
1 | 1000 |
let client = Client(network: .emulator)
guard let referenceBlockId = try await client.getLatestBlock(isSealed: true)?.id else {
return
}
guard let account1 = try await client.getAccountAtLatestBlock(address: Address(hexString: "01")) else {
return
}
let key1 = account1.keys[0]
// create signer from securely-stored private key
let key1Signer: Signer = getSignerForKey1()
var transaction = Transaction(
script: """
transaction {
prepare(signer: AuthAccount) { log(signer.address) }
}
""".data(using: .utf8)!,
referenceBlockId: referenceBlockId,
gasLimit: 100,
proposalKey: .init(
address: account1.address,
keyIndex: key1.index,
sequenceNumber: key1.sequenceNumber),
payer: account1.address,
authorizers: [account1.address])
// account 1 signs the envelope with key 1
try transaction.signEnvelope(address: account1.address, keyIndex: key1.index, signer: key1Signer)
- Proposer, payer and authorizer are the same account (
0x01
). - Only the envelope must be signed.
- Each key has weight 500, so two signatures are required.
Account | Key ID | Weight |
---|---|---|
0x01 |
1 | 500 |
0x01 |
2 | 500 |
let client = Client(network: .emulator)
guard let referenceBlockId = try await client.getLatestBlock(isSealed: true)?.id else {
return
}
guard let account1 = try await client.getAccountAtLatestBlock(address: Address(hexString: "01")) else {
return
}
let key1 = account1.keys[0]
let key2 = account1.keys[1]
// create signer from securely-stored private key
let key1Signer: Signer = getSignerForKey1()
let key2Signer: Signer = getSignerForKey2()
var transaction = Transaction(
script: """
transaction {
prepare(signer: AuthAccount) { log(signer.address) }
}
""".data(using: .utf8)!,
referenceBlockId: referenceBlockId,
gasLimit: 100,
proposalKey: .init(
address: account1.address,
keyIndex: key1.index,
sequenceNumber: key1.sequenceNumber),
payer: account1.address,
authorizers: [account1.address])
// account 1 signs the envelope with key 1
try transaction.signEnvelope(address: account1.address, keyIndex: key1.index, signer: key1Signer)
// account 1 signs the envelope with key 2
try transaction.signEnvelope(address: account1.address, keyIndex: key2.index, signer: key2Signer)
- Proposer and authorizer are the same account (
0x01
). - Payer is a separate account (
0x02
). - Account
0x01
signs the payload. - Account
0x02
signs the envelope.- Account
0x02
must sign last since it is the payer.
- Account
Account | Key ID | Weight |
---|---|---|
0x01 |
1 | 1000 |
0x02 |
3 | 1000 |
let client = Client(network: .emulator)
guard let referenceBlockId = try await client.getLatestBlock(isSealed: true)?.id else {
return
}
guard let account1 = try await client.getAccountAtLatestBlock(address: Address(hexString: "01")) else {
return
}
guard let account2 = try await client.getAccountAtLatestBlock(address: Address(hexString: "02")) else {
return
}
let key1 = account1.keys[0]
let key3 = account2.keys[0]
// create signer from securely-stored private key
let key1Signer: Signer = getSignerForKey1()
let key3Signer: Signer = getSignerForKey3()
var transaction = Transaction(
script: """
transaction {
prepare(signer: AuthAccount) { log(signer.address) }
}
""".data(using: .utf8)!,
referenceBlockId: referenceBlockId,
gasLimit: 100,
proposalKey: .init(
address: account1.address,
keyIndex: key1.index,
sequenceNumber: key1.sequenceNumber),
payer: account2.address,
authorizers: [account1.address])
// account 1 signs the envelope with key 1
try transaction.signPayload(address: account1.address, keyIndex: key1.index, signer: key1Signer)
// account 2 signs the envelope with key 3
try transaction.signEnvelope(address: account2.address, keyIndex: key3.index, signer: key3Signer)
- Proposer and authorizer are the same account (
0x01
). - Payer is a separate account (
0x02
). - Account
0x01
signs the payload. - Account
0x02
signs the envelope.- Account
0x02
must sign last since it is the payer.
- Account
- Account
0x02
is also an authorizer to show how to include two AuthAccounts into an transaction
Account | Key ID | Weight |
---|---|---|
0x01 |
1 | 1000 |
0x02 |
3 | 1000 |
let client = Client(network: .emulator)
guard let referenceBlockId = try await client.getLatestBlock(isSealed: true)?.id else {
return
}
guard let account1 = try await client.getAccountAtLatestBlock(address: Address(hexString: "01")) else {
return
}
guard let account2 = try await client.getAccountAtLatestBlock(address: Address(hexString: "02")) else {
return
}
let key1 = account1.keys[0]
let key3 = account2.keys[0]
// create signer from securely-stored private key
let key1Signer: Signer = getSignerForKey1()
let key3Signer: Signer = getSignerForKey3()
var transaction = Transaction(
script: """
transaction {
prepare(signer1: AuthAccount, signer2: AuthAccount) {
log(signer.address)
log(signer2.address)
}
}
""".data(using: .utf8)!,
referenceBlockId: referenceBlockId,
gasLimit: 100,
proposalKey: .init(
address: account1.address,
keyIndex: key1.index,
sequenceNumber: key1.sequenceNumber),
payer: account2.address,
authorizers: [account1.address, account2.address])
// account 1 signs the envelope with key 1
try transaction.signPayload(address: account1.address, keyIndex: key1.index, signer: key1Signer)
// account 2 signs the envelope with key 3
// note: payer always signs last
try transaction.signEnvelope(address: account2.address, keyIndex: key3.index, signer: key3Signer)
- Proposer and authorizer are the same account (
0x01
). - Payer is a separate account (
0x02
). - Account
0x01
signs the payload. - Account
0x02
signs the envelope.- Account
0x02
must sign last since it is the payer.
- Account
- Both accounts must sign twice (once with each of their keys).
Account | Key ID | Weight |
---|---|---|
0x01 |
1 | 500 |
0x01 |
2 | 500 |
0x02 |
3 | 500 |
0x02 |
4 | 500 |
let client = Client(network: .emulator)
guard let referenceBlockId = try await client.getLatestBlock(isSealed: true)?.id else {
return
}
guard let account1 = try await client.getAccountAtLatestBlock(address: Address(hexString: "01")) else {
return
}
guard let account2 = try await client.getAccountAtLatestBlock(address: Address(hexString: "02")) else {
return
}
let key1 = account1.keys[0]
let key2 = account1.keys[1]
let key3 = account2.keys[0]
let key4 = account2.keys[1]
// create signer from securely-stored private key
let key1Signer: Signer = getSignerForKey1()
let key2Signer: Signer = getSignerForKey2()
let key3Signer: Signer = getSignerForKey3()
let key4Signer: Signer = getSignerForKey4()
var transaction = Transaction(
script: """
transaction {
prepare(signer: AuthAccount) { log(signer.address) }
}
""".data(using: .utf8)!,
referenceBlockId: referenceBlockId,
gasLimit: 100,
proposalKey: .init(
address: account1.address,
keyIndex: key1.index,
sequenceNumber: key1.sequenceNumber),
payer: account2.address,
authorizers: [account1.address])
// account 1 signs the envelope with key 1
try transaction.signPayload(address: account1.address, keyIndex: key1.index, signer: key1Signer)
// account 1 signs the payload with key 2
try transaction.signPayload(address: account1.address, keyIndex: key2.index, signer: key2Signer)
// account 2 signs the envelope with key 3
// note: payer always signs last
try transaction.signEnvelope(address: account2.address, keyIndex: key3.index, signer: key3Signer)
// account 2 signs the envelope with key 4
// note: payer always signs last
try transaction.signEnvelope(address: account2.address, keyIndex: key4.index, signer: key4Signer)
You can submit a transaction to the network using the Access API client.
import FlowSDK
let client = Client(host: "localhost", port: 3569)
// or
// let client = Client(network: .emulator)
try await client.sendTransaction(transaction: transaction)
After you have submitted a transaction, you can query its status by transaction ID:
let result = try await client.getTransactionResult(id: txId)
result.status
will be one of the following values:
- unknown
- pending
- finalized
- executed
- sealed
- expired
Check out the documentation for more details.
You can use the executeScriptAtLatestBlock
method to execute a read-only script against the latest sealed execution state.
Here is a simple script with a single return value:
pub fun main(): UInt64 {
return 1 as UInt64
}
Run script and decode as Swift type:
import FlowSDK
let client = Client(network: .testnet)
let script = """
pub fun main(): UInt64 {
return 1 as UInt64
}
"""
let cadenceValue: Cadence.Value = try await client.executeScriptAtLatestBlock(script: script.data(using: .utf8)!)
let value: UInt64 = try cadenceValue.toSwiftValue()
You can use the getLatestBlock
method to fetch the latest block with sealed boolean flag:
import FlowSDK
let client = Client(network: .testnet)
let isSealed: Bool = true
let block = try await client.getLatestBlock(isSealed: isSealed)
Block contains BlockHeader and BlockPayload. BlockHeader contains the following fields:
- id: the ID (hash) of the block
- parentId: the ID of the previous block.
- height: the height of the block.
- timestamp: the block timestamp.
BlockPayload contains the folowing fields:
- collectionGuarantees: an attestation signed by the nodes that have guaranteed a collection.
- seals: the attestation by verification nodes that the transactions in a previously executed block have been verified.
You can use the getEventsForHeightRange
method to query events.
import FlowSDK
let client = Client(network: .testnet)
let events: [BlockEvents] = try await client.getEventsForHeightRange(
eventType: "flow.AccountCreated",
startHeight: 10,
endHeight: 15)
An event type contains the following fields:
The event type to filter by. Event types are namespaced by the account and contract in which they are declared.
For example, a Transfer
event that was defined in the Token
contract deployed at account 0x55555555555555555555
will have a type of A.0x55555555555555555555.Token.Transfer
.
Read the language documentation for more information on how to define and emit events in Cadence.
You can use getAccountAtLatestBlock to query the state of an account.
let client = Client(network: .testnet)
let address = Address(hexString: "0xcb2d04fc89307107")
let account = try await client.getAccountAtLatestBlock(address: address)
An Account
contains the following fields:
- address: the account address.
- balance: the account balance.
- keys: a list of the public keys associated with this account.
- contracts: the contract code deployed at this account.
Check out example that how to use the SDK to interact wit Flow blockchain.
This repo was inspired from flow-go-sdk and make it more Swifty.
make install
make generate-protobuf
Flow Swift SDK is available under the Apache 2.0 license. Same with gRPC-Swift.