kingpin-apps/swift-cardano-cips
Swift implementations of Cardano Improvement Proposals, built on
Installation
Swift Package Manager
Add the package as a dependency in Package.swift:
.package(url: "https://github.com/Kingpin-Apps/swift-cardano-cips.git", from: "0.3.3")then in your target:
.target(name: "MyApp", dependencies: [
.product(name: "SwiftCardanoCIPs", package: "swift-cardano-cips")
])Xcode
File → Add Package Dependencies… → enter https://github.com/Kingpin-Apps/swift-cardano-cips.git → add the SwiftCardanoCIPs library to your target.
Import
import SwiftCardanoCIPsPlatforms
| Platform | Minimum | | ---------- | --------- | | iOS | 16 | | macOS | 14 | | tvOS | 16 | | watchOS | 9 | | visionOS | 1 | | Linux | Swift 6.1+ |
CIP30WebBridge is only available where WebKit exists (iOS, macOS, visionOS).
CIP-8 — message signing
CIP-8 signs an arbitrary payload with a payment or stake key, producing a COSE_Sign1 envelope a wallet bridge or off-chain verifier can validate.
let signed = try CIP8.sign(
message: "hello dApp",
signingKey: .signingKey(paymentSK),
attachCoseKey: true,
network: .mainnet
)
let result = try CIP8.verify(signedMessage: signed)
assert(result.verified)
assert(result.message == "hello dApp")Notes:
- Same input produces a byte-identical signed message every time —
signing routes through libsodium's deterministic Ed25519 (RFC 8032), not CryptoKit's hedged variant. This matches cardano-signer.js output exactly, which CIP-30 dApp bridges and offline signing tools rely on.
- Stake keys must be passed as
StakeSigningKeyor
StakeExtendedSigningKey to derive a stake address. Anything else is treated as a payment key.
attachCoseKey: trueships the public COSE_Key alongside the signature
(CIP-30 signData shape). attachCoseKey: false embeds the verification key in the protected header (kid).
CIP-14 — native asset fingerprint
CIP-14 derives the asset1… fingerprint exchanges and explorers display for native tokens.
let fingerprint = CIP14.encodeAsset(
policyId: .hexString("7eae28af2208be856f7a119668ae52a49b73725e326dc16579dcc373"),
assetName: .hexString("504154415445")
)
// "asset13n9uvz077dxncpe7e7cesxldwfeexye2qrhqvk"Inputs accept multiple forms — .policyId(PolicyID), .data(Data), .hexString(String) — so you don't have to pre-convert. Returns nil only if hashing / bech32 fails.
CIP-30 — dApp connector for WKWebView
CIP-30 is the dApp-Wallet web bridge. CIP30WebBridge injects a JS shim into a WKWebView as window.cardano.<walletKey>, so dApps can call enable(), signTx, signData, submitTx, etc. without changes.
Security model
Three independent gates sit between an incoming RPC and any wallet action:
- Identifier validation —
walletKeyandmessageHandlerNamemust
match ^[A-Za-z0-9_]{1,64}$. The bridge init throws CIP30WebBridgeError.invalidIdentifier on anything else, so a misconfigured wallet can't accidentally inject JS into pages via the wallet name.
- Origin policy — every RPC is gated by a
CIP30OriginPolicy. Default
is .mainFrameOnly, which refuses any request from an iframe (ad, embed, third-party widget). Use .allowOrigins([...]) to permit specific embedded dApps; use .custom for anything else.
- Per-operation approval —
KeyStoreCIP30Providerconsults a
CIP30ApprovalPolicy before signing or submitting. The default is .denyAll. Real wallets must supply a policy whose closures pop a UI sheet, hit biometrics, etc. .allowAll exists for tests and developer harnesses only.
Enable state is tracked per origin. enable() for https://app-a.example does not authorize https://attacker.example. bridge.invalidate(origin:) and bridge.invalidateAll() clear the state when the user disconnects a dApp or signs out.
The transport itself uses WKScriptMessageHandlerWithReply (iOS 14 / macOS 11+), so the shim contains no global resolver functions on window. Predictable RPC ids and global window._cip30* callbacks — both reachable by any script in the page — were removed. Refused requests come back as a JSON error envelope ({code, info}, or {maxSize, info} for PaginateError) carried in the rejection's Error.message; the shim re-parses it so dApp catch handlers receive the structured object the spec asks for.
Minimal wiring
import SwiftCardanoCIPs
let info = WalletInfo(name: "SwiftWallet", icon: "data:image/png;base64,...")
// Real wallets gate sensitive operations behind a consent UI.
// .allowAll is for tests.
let approvals = CIP30ApprovalPolicy(
approveSignTx: { tx, _, ctx in
await MyConsentUI.confirmSignTx(tx, requestedBy: ctx?.origin)
},
approveSignData: { addr, payload, ctx in
await MyConsentUI.confirmSignData(addr, payload, requestedBy: ctx?.origin)
},
approveSubmitTx: { _, ctx in
await MyConsentUI.confirmSubmitTx(requestedBy: ctx?.origin)
}
)
let initial = KeyStoreCIP30Initial(
info: info,
consent: { extensions, ctx in
await MyConsentUI.confirmEnable(origin: ctx.origin, extensions: extensions)
},
makeProvider: { extensions, _ in
try KeyStoreCIP30Provider(
info: info,
paymentKey: paymentSK,
stakeKey: stakeSK,
network: .mainnet,
dataSource: myChainDataSource,
grantedExtensions: extensions,
policy: approvals
)
}
)
// Default originPolicy = .mainFrameOnly. Override with
// .allowOrigins(["https://app.example"]) only if you intentionally embed
// a dApp inside a parent page that should also be a wallet client.
let bridge = try CIP30WebBridge(initial: initial, walletKey: "swiftWallet")
bridge.attach(to: webView)What the dApp sees
Once attached, the dApp uses the standard CIP-30 entry point:
const api = await window.cardano.swiftWallet.enable();
const network = await api.getNetworkId();
const witnessSet = await api.signTx(txCborHex, false /* partialSign */);What the host app owns
- Implementing the consent / approval UI. The library will
refuse-by-default if you don't.
- Calling
bridge.invalidate(origin:)when the user explicitly disconnects
a dApp.
- Calling
bridge.invalidateAll()on app sign-out, if applicable. - Choosing an
originPolicythat matches your embedding model. Default
.mainFrameOnly is the safe choice for "load arbitrary dApps in a webview."
- Implementing
CIP30DataSourceto surface UTxOs and submit transactions
for KeyStoreCIP30Provider. The library doesn't ship chain access.
CIP-36 — Catalyst voting registration
CIP-36 registers a stake credential for Catalyst voting. Build the auxiliary metadata, attach it to your transaction, and the on-chain witness establishes voting power.
let aux = try CIP36.makeRegistration(
delegations: [
Delegation(votingKey: catalystVKey32, weight: 1)
],
stakeSigningKey: .signingKey(stakeSK),
rewardsAddress: rewardsAddr,
nonce: currentSlotHeight,
votingPurpose: 0
)Use CIP36.makeDeregistration(...) to revoke a prior registration.
Gotchas:
- Voting keys are exactly 32 bytes (CIP-36 vote keys, not stake keys).
noncemust strictly increase per on-chain registration for the same
stake credential — typical pattern is the current slot height.
- Field
2carries the raw 32-byte stake verification key, not its
28-byte Blake2b-224 hash.
CIP-88 — Calidus pool key registration
CIP-88 v2 / CIP-151 lets a pool operator delegate online signing authority (governance votes, hot key ops, etc.) to a separate Calidus key without exposing the cold key.
let aux = try CIP88.makeCalidusRegistration(
calidusPublicKey: calidusEd25519_32bytes,
poolSigningKey: .signingKey(coldKey),
nonce: currentSlotHeight
)Behavior matches cardano-signer.js --cip88: the signed payload is the hex-encoded CBOR, not the raw bytes.
CIP-100 — governance metadata signing
CIP-100 is the JSON-LD framework used by CIP-108 (governance actions), CIP-119 (DRep metadata), and friends. Documents are canonicalized with RDFC-1.0 and signed by one or more authors with Ed25519 witnesses embedded in the document itself.
let signed = try await CIP100.signMetadata(
document: jsonBytes,
signingKey: .signingKey(authorSK),
authorName: "Alice"
)
let result = try await CIP100.verifyMetadata(signed)
assert(result.allValid)For HSM-style workflows, derive the hash to sign externally:
let hash = try await CIP100.canonicalBodyHash(of: jsonBytes)
let signature = try myHSM.signEd25519(hash)
// …then append author entry manually.Notes:
- Async because JSON-LD canonicalization is.
- Signing strips any existing
authorsfield before canonicalizing, so
prior signatures don't pollute the hash.
- Public keys and signatures are hex strings in the JSON, not raw
bytes.
CIP-119 — DRep metadata
CIP-119 defines the DRep metadata schema (name, image, objectives, motivations, qualifications, references). It builds on CIP-100 — sign it the same way.
let dRep = DRepMetadata(
paymentAddress: nil,
givenName: "Alice",
image: nil,
objectives: "Govern with the long-term interests of stakers in mind.",
motivations: "I've worked in protocol governance for five years.",
qualifications: nil,
references: [
Reference(type: "Other", label: "Twitter", uri: "https://twitter.com/alice")
],
doNotList: false
)
guard let json = dRep.toJSON() else { … }
let signed = try await CIP100.signMetadata(
document: Data(json.utf8),
signingKey: .signingKey(dRepSK),
authorName: "Alice"
)
// blake2b-256 of canonical JSON; what you put in the on-chain anchor.
let anchorHash = try dRep.hash()CIP-129 — governance credential bech32
CIP-129 defines the human-readable identifiers for DRep, constitutional-committee, and Calidus credentials.
let drepId = CIP129.encode(
keyHash: blake2b224(vkeyBytes),
as: .drep,
isScript: false
)
// "drep1…"
let (prefix, hash, isScript) = try CIP129.decode("drep1…")Prefixes: .drep, .ccCold, .ccHot, .calidus. Key hash is always 28 bytes. calidus has no script form.
Documentation
Full API reference is in the DocC catalog. Build it locally:
swift package --disable-sandbox preview-documentation --target SwiftCardanoCIPsContributing
Tests live alongside each module:
swift testThe CI workflow runs the suite on macOS plus Linux Swift 6.1+. CIP-30 bridge tests stub WKWebView so they exercise both the JS shim contract and the per-origin gating without a UI dependency.
License
Package Metadata
Repository: kingpin-apps/swift-cardano-cips
Default branch: main
README: README.md