SwiftCardanoChain is a Swift implementation of Cardano Data Types with CBOR (and JSON) serialization.
To add SwiftCardanoChain as dependency to your Xcode project, select File > Swift Packages > Add Package Dependency, enter its repository URL: https://github.com/Kingpin-Apps/swift-cardano-chain.git and import SwiftCardanoChain.
Then, to use it in your source code, add:
import SwiftCardanoChain
let blockfrostChainContext = try await BlockFrostChainContext(
network: .preview,
environmentVariable: "BLOCKFROST_API_KEY"
)
let cardanoCliChainContext = try CardanoCliChainContext(
configFile: URL(fileURLWithPath: "/path/to/preview/config.json"),
network: .preview
)SwiftCardanoChain provides two powerful chain context implementations that allow you to read from and write to the Cardano blockchain:
- BlockFrostChainContext: Uses the BlockFrost API for cloud-based blockchain interactions
- CardanoCliChainContext: Uses the Cardano CLI for local node interactions
Both implementations conform to the ChainContext protocol and provide the same interface for:
- Reading blockchain data (UTxOs, protocol parameters, genesis parameters)
- Writing transactions to the blockchain
- Evaluating transaction execution units
- Querying stake address information
The BlockFrost chain context is ideal for applications that need to interact with the Cardano blockchain without running a local node.
import SwiftCardanoChain
// Initialize with environment variable
let chainContext = try await BlockFrostChainContext<Never>(
network: .preview,
environmentVariable: "BLOCKFROST_API_KEY"
)
// Or initialize with project ID directly
let chainContext = try await BlockFrostChainContext<Never>(
projectId: "your-project-id",
network: .mainnet
)The Cardano CLI chain context is perfect for applications that have access to a local Cardano node.
import SwiftCardanoChain
let chainContext = try CardanoCliChainContext<Never>(
configFile: URL(fileURLWithPath: "/path/to/config.json"),
network: .preview
)Retrieve all UTxOs for a specific address:
let address = try Address(
from: .string("addr_test1qp4kux2v7xcg9urqssdffff5p0axz9e3hcc43zz7pcuyle0e20hkwsu2ndpd9dh9anm4jn76ljdz0evj22stzrw9egxqmza5y3")
)
let utxos = try await chainContext.utxos(address: address)
for utxo in utxos {
print("Transaction ID: \(utxo.input.transactionId.payload.toHex)")
print("Output Index: \(utxo.input.index)")
print("Address: \(try utxo.output.address.toBech32())")
print("Amount: \(utxo.output.amount.coin) lovelace")
// Handle multi-assets if present
for (policyId, assets) in utxo.output.amount.multiAsset {
for (assetName, amount) in assets {
print("Asset: \(policyId.payload.toHex).\(assetName.name.toHex) = \(amount)")
}
}
}Retrieve current protocol parameters:
let protocolParams = try await chainContext.protocolParameters()
print("Min fee per byte: \(protocolParams.txFeePerByte)")
print("Fixed fee: \(protocolParams.txFeeFixed)")
print("Max transaction size: \(protocolParams.maxTxSize)")
print("UTxO cost per byte: \(protocolParams.utxoCostPerByte)")Retrieve genesis parameters for the network:
let genesisParams = try await chainContext.genesisParameters()
print("Network ID: \(genesisParams.networkId)")
print("Network Magic: \(genesisParams.networkMagic)")
print("Slot length: \(genesisParams.slotLength) seconds")
print("Epoch length: \(genesisParams.epochLength) slots")
print("Security parameter: \(genesisParams.securityParam)")// Get current epoch
let currentEpoch = try await chainContext.epoch()
print("Current epoch: \(currentEpoch)")
// Get last block slot
let lastSlot = try await chainContext.lastBlockSlot()
print("Last block slot: \(lastSlot)")
// Get network type
let network = chainContext.network
print("Network: \(network)")The chain contexts provide multiple ways to submit transactions:
// Assuming you have a built transaction
let transaction: Transaction<Never> = // ... your transaction
let txId = try await chainContext.submitTx(tx: .transaction(transaction))
print("Transaction submitted with ID: \(txId)")let cborData = transaction.toCBORData()
let txId = try await chainContext.submitTx(tx: .bytes(cborData))
print("Transaction submitted with ID: \(txId)")let cborHex = "84a70081825820b35a4ba9ef3ce21adcd6879d..."
let txId = try await chainContext.submitTx(tx: .string(cborHex))
print("Transaction submitted with ID: \(txId)")Before submitting a transaction with Plutus scripts, you may need to evaluate execution units:
// Evaluate using transaction object
let executionUnits = try await chainContext.evaluateTx(tx: transaction)
for (redeemerIndex, units) in executionUnits {
print("Redeemer \(redeemerIndex): \(units.mem) memory, \(units.steps) steps")
}
// Or evaluate using CBOR data
let executionUnits = try await chainContext.evaluateTxCBOR(cbor: cborData)let stakeAddress = try Address(
from: .string("stake_test1upyz3gk6mw5he20apnwfn96cn9rscgvmmsxc9r86dh0k66gswf59n")
)
let stakeInfo = try await chainContext.stakeAddressInfo(address: stakeAddress)
for info in stakeInfo {
print("Address: \(info.address)")
print("Balance: \(info.rewardAccountBalance) lovelace")
print("Delegated to pool: \(info.stakeDelegation ?? "None")")
print("Vote delegation: \(info.voteDelegation ?? "None")")
print("DRep: \(info.delegateRepresentative ?? "None")")
}Both chain contexts use the CardanoChainError enum for error handling:
do {
let utxos = try await chainContext.utxos(address: address)
// Process UTxOs
} catch let error as CardanoChainError {
switch error {
case .blockfrostError(let message):
print("BlockFrost API error: \(message)")
case .transactionFailed(let message):
print("Transaction failed: \(message)")
case .invalidArgument(let message):
print("Invalid argument: \(message)")
case .valueError(let message):
print("Value error: \(message)")
case .unsupportedNetwork(let message):
print("Unsupported network: \(message)")
}
} catch {
print("Unexpected error: \(error)")
}Both chain contexts support multiple Cardano networks:
// Mainnet
let mainnetContext = try await BlockFrostChainContext<Never>(
projectId: "mainnet-project-id",
network: .mainnet
)
// Preprod testnet
let preprodContext = try await BlockFrostChainContext<Never>(
projectId: "preprod-project-id",
network: .preprod
)
// Preview testnet
let previewContext = try await BlockFrostChainContext<Never>(
projectId: "preview-project-id",
network: .preview
)The ChainContext protocol uses a generic associatedtype called ReedemerType that must conform to CBORSerializable & Hashable. This type parameter determines how Plutus script redeemers are represented in your transactions.
In Cardano's extended UTxO (eUTxO) model, when you spend UTxOs that are locked by Plutus scripts, you must provide:
- Datum: Data associated with the UTxO (the "lock")
- Redeemer: Data you provide to "unlock" the UTxO (the "key")
- Script Context: Information about the transaction (provided automatically)
The ReedemerType generic parameter specifies the Swift type used to represent redeemer data in your transactions.
Use Never as your ReedemerType when your application:
- Only performs simple transactions without Plutus scripts
- Only reads blockchain data (UTxOs, protocol parameters, etc.)
- Submits pre-built transactions as CBOR data or hex strings
- Doesn't need to construct transactions with custom redeemer types
// For read-only operations or simple transactions
let chainContext = try await BlockFrostChainContext<Never>(
projectId: "your-project-id",
network: .mainnet
)
// Reading data works perfectly with Never
let utxos = try await chainContext.utxos(address: address)
let protocolParams = try await chainContext.protocolParameters()
// Submitting pre-built transactions works too
let txId = try await chainContext.submitTx(tx: .string(cborHex))Define a custom ReedemerType when your application:
- Constructs transactions that interact with specific Plutus scripts
- Has domain-specific redeemer data structures
- Needs type safety for redeemer construction
// Define your custom redeemer type
struct MyRedeemer: CBORSerializable, Hashable {
let action: String
let amount: Int
let recipient: String
func toCBOR() -> CBOR {
// Implementation to serialize to CBOR
return .array([
.textString(action),
.unsignedInteger(UInt64(amount)),
.textString(recipient)
])
}
static func fromCBOR(_ cbor: CBOR) throws -> MyRedeemer {
// Implementation to deserialize from CBOR
guard case let .array(items) = cbor,
items.count == 3,
case let .textString(action) = items[0],
case let .unsignedInteger(amount) = items[1],
case let .textString(recipient) = items[2] else {
throw CBORError.invalidFormat
}
return MyRedeemer(action: action, amount: Int(amount), recipient: recipient)
}
}
// Use your custom redeemer type
let chainContext = try await BlockFrostChainContext<MyRedeemer>(
projectId: "your-project-id",
network: .mainnet
)
// Now you can work with strongly-typed transactions
let transaction = Transaction<MyRedeemer>(
body: transactionBody,
witnessSet: witnessSet
)
let txId = try await chainContext.submitTx(tx: .transaction(transaction))struct UnitRedeemer: CBORSerializable, Hashable {
func toCBOR() -> CBOR {
return .null // Plutus Unit type
}
static func fromCBOR(_ cbor: CBOR) throws -> UnitRedeemer {
return UnitRedeemer()
}
}enum ScriptAction: CBORSerializable, Hashable {
case mint(amount: Int)
case burn(amount: Int)
case transfer(to: String)
func toCBOR() -> CBOR {
switch self {
case .mint(let amount):
return .array([.unsignedInteger(0), .unsignedInteger(UInt64(amount))])
case .burn(let amount):
return .array([.unsignedInteger(1), .unsignedInteger(UInt64(amount))])
case .transfer(let to):
return .array([.unsignedInteger(2), .textString(to)])
}
}
static func fromCBOR(_ cbor: CBOR) throws -> ScriptAction {
guard case let .array(items) = cbor,
items.count >= 2,
case let .unsignedInteger(tag) = items[0] else {
throw CBORError.invalidFormat
}
switch tag {
case 0:
guard case let .unsignedInteger(amount) = items[1] else {
throw CBORError.invalidFormat
}
return .mint(amount: Int(amount))
case 1:
guard case let .unsignedInteger(amount) = items[1] else {
throw CBORError.invalidFormat
}
return .burn(amount: Int(amount))
case 2:
guard case let .textString(to) = items[1] else {
throw CBORError.invalidFormat
}
return .transfer(to: to)
default:
throw CBORError.invalidFormat
}
}
}- Start with
Neverif you're unsure - it works for most use cases - Use
Neverfor prototyping and testing blockchain interactions - Define custom types only when you need to construct transactions with specific script interactions
- Keep redeemer types simple and focused on the data your scripts need
- Test CBOR serialization thoroughly - incorrect serialization will cause transaction failures
Using specific redeemer types provides:
- Compile-time safety: Catch redeemer structure errors at build time
- Clear documentation: Types serve as documentation for script interfaces
- IDE support: Better autocomplete and refactoring capabilities
- Maintainability: Easier to update when script interfaces change
Both implementations include intelligent caching:
- Protocol parameters are cached per epoch
- Genesis parameters are cached permanently
- UTxOs are cached by slot and address (CardanoCLI only)
- Chain tip data has configurable refresh intervals
// Configure CardanoCLI context with custom cache sizes
let cliContext = try CardanoCliChainContext<Never>(
configFile: configURL,
network: .preview,
refetchChainTipInterval: 30.0, // Refresh every 30 seconds
utxoCacheSize: 5000, // Cache up to 5000 UTxO sets
datumCacheSize: 1000 // Cache up to 1000 datums
)