kingpin-apps/swift-cardano-chain
A Swift library for interacting with the Cardano blockchain through a unified ChainContext protocol backed by six pluggable implementations.
Installation
Add the package in Xcode via File › Add Package Dependencies and enter the repository URL:
https://github.com/Kingpin-Apps/swift-cardano-chain.gitOr add it to your Package.swift:
.package(url: "https://github.com/Kingpin-Apps/swift-cardano-chain.git", from: "0.5.0")Then import it in your source files:
import SwiftCardanoChainOverview
SwiftCardanoChain provides a single ChainContext protocol and six concrete implementations. Pick the one that matches your environment — the rest of your code stays the same.
| Context | When to use | |---|---| | BlockFrostChainContext | Cloud API — no local node required | | KoiosChainContext | Decentralised community API — no local node required | | CardanoCliChainContext | Local node via cardano-cli | | OgmiosChainContext | Local node via the Ogmios WebSocket bridge | | NodeSocketChainContext | Local node via the NtC Unix socket directly | | OfflineTransferChainContext | Air-gapped / offline transaction signing |
All contexts support:
- Reading blockchain data (UTxOs, protocol parameters, genesis parameters, epoch, era, slot, chain tip)
- Submitting and evaluating transactions (with a backend-agnostic local UPLC evaluator for contexts that don't expose a remote evaluator)
- Querying stake addresses, pools, DReps, governance actions, and committee members
- Querying treasury balance, DRep / SPO stake distributions, full constitutional committee state, and per-proposal vote tallies
Getting Started
BlockFrost (Cloud — No Local Node)
import SwiftCardanoChain
// From an environment variable (recommended)
let context = try await BlockFrostChainContext(
network: .preview,
environmentVariable: "BLOCKFROST_API_KEY"
)
// Or with a project ID directly
let context = try await BlockFrostChainContext(
projectId: "previewXXXXXXXXXXXXXXXXXXXX",
network: .preview
)Koios (Community API — No Local Node)
Supports .mainnet, .preprod, .preview, .guildnet, and .sanchonet.
// Without an API key (rate-limited)
let context = try await KoiosChainContext(network: .mainnet)
// With an API key
let context = try await KoiosChainContext(
apiKey: "your-koios-api-key",
network: .mainnet
)Cardano CLI (Local Node)
let context = try await CardanoCliChainContext(
nodeConfig: FilePath("/opt/cardano/preview/config.json"),
binary: FilePath("/usr/local/bin/cardano-cli"),
socket: FilePath("/ipc/node.socket"),
network: .preview
)Ogmios (Local Node via WebSocket)
let context = try await OgmiosChainContext(
host: "localhost",
port: 1337,
network: .mainnet
)NodeSocket (Local Node — Direct NtC)
let context = NodeSocketChainContext(
socketPath: FilePath("/ipc/node.socket"),
network: .mainnet
)OfflineTransfer (Air-Gapped Signing)
// Load a transfer file prepared by an online machine
let context = try OfflineTransferChainContext(
filePath: FilePath("/path/to/transfer.json"),
network: .mainnet
)Reading Blockchain Data
UTxOs
let address = try Address(from: .string("addr1..."))
let utxos = try await context.utxos(address: address)
for utxo in utxos {
print("\(utxo.input.transactionId.payload.toHex)#\(utxo.input.index)")
print(" \(utxo.output.amount.coin) lovelace")
for (policyId, assets) in utxo.output.amount.multiAsset {
for (assetName, amount) in assets {
print(" \(policyId.payload.toHex).\(assetName.name.toHex) = \(amount)")
}
}
}
// Resolve a single UTxO by transaction input
if let (utxo, isSpent) = try await context.utxo(input: input) {
print(isSpent ? "Spent: \(utxo)" : "Unspent: \(utxo)")
}Protocol Parameters
let params = try await context.protocolParameters()
print("Min fee per byte : \(params.txFeePerByte)")
print("Fixed fee : \(params.txFeeFixed)")
print("Max tx size : \(params.maxTxSize)")
print("UTxO cost/byte : \(params.utxoCostPerByte)")Genesis Parameters
let genesis = try await context.genesisParameters()
print("Network magic : \(genesis.networkMagic)")
print("Slot length : \(genesis.slotLength)s")
print("Epoch length : \(genesis.epochLength) slots")
print("Security param : \(genesis.securityParam)")Current Chain State
let epoch = try await context.epoch()
let slot = try await context.lastBlockSlot()
let era = try await context.era()
print("Epoch \(epoch), slot \(slot), era \(era?.description ?? "unknown")")Chain Tip
For a single call that returns slot, block, epoch, era, and sync progress in one shot:
let tip = try await context.chainTip()
print("Slot : \(tip.slot)")
print("Block : \(tip.block ?? 0)")
print("Epoch : \(tip.epoch)")
print("Era : \(tip.era ?? "unknown")")
print("Sync progress : \(tip.syncProgress.map { "\($0)%" } ?? "n/a")")NodeSocket, Ogmios, and CardanoCLI populate every field; cloud APIs derive the values they can and leave the rest nil. OfflineTransfer derives slot and epoch from cached genesis parameters and wall-clock time.
Writing to the Blockchain
Submitting Transactions
All contexts accept transactions in three forms:
// Transaction object
let txId = try await context.submitTx(tx: .transaction(transaction))
// CBOR bytes
let txId = try await context.submitTx(tx: .bytes(cborData))
// CBOR hex string
let txId = try await context.submitTx(tx: .string("84a700..."))
print("Submitted: \(txId)")Evaluating Plutus Script Execution Units
Contexts with a remote evaluator (BlockFrost, Koios, Ogmios) call out directly:
let units = try await context.evaluateTx(tx: transaction)
for (redeemer, eu) in units {
print("\(redeemer): mem=\(eu.mem) steps=\(eu.steps)")
}For contexts without a remote evaluator (CardanoCLI, NodeSocket), the protocol exposes a local UPLC fallback. Fetch the resolved UTxOs for the transaction's inputs and reference inputs using whatever transport you have, then hand them off:
let resolvedInputs = try await fetchResolvedInputs(for: transaction)
let params = try await context.protocolParameters()
let units = try await context.evaluateTx(
tx: transaction,
resolvedInputs: resolvedInputs,
protocolParameters: params
)OfflineTransferChainContext returns execution units from OfflineTransfer.evaluations populated on the online machine — see the offline signing section below.
Staking Operations
let stakeAddress = try Address(from: .string("stake1..."))
let stakeInfo = try await context.stakeAddressInfo(address: stakeAddress)
for info in stakeInfo {
print("Rewards : \(info.rewardAccountBalance) lovelace")
print("Pool : \(info.stakeDelegation ?? "unregistered")")
print("DRep : \(info.delegateRepresentative ?? "none")")
}Treasury
let balance = try await context.treasury()
print("Treasury: \(balance) lovelace")Governance Queries (Conway Era)
DRep, Governance Action, and Committee Member Info
// DRep information
let drepInfo = try await context.drepInfo(drep: someDRep)
// Governance action details
let govInfo = try await context.govActionInfo(govActionID: someActionId)
// Committee member state, looked up by either cold or hot credential
let cmInfo = try await context.committeeMemberInfo(cold: coldCred)
let cmInfo2 = try await context.committeeMemberInfo(hot: hotCred)Per-Proposal Vote Tally
govActionVotes returns a GovActionVotes aggregate carrying the proposal procedure (deposit, return address, anchor) plus the three vote arrays (committee, DRep, stake-pool) and lifecycle epochs (proposed / expires / ratified / enacted / dropped / expired). Each empty array means no votes have been recorded for that voter class yet.
let votes = try await context.govActionVotes(govActionID: actionId)
print("Status : \(votes.status?.rawValue ?? "active")")
print("Deposit : \(votes.deposit) lovelace")
print("CC votes : \(votes.committeeVotes.count)")
print("DRep : \(votes.dRepVotes.count)")
print("Pool : \(votes.stakePoolVotes.count)")To pull every active proposal in one round-trip (equivalent to cardano-cli query gov-state | jq .proposals):
let all = try await context.govActionsAll()
let activeOnly = all.filter { $0.status == nil }Stake Distributions
Effective stake delegated to each DRep and each stake pool for the current epoch — the inputs ratifier code uses to decide proposal outcomes. These map to cardano-cli query drep-stake-distribution --all-dreps and cardano-cli query spo-stake-distribution --all-spos.
let drepStake = try await context.drepStakeDistribution()
let spoStake = try await context.spoStakeDistribution()Constitutional Committee State
let state = try await context.committeeState()
print("Quorum threshold: \(state.threshold)")
for member in state.members {
print("\(member.coldCredential) → \(member.hotCredential.map(String.init(describing:)) ?? "unauthorized")")
}Offline Signing Workflow
For air-gapped transaction signing, see the OfflineTransferChainContext:
// --- Online machine ---
var transfer = OfflineTransfer()
transfer.addUtxos(try await onlineContext.utxos(address: address), for: address)
transfer.protocol.protocolParameters = try await onlineContext.protocolParameters()
transfer.protocol.genesisParameters = try await onlineContext.genesisParameters()
transfer.protocol.era = try await onlineContext.era()
transfer.protocol.network = .mainnet
// Optionally cache governance and committee snapshots for offline reads
transfer.treasury = try await onlineContext.treasury()
transfer.govActionVotesList = try await onlineContext.govActionsAll()
transfer.drepStakeEntries = try await onlineContext.drepStakeDistribution()
transfer.spoStakeEntries = try await onlineContext.spoStakeDistribution()
transfer.committeeStateSnapshot = try await onlineContext.committeeState()
// Optionally pre-compute Plutus execution units so the offline machine can serve them
let units = try await onlineContext.evaluateTx(tx: tx)
transfer.evaluations.append(
OfflineTransferEvaluation(txCborHex: tx.toCBORData().toHex, executionUnits: units)
)
try transfer.save(to: FilePath("/path/to/transfer.json"))
// --- Copy file to offline machine ---
// --- Offline machine ---
let offlineContext = try OfflineTransferChainContext(
filePath: FilePath("/path/to/transfer.json"),
network: .mainnet
)
let utxos = try await offlineContext.utxos(address: address) // from the file
// ... build and sign transaction ...
try await offlineContext.submitTx(tx: .string(signedCborHex)) // writes to file
// --- Copy file back and submit online ---
let txId = try await onlineContext.submitTx(tx: .string(signedCborHex))Every read and write through the offline context appends a typed entry to OfflineTransfer.history, giving you a tamper-evident audit log of every action taken against the file.
Error Handling
All contexts throw CardanoChainError:
do {
let utxos = try await context.utxos(address: address)
} catch let error as CardanoChainError {
switch error {
case .blockfrostError(let msg): print("BlockFrost: \(msg ?? "")")
case .koiosError(let msg): print("Koios: \(msg ?? "")")
case .cardanoCLIError(let msg): print("CardanoCLI: \(msg ?? "")")
case .operationError(let msg): print("Operation: \(msg ?? "")")
case .transactionFailed(let msg): print("Tx failed: \(msg ?? "")")
case .invalidArgument(let msg): print("Bad argument: \(msg ?? "")")
case .unsupportedNetwork(let msg): print("Bad network: \(msg ?? "")")
case .offlineTransferError(let msg): print("Offline: \(msg ?? "")")
case .notImplemented(let msg): print("Not implemented: \(msg ?? "")")
default: print("Other: \(error)")
}
} catch {
print("Unexpected: \(error)")
}Network Support
| Network | BlockFrost | Koios | CardanoCLI | Ogmios | NodeSocket | OfflineTransfer | |---|:---:|:---:|:---:|:---:|:---:|:---:| | mainnet | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | preprod | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | preview | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | guildnet | | ✓ | | | | | | sanchonet | | ✓ | | | | |
Performance and Caching
| Context | Cached data | |---|---| | BlockFrost | Protocol params (per epoch), genesis params (permanent) | | Koios | Protocol params (per epoch), genesis params (permanent) | | CardanoCLI | Genesis params (permanent), protocol params (per tip update), UTxOs (per slot+address), datums (LRU) | | Ogmios | Epoch + protocol params (60s TTL), genesis params (permanent) | | NodeSocket | Epoch (60s TTL), protocol params (per epoch), genesis params (permanent) | | OfflineTransfer | Everything read from file — no caching needed |
Configure CardanoCLI cache sizes at initialisation:
let context = try await CardanoCliChainContext(
nodeConfig: FilePath("/opt/cardano/preview/config.json"),
binary: FilePath("/usr/local/bin/cardano-cli"),
socket: FilePath("/ipc/node.socket"),
network: .preview,
refetchChainTipInterval: 30, // seconds
utxoCacheSize: 5_000,
datumCacheSize: 1_000
)Documentation
Full DocC documentation with per-backend usage guides is available via Xcode's documentation browser (Product › Build Documentation) or at Swift Package Index.
Package Metadata
Repository: kingpin-apps/swift-cardano-chain
Default branch: main
README: README.md