Skip to content

Latest commit

 

History

History
594 lines (459 loc) · 18.6 KB

README.md

File metadata and controls

594 lines (459 loc) · 18.6 KB

Flow Swift SDK

The Flow Swift SDK provides Swift developers to build decentralized apps on Apple devices that interact with the Flow blockchain.

Getting Started

Installation

CocoaPods

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '13.0'
use_frameworks!

target 'ExampleApp' do
  pod 'FlowSDK', '~> 0.7.1'
end

Swift Package Manager

  • File > Swift Packages > Add Package Dependency
  • Add https://github.com/portto/flow-swift-sdk.git
  • Select "Up to Next Major" with "0.7.0"

Usage

Before sending out any transactions, please install flow-cli and start emulator first.

Check out Flow Access API Specification for all apis.

Generating Key

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

Creating an Account

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

Signing a Transaction

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 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 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.
  • 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)

Sending a Transaction

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)

Querying Transaction Results

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.

Executing a Script

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()

Querying Blocks

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.

Querying Events

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)

Event Type

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.

Querying Accounts

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.

Examples

Check out example that how to use the SDK to interact wit Flow blockchain.

Development

This repo was inspired from flow-go-sdk and make it more Swifty.

Generate protobuf swift files

make install
make generate-protobuf

License

Flow Swift SDK is available under the Apache 2.0 license. Same with gRPC-Swift.