A Swift package providing a convenient interface for interacting with Cardano CLI tools, including cardano-cli, cardano-node, Ogmios, Kupo, and Mithril — with optional Docker and Apple Container support.
- macOS 14.0+
- Swift 6.0+
- cardano-cli 8.0.0+ (installed separately, or via Docker/Apple Container)
- cardano-node (for socket connection, or via Docker/Apple Container)
- cardano-hw-cli (optional, for hardware wallet support)
- cardano-signer (optional, for advanced signing operations)
- Docker or Apple Container (optional, for container mode)
Add SwiftCardanoUtils to your Package.swift dependencies:
dependencies: [
.package(url: "https://github.com/Kingpin-Apps/swift-cardano-utils.git", from: "0.1.0")
]Then add it to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "SwiftCardanoUtils", package: "swift-cardano-utils")
]
)Before using this package, you need to install the Cardano CLI tools:
- Download the latest release from Cardano Node releases
- Extract and place binaries in your PATH (e.g.,
/usr/local/bin/)
- Download the latest release from Cardano HW CLI releases
- Extract and place binaries in your PATH (e.g.,
/usr/local/bin/)
- Download the latest release from Cardano Signer releases
- Extract and place binaries in your PATH (e.g.,
/usr/local/bin/)
import SwiftCardanoUtils
import SwiftCardanoCore
import System
// Create configuration
let cardanoConfig = CardanoConfig(
cli: FilePath("/usr/local/bin/cardano-cli"),
node: FilePath("/usr/local/bin/cardano-node"),
hwCli: FilePath("/usr/local/bin/cardano-hw-cli"), // Optional
signer: FilePath("/usr/local/bin/cardano-signer"), // Optional
socket: FilePath("/tmp/cardano-node.socket"),
config: FilePath("/path/to/config.json"),
topology: FilePath("/path/to/topology.json"), // Optional
database: FilePath("/path/to/database"), // Optional
port: 3001, // Optional
hostAddr: "127.0.0.1", // Optional
network: .preview, // .mainnet, .preprod, .guildnet, .sanchonet, .custom(Int)
era: .conway,
ttlBuffer: 3600,
workingDir: FilePath("/tmp"),
showOutput: true // Optional
)
let configuration = Config(
cardano: cardanoConfig,
ogmios: nil,
kupo: nil
)
// Initialize CLI
let cli = try await CardanoCLI(configuration: configuration)// Get cardano-cli version
let version = try await cli.version()
print("Cardano CLI version: \(version)")
// Check node sync progress
let syncProgress = try await cli.getSyncProgress()
print("Sync progress: \(syncProgress)%")
// Get current era
if let era = try await cli.getEra() {
print("Current era: \(era)")
}
// Get current epoch
let epoch = try await cli.getEpoch()
print("Current epoch: \(epoch)")// Get current tip (latest slot)
let tip = try await cli.getTip()
print("Current tip: \(tip)")
// Get chain tip details
let chainTip = try await cli.query.tip()
print("Block: \(chainTip.block)")
print("Epoch: \(chainTip.epoch)")
print("Era: \(chainTip.era)")
print("Hash: \(chainTip.hash)")
print("Slot: \(chainTip.slot)")
print("Sync Progress: \(chainTip.syncProgress)%")
// Get protocol parameters
let protocolParams = try await cli.getProtocolParameters()
print("Min fee A: \(protocolParams.txFeePerByte)")
print("Min fee B: \(protocolParams.txFeeFixed)")// Build an address
let address = try await cli.address.build(arguments: [
"--payment-verification-key-file", "payment.vkey",
"--stake-verification-key-file", "stake.vkey",
"--testnet-magic", "2"
])
print("Generated address: \(address)")
// Get address info
let addressInfo = try await cli.query.addressInfo(
address: "addr_test1...",
arguments: ["--testnet-magic", "2"]
)// Generate payment key pair
try await cli.key.generate(arguments: [
"--verification-key-file", "payment.vkey",
"--signing-key-file", "payment.skey"
])
// Generate stake key pair
try await cli.key.generate(arguments: [
"--verification-key-file", "stake.vkey",
"--signing-key-file", "stake.skey"
])// Assuming you have cardano-hw-cli installed and configured
let hwCli = try await CardanoHWCLI(configuration: configuration)
// Get hardware wallet address
let hwAddress = try await hwCli.address.show(arguments: [
"--payment-path", "1852'/1815'/0'/0/0",
"--stake-path", "1852'/1815'/0'/2/0",
"--testnet-magic", "2"
])You can also load configuration from a JSON file:
{
"cardano": {
"cli": "/usr/local/bin/cardano-cli",
"node": "/usr/local/bin/cardano-node",
"hw_cli": "/usr/local/bin/cardano-hw-cli",
"signer": "/usr/local/bin/cardano-signer",
"socket": "/tmp/cardano-node.socket",
"config": "/path/to/config.json",
"topology": "/path/to/topology.json",
"database": "/path/to/database",
"port": 3001,
"host_addr": "127.0.0.1",
"network": "preview",
"era": "conway",
"ttl_buffer": 3600,
"working_dir": "/tmp",
"show_output": true
},
"ogmios": {
"binary": "/usr/local/bin/ogmios",
"host": "0.0.0.0",
"port": 1337,
"timeout": 30,
"max_in_flight": 100,
"log_level": "info",
"working_dir": "/tmp",
"show_output": true
},
"kupo": {
"binary": "/usr/local/bin/kupo",
"host": "0.0.0.0",
"port": 1442,
"since": "origin",
"matches": ["*"],
"defer_db_indexes": false,
"prune_utxo": false,
"gc_interval": 300,
"max_concurrency": 10,
"log_level": "info",
"working_dir": "/tmp",
"show_output": true
}
}// Load from JSON file
let configuration = try await SwiftCardanoUtilsConfig.load(path: FilePath("/path/to/config.json"))The package supports different Cardano networks:
// Available networks
.mainnet // Cardano mainnet
.preview // Preview testnet (magic: 2)
.preprod // Pre-production testnet (magic: 1)
.guildnet // Guild testnet (magic: 141)
.sanchonet // SanchoNet testnet (magic: 4)
.custom(Int) // Custom network with magic numberEach network provides convenient properties:
let network = Network.preview
print(network.testnetMagic) // Optional(2)
print(network.arguments) // ["--testnet-magic", "2"]
print(network.description) // "preview"The package supports configuration via environment variables:
# Core Cardano settings
export CARDANO_SOCKET_PATH="/tmp/cardano-node.socket"
export CARDANO_CONFIG="/path/to/config.json"
export CARDANO_TOPOLOGY="/path/to/topology.json"
export CARDANO_DATABASE_PATH="/path/to/database"
export CARDANO_LOG_DIR="/path/to/logs"
export CARDANO_PORT="3001"
export CARDANO_BIND_ADDR="127.0.0.1"
export NETWORK="preview"
# Optional settings
export DEBUG="true"
export CARDANO_BLOCK_PRODUCER="false"// Access environment variables programmatically
import SwiftCardanoUtils
// Set environment variables at runtime
Environment.set(.network, value: "preview")
Environment.set(.cardanoSocketPath, value: "/tmp/node.socket")
// Get environment variables
if let network = Environment.get(.network) {
print("Network: \(network)")
}
// Get file paths from environment
if let socketPath = Environment.getFilePath(.cardanoSocketPath) {
print("Socket: \(socketPath)")
}Generate default configuration from environment variables:
// Create default CardanoConfig from environment
let defaultCardanoConfig = try CardanoConfig.default()
let configuration = Configuration(
cardano: defaultCardanoConfig,
ogmios: try? OgmiosConfig.default(),
kupo: try? KupoConfig.default()
)// Available eras
.byron
.shelley
.allegra
.mary
.alonzo
.babbage
.conway// Get UTxOs for an address
let utxos = try await cli.query.utxos(
address: "addr_test1...",
arguments: ["--testnet-magic", "2"]
)
// Build transaction
let txBody = try await cli.transaction.build(arguments: [
"--tx-in", "txhash#0",
"--tx-out", "addr_test1...+1000000",
"--change-address", "addr_test1...",
"--testnet-magic", "2",
"--out-file", "tx.raw"
])// Query stake pools
let stakePools = try await cli.query.stakePools(
arguments: ["--testnet-magic", "2"]
)
// Get stake pool information
let poolInfo = try await cli.query.poolParams(
poolId: "pool1...",
arguments: ["--testnet-magic", "2"]
)// Get current protocol parameters
let params = try await cli.getProtocolParameters()
// Save to file
let paramsFile = FilePath("/tmp/protocol.json")
let _ = try await cli.getProtocolParameters(paramsFile: paramsFile)// Configure Ogmios for WebSocket queries
let ogmiosConfig = OgmiosConfig(
binary: FilePath("/usr/local/bin/ogmios"),
host: "127.0.0.1",
port: 1337,
timeout: 30,
maxInFlight: 100,
logLevel: "info",
workingDir: FilePath("/tmp"),
showOutput: true
)
// Configure Kupo for UTxO indexing
let kupoConfig = KupoConfig(
binary: FilePath("/usr/local/bin/kupo"),
host: "127.0.0.1",
port: 1442,
since: "origin",
matches: ["addr_test*", "stake_test*"],
deferDbIndexes: false,
pruneUTxO: false,
gcInterval: 300,
maxConcurrency: 10,
logLevel: "info",
workingDir: FilePath("/tmp"),
showOutput: true
)
// Initialize services
let ogmios = try await Ogmios(configuration: Configuration(cardano: cardanoConfig, ogmios: ogmiosConfig, kupo: nil))
let kupo = try await Kupo(configuration: Configuration(cardano: cardanoConfig, ogmios: nil, kupo: kupoConfig))
// Start services
try await ogmios.start()
try await kupo.start()
// Check if services are running
print("Ogmios running: \(ogmios.isRunning)")
print("Kupo running: \(kupo.isRunning)")All CLI tools support running inside Docker or Apple Container — no local binary installations required. Attach a ContainerConfig to any service configuration to route commands through the container runtime.
| Mode | Tools | Docker Command |
|---|---|---|
| Run (daemon) | CardanoNode, Ogmios, Kupo | docker run [flags] <image> |
| Exec (one-shot) | CardanoCLI, CardanoHWCLI, CardanoSigner, MithrilClient | docker exec <name> <binary> |
Run-mode tools launch a fresh container on each start() call (defaults to --detach). Exec-mode tools issue commands against a running named container (containerName is required).
// Run CardanoNode inside Docker
let container = ContainerConfig(
runtime: .docker,
imageName: "ghcr.io/intersectmbo/cardano-node:10.0.0",
containerName: "cardano-node",
volumes: ["/data/cardano-node:/data", "/ipc:/ipc"],
environment: ["NETWORK=preview"],
network: "host",
restart: "unless-stopped",
detach: true
)
let cardanoConfig = CardanoConfig(
socket: FilePath("/ipc/node.socket"),
config: FilePath("/data/config/config.json"),
network: .preview,
era: .conway,
ttlBuffer: 3600,
container: container // <-- attach container config
)
let node = try await CardanoNode(configuration: Config(cardano: cardanoConfig))
try await node.start() // → docker run --detach --name cardano-node ...The same pattern works for Ogmios and Kupo:
let ogmiosConfig = OgmiosConfig(
host: "0.0.0.0",
port: 1337,
container: ContainerConfig(
runtime: .docker,
imageName: "cardanosolutions/ogmios:v6.13",
containerName: "ogmios",
volumes: ["/ipc:/ipc"],
ports: ["1337:1337"],
detach: true
)
)
let kupoConfig = KupoConfig(
host: "0.0.0.0",
port: 1442,
container: ContainerConfig(
runtime: .docker,
imageName: "cardanosolutions/kupo:v2.10",
containerName: "kupo",
volumes: ["/data:/db", "/ipc:/ipc"],
ports: ["1442:1442"],
detach: true
)
)Exec-mode tools assume the container is already running. Start it with CardanoNode (run-mode) first, then issue commands into it:
// Assumes "cardano-node" container is running (started above)
let container = ContainerConfig(
runtime: .docker,
imageName: "ghcr.io/intersectmbo/cardano-node:10.0.0",
containerName: "cardano-node" // Must match the running container name
)
let cardanoConfig = CardanoConfig(
socket: FilePath("/ipc/node.socket"),
config: FilePath("/data/config/config.json"),
network: .preview,
era: .conway,
ttlBuffer: 3600,
container: container
)
let cli = try await CardanoCLI(configuration: Config(cardano: cardanoConfig))
let tip = try await cli.getTip() // → docker exec cardano-node cardano-cli query tip ...Replace .docker with .appleContainer to use the container CLI. Start the daemon first if it isn't already running:
container system start # start the Apple Container virtualization service
container system status # verify it is runninglet container = ContainerConfig(
runtime: .appleContainer, // Uses 'container' instead of 'docker'
imageName: "ghcr.io/intersectmbo/cardano-node:10.0.0",
containerName: "cardano-node",
detach: true
)Container config is embedded inside any service block using snake_case keys:
{
"cardano": {
"socket": "/ipc/node.socket",
"config": "/data/config/config.json",
"network": "preview",
"era": "conway",
"ttl_buffer": 3600,
"container": {
"runtime": "docker",
"image_name": "ghcr.io/intersectmbo/cardano-node:10.0.0",
"container_name": "cardano-node",
"volumes": ["/data:/data", "/ipc:/ipc"],
"network": "host",
"restart": "unless-stopped",
"detach": true
}
},
"ogmios": {
"host": "0.0.0.0",
"port": 1337,
"container": {
"runtime": "docker",
"image_name": "cardanosolutions/ogmios:v6.13",
"container_name": "ogmios",
"ports": ["1337:1337"],
"detach": true
}
}
}On initialisation, container-mode tools verify the image exists locally before starting:
# Docker
docker pull ghcr.io/intersectmbo/cardano-node:10.0.0
docker pull cardanosolutions/ogmios:v6.13
docker pull cardanosolutions/kupo:v2.10
docker pull ghcr.io/input-output-hk/mithril-client:latest
# Apple Container (start the service first if needed)
container system start
container pull ghcr.io/intersectmbo/cardano-node:10.0.0
container pull cardanosolutions/ogmios:v6.13
container pull cardanosolutions/kupo:v2.10The package defines comprehensive error types:
do {
let tip = try await cli.getTip()
print("Current tip: \(tip)")
} catch SwiftCardanoUtilsError.nodeNotSynced(let progress) {
print("Node not fully synced: \(progress)%")
} catch SwiftCardanoUtilsError.commandFailed(let command, let message) {
print("Command failed: \(command)")
print("Error: \(message)")
} catch SwiftCardanoUtilsError.binaryNotFound(let path) {
print("Binary not found at: \(path)")
} catch {
print("Unexpected error: \(error)")
}Comprehensive error handling with specific error types:
public enum SwiftCardanoUtilsError: Error, Equatable {
case binaryNotFound(String) // CLI binary not found at path
case commandFailed([String], String) // CLI command execution failed
case nodeNotSynced(Double) // Node sync progress < 100%
case invalidOutput(String) // Command output parsing failed
case unsupportedVersion(String, String) // CLI version incompatible
case configurationMissing(String) // Required config missing
case fileNotFound(String) // Required file not found
case processAlreadyRunning(String) // Process already running
case deviceError(String) // Hardware wallet device error
case invalidMultiSigConfig(String) // Multi-signature config invalid
case versionMismatch(String, String) // Version compatibility issue
}do {
let tip = try await cli.getTip()
print("Current tip: \(tip)")
} catch SwiftCardanoUtilsError.nodeNotSynced(let progress) {
print("Node synchronizing: \(String(format: "%.1f", progress))%")
// Wait and retry logic
} catch SwiftCardanoUtilsError.commandFailed(let command, let message) {
print("Command failed: \(command.joined(separator: " "))")
print("Error details: \(message)")
// Command-specific error handling
} catch SwiftCardanoUtilsError.binaryNotFound(let path) {
print("Install cardano-cli at: \(path)")
// Installation guidance
} catch SwiftCardanoUtilsError.unsupportedVersion(let current, let required) {
print("Version \(current) found, \(required) required")
// Version upgrade guidance
} catch {
print("Unexpected error: \(error.localizedDescription)")
}The package uses a clean protocol-based architecture:
// Core protocols for binary management
protocol BinaryExecutable {
static var binaryName: String { get }
static var minimumVersion: String { get }
static func getBinaryPath() throws -> FilePath
static func checkBinary(at path: FilePath) throws
static func checkVersion(_ version: String) throws
}
protocol BinaryInterfaceable: BinaryExecutable {
var configuration: Configuration { get }
var logger: Logger { get }
func runCommand(_ arguments: [String]) async throws -> String
}
protocol BinaryRunnable: BinaryInterfaceable {
var isRunning: Bool { get }
func start() async throws
func stop() async throws
}// Commands are strongly typed and validated
let addressCommand = cli.address
let keyCommand = cli.key
let transactionCommand = cli.transaction
let queryCommand = cli.query
// Each command provides specific methods
try await addressCommand.build(arguments: [...])
try await keyCommand.generate(arguments: [...])
try await transactionCommand.sign(arguments: [...])
try await queryCommand.tip()// Advanced process lifecycle management
let node = try await CardanoNode(configuration: configuration)
// Start node with automatic socket management
try await node.start()
print("Node PID: \(node.process?.processIdentifier ?? -1)")
print("Socket path: \(node.configuration.cardano.socket)")
// Graceful shutdown
try await node.stop()// Hardware wallet support with device detection
let hwCli = try await CardanoHWCLI(configuration: configuration)
// Device-specific operations
try await hwCli.device.version()
try await hwCli.address.show(arguments: [
"--payment-path", "1852'/1815'/0'/0/0",
"--address-format", "bech32"
])Run the test suite:
# Run all tests
swift test
# Run tests with coverage
swift test --enable-code-coverage
# Run specific test suites
swift test --filter "EnvironmentTests"
swift test --filter "ConfigurationTests"- swift-log
1.6.2+- Structured logging framework - swift-cardano-core
0.1.33+- Cardano protocol types and utilities
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for new functionality
- Ensure all tests pass (
swift test) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
For issues and questions:
- Open an issue on GitHub
- Check the Cardano Developer Portal
- Input Output Global for Cardano
- Cardano Foundation
- The Cardano community for tools and documentation