Skip to content

p2p-org/solana-swift

Repository files navigation

SolanaSwift

Solana-blockchain client, written in pure swift.

Version License Platform Documentation Status

Breaking changes

v5.0

...

  • Remove deprecated typealias Mint, use SPLTokenMintState or Token2022MintState instead.
  • Remove deprecated typealias Wallet, use AccountBalance instead.
  • Support token 2022 via method getAccountBalances (See GetAccountBalancesTests).
  • Support token 2022 and Token2022Program. ... See more

v2.0

  • From v2.0.0 we officially omited Rx library and a lot of dependencies, thus we also adopt swift concurrency to solana-swift. What have been changed?
  • For those who still use SolanaSDK class, follow this link

Features

  • Supported swift concurrency (from 2.0.0)
  • Key pairs generation
  • Solana JSON RPC API
  • Create, sign transactions
  • Send, simulate transactions
  • Solana token list
  • Socket communication
  • OrcaSwapSwift
  • RenVMSwift
  • Token2022

Requirements

  • iOS 13 or later

Dependencies

  • TweetNacl
  • secp256k1.swift
  • Task_retrying

Installation

Cocoapods

SolanaSwift is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'SolanaSwift', '~> 5.0.0'

Swift package manager

...
dependencies: [
    ...
    .package(url: "https://github.com/p2p-org/solana-swift", from: "5.0.0")
],
...

How to use

Import

import SolanaSwift

Logger

Create a logger that confirm to SolanaSwiftLogger

import SolanaSwift

class MyCustomLogger: SolanaSwiftLogger {
    func log(event: String, data: String?, logLevel: SolanaSwiftLoggerLogLevel) {
        // Custom log goes here
    }
}

// AppDelegate or somewhere eles

let customLogger: SolanaSwiftLogger = MyCustomLogger()
SolanaSwift.Logger.setLoggers([customLogger])

AccountStorage

Create an SolanaAccountStorage for saving account's keyPairs (public and private key), for example: KeychainAccountStorage for saving into Keychain in production, or InMemoryAccountStorage for temporarily saving into memory for testing. The "CustomAccountStorage" must conform to protocol SolanaAccountStorage, which has 2 requirements: function for saving save(_ account:) throws and computed property account: Account? { get thrrows } for retrieving user's account.

Example:

import SolanaSwift
import KeychainSwift
struct KeychainAccountStorage: SolanaAccountStorage {
    let tokenKey = <YOUR_KEY_TO_STORE_IN_KEYCHAIN>
    func save(_ account: Account) throws {
        let data = try JSONEncoder().encode(account)
        keychain.set(data, forKey: tokenKey)
    }
    
    var account: Account? {
        guard let data = keychain.getData(tokenKey) else {return nil}
        return try JSONDecoder().decode(Account.self, from: data)
    }
}

struct InMemoryAccountStorage: SolanaAccountStorage {
    private var _account: Account?
    func save(_ account: Account) throws {
        _account = account
    }
    
    var account: Account? {
        _account
    }
}

Create an account (keypair)

let account = try await KeyPair(network: .mainnetBeta)
// optional
accountStorage.save(account)

Restore an account from a seed phrase (keypair)

let account = try await KeyPair(phrases: ["miracle", "hundred", ...], network: .mainnetBeta, derivablePath: ...)
// optional
accountStorage.save(account)

Solana RPC Client

APIClient for Solana JSON RPC API. See Documentation

Example:

import SolanaSwift

let endpoint = APIEndPoint(
    address: "https://api.mainnet-beta.solana.com",
    network: .mainnetBeta
)

// To get block height
let apiClient = JSONRPCAPIClient(endpoint: endpoint)
let result = try await apiClient.getBlockHeight()

// To get balance of the current account
guard let account = try? accountStorage.account?.publicKey.base58EncodedString else { throw UnauthorizedError }
let balance = try await apiClient.getBalance(account: account, commitment: "recent")

Wait for confirmation method.

// Wait for confirmation
let signature = try await blockChainClient.sendTransaction(...)
try await apiClient.waitForConfirmation(signature: signature, ignoreStatus: true) // transaction will be mark as confirmed after timeout no matter what status is when ignoreStatus = true
let signature2 = try await blockchainClient.sendTransaction(/* another transaction that requires first transaction to be completed */)

Observe signature status. In stead of using socket to observe signature status, which is not really reliable (socket often returns signature status == finalized when it is not fully finalized), we observe its status by periodically sending getSignatureStatuses (with observeSignatureStatus method)

// Observe signature status with `observeSignatureStatus` method
var statuses = [TransactionStatus]()
for try await status in apiClient.observeSignatureStatus(signature: "jaiojsdfoijvaij", timeout: 60, delay: 3) {
    print(status)
    statuses.append(status)
}
// statuses.last == .sending // the signature is not confirmed
// statuses.last?.numberOfConfirmations == x // the signature is confirmed by x nodes (partially confirmed)
// statuses.last == .finalized // the signature is confirmed by all nodes

Batch support

// Batch request with different types
let req1: JSONRPCAPIClientRequest<AnyDecodable> = JSONRPCAPIClientRequest(method: "getAccountInfo", params: ["63ionHTAM94KaSujUCg23hfg7TLharchq5BYXdLGqia1"])
let req2: JSONRPCAPIClientRequest<AnyDecodable> = JSONRPCAPIClientRequest(method: "getBalance", params: ["63ionHTAM94KaSujUCg23hfg7TLharchq5BYXdLGqia1"])
let response = try await apiClient.batchRequest(with: [req1, req2])

// Batch request with same type
let balances: [Rpc<UInt64>?] = try await apiClient.batchRequest(method: "getBalance", params: [["63ionHTAM94KaSujUCg23hfg7TLharchq5BYXdLGqia1"], ["63ionHTAM94KaSujUCg23hfg7TLharchq5BYXdLGqia1"], ["63ionHTAM94KaSujUCg23hfg7TLharchq5BYXdLGqia1"]])

For the method that is not listed, use generic method request(method:params:) or request(method:) without params.

let result: String = try await apiClient.request(method: "getHealth")
XCTAssertEqual(result, "ok")

Solana Blockchain Client

Prepare, send and simulate transactions. See Documentation

Example:

import SolanaSwift

let blockchainClient = BlockchainClient(apiClient: JSONRPCAPIClient(endpoint: endpoint))

/// Prepare any transaction, use any Solana program to create instructions, see section Solana program. 
let preparedTransaction = try await blockchainClient.prepareTransaction(
    instructions: [...],
    signers: [...],
    feePayer: ...
)

/// SPECIAL CASE: Prepare Sending Native SOL
let preparedTransaction = try await blockchainClient.prepareSendingNativeSOL(
    account: account,
    to: toPublicKey,
    amount: 0
)

/// SPECIAL CASE: Sending SPL Tokens
let preparedTransactions = try await blockchainClient.prepareSendingSPLTokens(
    account: account,
    mintAddress: <SPL TOKEN MINT ADDRESS>,  // USDC mint
    decimals: 6,
    from: <YOUR SPL TOKEN ADDRESS>, // Your usdc address
    to: destination,
    amount: <AMOUNT IN LAMPORTS>
)

/// Simulate or send

blockchainClient.simulateTransaction(
    preparedTransaction: preparedTransaction
)

blockchainClient.sendTransaction(
    preparedTransaction: preparedTransaction
)

Solana Program

List of default programs and pre-defined method that live on Solana network:

  1. SystemProgram. See Documentation
  2. TokenProgram. See Documentation
  3. AssociatedTokenProgram. See Documentation
  4. OwnerValidationProgram. See Documentation
  5. TokenSwapProgram. See Documentation
  6. Token2022Program. See Documentation

Solana Tokens Repository

Tokens repository usefull when you need to get a list of tokens. See Documentation

Example:

let tokenRepository = TokensRepository(endpoint: endpoint)
let list = try await tokenRepository.getTokensList()

TokenRepository be default uses cache not to make extra calls, it can disabled manually .getTokensList(useCache: false)

Contribution

  • Welcome to contribute, feel free to change and open a PR.

Author

Chung Tran, chung.t@p2p.org

License

SolanaSwift is available under the MIT license. See the LICENSE file for more info.