Skip to content

Kingpin-Apps/swift-cardano-utils

Repository files navigation

SwiftCardanoUtils

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.

Swift Platform Tests

Requirements

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

Installation

Swift Package Manager

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")
    ]
)

Installing Swift Cardano Utils

Before using this package, you need to install the Cardano CLI tools:

Manual Installation

  1. Download the latest release from Cardano Node releases
  2. Extract and place binaries in your PATH (e.g., /usr/local/bin/)

Hardware Wallet Support (Optional)

  1. Download the latest release from Cardano HW CLI releases
  2. Extract and place binaries in your PATH (e.g., /usr/local/bin/)

Cardano Signer Support (Optional)

  1. Download the latest release from Cardano Signer releases
  2. Extract and place binaries in your PATH (e.g., /usr/local/bin/)

Quick Start

Basic Setup

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)

Basic Operations

Check Node Status

// 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)")

Query Chain Information

// 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)")

Address Operations

// 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"]
)

Key Management

// 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"
])

Hardware Wallet Support

// 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"
])

Configuration

JSON Configuration File

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

Network Types

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 number

Network Configuration

Each network provides convenient properties:

let network = Network.preview
print(network.testnetMagic)  // Optional(2)
print(network.arguments)     // ["--testnet-magic", "2"]
print(network.description)   // "preview"

Environment Variables

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

Default Configuration

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

Era Types

// Available eras
.byron
.shelley
.allegra
.mary
.alonzo
.babbage
.conway

Advanced Usage

Transaction Building

// 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"
])

Stake Pool Operations

// 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"]
)

Protocol Parameters

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

Ogmios and Kupo Integration

// 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)")

Container Support

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.

Execution Modes

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-Mode Example (Daemons)

// 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 Example (CLI Tools)

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

Apple Container Runtime

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 running
let container = ContainerConfig(
    runtime: .appleContainer,      // Uses 'container' instead of 'docker'
    imageName: "ghcr.io/intersectmbo/cardano-node:10.0.0",
    containerName: "cardano-node",
    detach: true
)

JSON Configuration

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

Pre-Flight Image Check

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.10

Error Handling

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

Error Types

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
}

Error Context and Recovery

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

Architecture & Protocols

Protocol-Based Design

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
}

Type-Safe Command Building

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

Process Management

// 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 Integration

// 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"
])

Testing

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"

Dependencies

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass (swift test)
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

For issues and questions:

Acknowledgments

About

Use cardano-cli and other binaries

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages