This guide covers the checked-in Swift SDK surface exposed by the ClearSigning package.
The primary integration API is the handwritten bindings/swift/ClearSigningClient.swift, which wraps descriptor resolution and formatting around the generated UniFFI layer.
Import the package product:
import ClearSigningApp-facing types:
ClearSigningClientDataProviderFfiFormatOutcomeFormatFailureDescriptorResolutionOutcomeFormatDiagnosticFallbackReasonDisplayModelTokenMetaFfi
Tagged releases are consumed as a Swift package from the repository URL:
dependencies: [
.package(url: "https://github.com/llbartekll/clear-signing", from: "0.1.0")
]Then add the ClearSigning product to your target dependencies.
Current repo caveat:
- The checked-in Package.swift on
maindefaults to the local XCFramework path. - Tagged releases use a CI-rewritten manifest that points at the published GitHub Release XCFramework zip and checksum.
- The checked-in
Package.swiftcurrently declares.iOS(.v14). - The Swift release workflow updates the manifest only on the tagged release commit;
mainstays in local-dev mode.
Build the local XCFramework first:
./scripts/build-xcframework.shThat script:
- Builds
target/ios/libclear_signing.xcframework - Regenerates
bindings/swift/clear_signing.swift
The local package target then resolves against the XCFramework at:
target/ios/libclear_signing.xcframework
On main, SwiftPM commands use that local XCFramework directly:
swift package describeAny SwiftPM command works the same way after the XCFramework is built, for example swift build, swift test, or swift package describe.
Typical app flow:
- Implement
DataProviderFfi. - Create
ClearSigningClient(dataProvider:). - Call
formatCalldata(...)orformatTypedData(...). - Switch on
FormatOutcomeand render either clear-signed or degraded UI.
The client performs descriptor resolution before formatting:
formatCalldata(...)resolves transaction descriptors, including nested calldata descriptors when needed.formatTypedData(...)resolves typed-data descriptors, including nested calldata descriptors when needed.- Proxy detection is delegated to your
DataProviderFfi.getImplementationAddress(...). - Missing token/name/NFT metadata stays best-effort and surfaces as diagnostics, not hard failures.
Wallet policy should branch on FormatDiagnostic.code, not parse FormatDiagnostic.message.
DataProviderFfi is the wallet-owned callback surface. The SDK calls it synchronously across the FFI boundary whenever it needs metadata that only the host app can provide.
Skeleton only:
This sketch is intentionally illustrative and omits concrete return statements.
import ClearSigning
final class WalletMetadataProvider: DataProviderFfi, @unchecked Sendable {
func resolveToken(chainId: UInt64, address: String) -> TokenMetaFfi? {
// Return token symbol, decimals, and name for this contract address.
// Use wallet caches or RPC-backed metadata if you have it.
}
func resolveEnsName(address: String, chainId: UInt64, types: [String]?) -> String? {
// Return an ENS or other remote name for this address when available.
// Return nil when the wallet cannot resolve a name.
}
func resolveLocalName(address: String, chainId: UInt64, types: [String]?) -> String? {
// Return a wallet-local contact or account label for this address.
// Return nil when no local label exists.
}
func resolveNftCollectionName(collectionAddress: String, chainId: UInt64) -> String? {
// Return the NFT collection name for this contract address.
// Return nil when unknown.
}
func resolveBlockTimestamp(chainId: UInt64, blockNumber: UInt64) -> UInt64? {
// Return the block timestamp for date-format rendering that depends on block numbers.
// Return nil when the wallet cannot look it up.
}
func getImplementationAddress(chainId: UInt64, address: String) -> String? {
// Return the proxy implementation address when this contract is a supported proxy.
// Return nil for non-proxies or when proxy detection is unavailable.
}
}Callback contract:
resolveToken: used for token amount formatting and symbol/decimals/name display.resolveEnsName: used for remote address naming, such as ENS-style labels.resolveLocalName: used for wallet-local labels, address book entries, or “My Wallet”-style naming.resolveNftCollectionName: used when the descriptor wants a collection label for an NFT contract.resolveBlockTimestamp: used when descriptor rendering needs a block number converted to time.getImplementationAddress: used for proxy-aware descriptor resolution whentx.todoes not directly match a descriptor.
let provider = WalletMetadataProvider()
let client = ClearSigningClient(dataProvider: provider)let outcome = try await client.formatCalldata(
chainId: 1,
to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
calldataHex: "0xa9059cbb000000000000000000000000...",
valueHex: nil,
fromAddress: "0x1234..."
)
switch outcome {
case .clearSigned(let model, let diagnostics):
renderTrusted(model: model, diagnostics: diagnostics)
case .fallback(let model, let reason, let diagnostics):
renderGeneric(model: model, reason: reason, diagnostics: diagnostics)
}Method behavior:
- Builds a
TransactionInput - Resolves descriptors for the transaction
- Formats the transaction into
FormatOutcome
Parameters:
chainId: target EVM chain IDto: destination contract addresscalldataHex: calldata as0x-prefixed hexvalueHex: optional0x-prefixed native token valuefromAddress: optional sender address for sender-aware rendering
let outcome = try await client.formatTypedData(
typedDataJson: typedDataJson
)Method behavior:
- Resolves descriptors for the typed data payload
- Formats the typed data into
FormatOutcome
let resolution = try await client.resolveDescriptorsForTx(
chainId: 1,
to: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
calldataHex: "0xa9059cbb000000000000000000000000...",
valueHex: nil,
fromAddress: nil
)
switch resolution {
case .found(let descriptors):
print("resolved \(descriptors.count) descriptors")
case .notFound:
print("no descriptors resolved")
}Use this when your app wants visibility into the resolved descriptor set before formatting.
let resolution = try await client.resolveDescriptorsForTypedData(
typedDataJson: typedDataJson
)Use this when your app wants descriptor diagnostics or staged formatting flows for typed data.
FormatOutcome is the primary SDK result.
Cases:
clearSigned(model:diagnostics:)fallback(model:reason:diagnostics:)
Recommended wallet policy:
clearSigned: show trusted clear-signing UIfallback: show generic / degraded UI and keep the reason visible- thrown
FormatFailure: fail closed
Concrete fallback cases:
- descriptor not found for the contract
- known contract but no selector / encodeType format matched
- typed data missing
domain.chainIdordomain.verifyingContract - nested calldata could not be clear-signed
DisplayModel is the render payload inside FormatOutcome.
Important fields:
intent: descriptor-defined intent labelinterpolatedIntent: optional resolved intent string with interpolated valuesentries: structured display entries for list/group/nested renderingowner: descriptor owner metadata when present
FormatDiagnostic replaces legacy warning strings.
Fields:
codeseverity(infoorwarning)message
Contract:
codeis machine-readable and intended for wallet policy and telemetrymessageis human-readable and may evolve independently
Example:
if diagnostics.contains(where: { $0.code == "nested_descriptor_not_found" }) {
showGenericNestedCallBadge()
}Descriptor resolution no longer uses empty arrays to signal misses.
Cases:
found([String])notFound
TokenMetaFfi is the token metadata record returned from resolveToken(...).
Fields:
symboldecimalsname
Swift client methods throw FormatFailure.
Cases:
InvalidInput(detail:retryable:)InvalidDescriptor(detail:retryable:)ResolutionFailed(detail:retryable:)Internal(detail:retryable:)
ClearSigningClient extends FormatFailure with var message: String and var retryable: Bool accessors that work across every variant.
Example:
do {
let outcome = try await client.formatTypedData(typedDataJson: typedDataJson)
// handle outcome
} catch let failure as FormatFailure {
switch failure {
case .InvalidInput(let detail, _):
showBlockingError(detail)
case .ResolutionFailed(let detail, let retryable):
showResolutionError(message: detail, retryable: retryable)
default:
showBlockingError(failure.message)
}
}retryable is intended for wallet policy and retry UX.
- bindings/swift/ClearSigningClient.swift
- bindings/swift/clear_signing.swift
- Package.swift
- wallet/Wallet/Services/WalletMetadataProvider.swift
Peer platform docs:
- docs/kotlin-integration.md — Kotlin / Android
- docs/react-native-integration.md — React Native
- docs/release-guide.md — release workflows for all three platforms