- Overview
- Requirements
- Installation
- Quick Start
- Configuration
- Authentication
- Account Linking
- Sessions
- Accounts
- Signing
- Sending Transactions
- Swaps
- Spend Permissions
- Delegation
- EIP-7702
- MFA
- Error Handling
- Testing Your Integration
- Documentation
- License
- Support
- Security
The CDP Swift SDK is an embedded-wallets solution for iOS and macOS applications. It provides end-user authentication, account creation, signing, swaps, and transaction broadcasting through the Coinbase Developer Platform.
The SDK ships a single library target — CDPCore — exposing an actor-based, async/await API.
Tip
If you're looking to contribute to the SDK, please see the Contributing Guide.
- Swift 5.9+ (Xcode 15+)
- iOS 16+ / macOS 13+
Add the SDK to your Swift package dependencies:
// Package.swift
dependencies: [
.package(url: "https://github.com/coinbase/cdp-swift", from: "0.1.0"),
]Then add the product to your target:
.target(
name: "YourApp",
dependencies: [
.product(name: "CDPCore", package: "cdp-swift"),
]
)import CDPCore
import SwiftUI
@main
struct MyApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
.task { await appState.initializeSDK() }
.onOpenURL { url in
Task { await appState.handleOpenURL(url) }
}
}
}
}
@MainActor
final class AppState: ObservableObject {
@Published var client: WalletsClient?
@Published var user: User?
func initializeSDK() async {
let c = try? WalletsClient(config: CDPCoreConfig(projectId: "your-project-id"))
await c?.start()
client = c
await c?.onAuthStateChange { [weak self] user in
Task { @MainActor in self?.user = user }
}
}
func handleOpenURL(_ url: URL) async {
try? await client?.handleOAuthCode(url: url)
}
}WalletsClient is an actor — every public method is async. Always call start() after init to restore any persisted session and register the default Apple platform services (Keychain, crypto, OAuth). For OAuth redirects, forward incoming URLs to handleOAuthCode(url:) via .onOpenURL.
CDPCoreConfig controls SDK behaviour. Only projectId is required.
let config = CDPCoreConfig(
projectId: "your-project-id",
customAuth: nil, // BYO identity provider — see Authentication
useMock: false, // true → MockWalletsAPIClient for previews / offline
debugging: false, // verbose logging
basePath: nil, // override CDP API base URL
ethereum: EthereumConfig(
createOnLogin: .smart, // .smart | .eoa | nil
enableSpendPermissions: true
),
solana: SolanaConfig(createOnLogin: true),
callbackURLScheme: "myapp" // OAuth deep-link scheme (defaults to bundleIdentifier)
)For custom platform services (alternative storage, crypto, OAuth), call PlatformRegistry.shared.setPlatformServices(...) before start().
The SDK supports five sign-in flows: Email OTP, SMS OTP, OAuth (Google/Apple/Telegram/…), Sign-In With Ethereum (SIWE), and developer-issued JWT (BYO auth).
let flow = try await client.signInWithEmail(SignInWithEmailOptions(email: "user@example.com"))
let verified = try await client.verifyEmailOTP(
VerifyEmailOTPOptions(flowId: flow.flowId, otp: "123456")
)
print("Signed in as \(verified.user.userId)")let flow = try await client.signInWithSms(SignInWithSmsOptions(phoneNumber: "+14155552671"))
let verified = try await client.verifySmsOTP(
VerifySmsOTPOptions(flowId: flow.flowId, otp: "123456")
)signInWithOAuth returns a flow ID and opens the provider's auth page. The provider redirects back to your app via the URL scheme configured in CDPCoreConfig.callbackURLScheme; forward that URL to handleOAuthCode (see Quick Start).
let flowId = try await client.signInWithOAuth(providerType: .google)
// Redirect arrives via .onOpenURL → handleOAuthCode(url:) completes the flow.Supported providers via OAuth2ProviderType: .google, .apple, .telegram, plus other configured providers.
For manual code exchange (no deep link):
let result = try await client.verifyOAuth(
VerifyOAuthOptions(flowId: flowId, code: code, providerType: .google)
)Observe in-progress OAuth state:
await client.onOAuthStateChange { state in
// state?.status: .pending | .completed | .failed | .cancelled
}let challenge = try await client.signInWithSiwe(SignInWithSiweOptions(...))
// Sign challenge.message with the user's wallet, then:
let result = try await client.verifySiweSignature(
VerifySiweSignatureOptions(flowId: challenge.flowId, signature: signature)
)Pass a CustomAuth closure that returns a JWT from your identity provider. Once configured, call authenticateWithJWT() to sign in.
let config = CDPCoreConfig(
projectId: "your-project-id",
customAuth: CustomAuth { try await myIdentityProvider.currentJwt() }
)
let client = try WalletsClient(config: config)
await client.start()
let result = try await client.authenticateWithJWT()
print("New user: \(result.isNewUser)")The SDK invokes getJwt automatically whenever a fresh bearer token is needed.
Add additional auth methods to an already signed-in user.
let flowId = try await client.linkEmail("alt@example.com")
_ = try await client.verifyEmailOTP(VerifyEmailOTPOptions(flowId: flowId, otp: "123456"))
let smsFlowId = try await client.linkSms("+14155552671")
let oauthFlowId = try await client.linkOAuth(providerType: .google)
let appleFlowId = try await client.linkApple()
let googleFlowId = try await client.linkGoogle()
let telegramFlowId = try await client.linkTelegram()let user = await client.getCurrentUser() // User?
let signedIn = await client.isSignedIn() // Bool
let token = try await client.getAccessToken() // String?
let expiry = await client.getAccessTokenExpiration() // Int? (epoch seconds)
try await client.signOut() // clears session
await client.resetSession() // nuclear: Keychain + cookies + state
await client.onAuthStateChange { user in
// Called on sign-in / sign-out / token refresh.
}let eoa = try await client.createEvmEoaAccount() // EndUserEvmAccount
let smart = try await client.createEvmSmartAccount() // EndUserEvmSmartAccount (requires an EOA owner)
let solana = try await client.createSolanaAccount() // EndUserSolanaAccountAll three accept an optional idempotencyKey: String. Existing accounts are exposed on the User:
let user = await client.getCurrentUser()
user?.evmAccountObjects // [EndUserEvmAccount]?
user?.evmSmartAccountObjects // [EndUserEvmSmartAccount]?
user?.solanaAccountObjects // [EndUserSolanaAccount]?All signing operations are MFA-gated when the project enables MFA — see MFA.
let msg = try await client.signEvmMessage(
SignEvmMessageOptions(evmAccount: address, message: "Hello, CDP")
)
let hash = try await client.signEvmHash(
SignEvmHashOptions(evmAccount: address, hash: "0xabc…")
)let tx = EvmTransaction(to: "0x…", value: "1000000000000000")
let signed = try await client.signEvmTransaction(
SignEvmTransactionOptions(evmAccount: address, transaction: tx)
)
// signed.signedTransaction is RLP-encoded with the 0x02 EIP-1559 prefix.let typedData = EIP712TypedData(
domain: EIP712Domain(name: "MyDapp", version: "1", chainId: 84532,
verifyingContract: "0x…"),
types: ["EIP712Domain": [...], "Message": [["name": "content", "type": "string"]]],
primaryType: "Message",
message: ["content": AnyCodable("Hello")]
)
let result = try await client.signEvmTypedData(
SignEvmTypedDataOptions(evmAccount: address, typedData: typedData)
)Solana payloads are passed through as base64.
let messageBase64 = Data("Hello".utf8).base64EncodedString()
let sig = try await client.signSolanaMessage(
SignSolanaMessageOptions(solanaAccount: address, message: messageBase64)
)
let signed = try await client.signSolanaTransaction(
SignSolanaTransactionOptions(solanaAccount: address, transaction: base64Tx)
)let tx = EvmTransaction(to: "0x…", value: "1000000000000000")
let res = try await client.sendEvmTransaction(
SendEvmTransactionOptions(
evmAccount: address,
network: .baseSepolia,
transaction: tx
)
)
// res.transactionHashUSDC helpers (auto-encodes ERC-20 transfer):
try await client.sendEvmUsdc(SendEvmUsdcOptions(
evmAccount: address,
to: recipient,
amount: "1.5", // decimal string, e.g. "1.5" USDC
network: .baseSepolia
))let call = EvmCall(to: contract, value: "0", data: callData)
let opRes = try await client.sendUserOperation(
SendUserOperationOptions(
evmSmartAccount: smartAccountAddress,
network: .baseSepolia,
calls: [call],
useCdpPaymaster: true
)
)
let hash: Hex = opRes.userOperationHash
// Poll status:
let status = try await client.getUserOperation(
GetUserOperationOptions(
userOperationHash: hash,
evmSmartAccount: smartAccountAddress,
network: .baseSepolia
)
)
// status.status.rawValue, status.transactionHashSmart-account USDC convenience:
try await client.sendEvmSmartAccountUsdc(SendEvmSmartAccountUsdcOptions(
evmSmartAccount: smartAccountAddress,
to: recipient,
amount: "1.5",
network: .baseSepolia,
useCdpPaymaster: true
))let res = try await client.sendSolanaTransaction(
SendSolanaTransactionOptions(
solanaAccount: address,
network: .solanaDevnet,
transaction: base64Tx,
useCdpSponsor: false
)
)
// res.transactionSignature
try await client.sendSolanaUsdc(SendSolanaUsdcOptions(
solanaAccount: address,
to: recipient,
amount: "1.5",
network: .solanaDevnet
))Network enums: SendEvmTransactionNetwork, SendEvmUsdcNetwork, EvmUserOperationNetwork, SendSolanaTransactionNetwork, SendSolanaUsdcNetwork.
let price = try await client.getSwapPrice(GetSwapPriceOptions(
fromToken: usdc, toToken: weth, fromAmount: "1000000",
account: nil, // auto-resolve taker (prefers smart account over EOA)
network: .base, slippageBps: 100
))
guard price.liquidityAvailable else { return }
print("Min out: \(price.minToAmount ?? "?")")
let result = try await client.executeSwap(ExecuteSwapOptions(
fromToken: usdc, toToken: weth, fromAmount: "1000000",
account: takerAddress, network: .base, slippageBps: 100
))
switch result {
case .eoaResult(let txHash): print("EOA tx: \(txHash)")
case .smartAccountResult(let opHash): print("UserOp: \(opHash)")
}useCdpPaymaster and paymasterUrl are mutually exclusive — passing both throws CDPCoreError.inputValidation.
Requires EthereumConfig(enableSpendPermissions: true) and an EVM smart account.
let created = try await client.createSpendPermission(CreateSpendPermissionOptions(
evmSmartAccount: smartAccount,
network: "base-sepolia",
spender: spenderAddress,
token: "eth", // "eth" or ERC-20 address
allowance: "1000000000000000", // wei
periodInDays: 7,
end: Int(Date().addingTimeInterval(86400 * 30).timeIntervalSince1970)
))
// created.userOpHash, created.status
let list = try await client.listSpendPermissions(
ListSpendPermissionsOptions(evmSmartAccount: smartAccount)
)
for permission in list.spendPermissions {
print(permission.permissionHash, permission.revoked)
}
let revoked = try await client.revokeSpendPermission(RevokeSpendPermissionOptions(
evmSmartAccount: smartAccount,
network: permission.network,
permissionHash: permission.permissionHash
))Developer-key delegation lets your backend perform certain actions on behalf of the user.
let info = try await client.getDelegation() // DelegationInfo?
try await client.createDelegation(
CreateDelegationOptions(expiresAt: Date().addingTimeInterval(86400))
)
try await client.revokeDelegation()Address-scoped variants are available: getDelegationForAddress, createDelegationForAddress, revokeDelegationForAddress.
Delegate an EOA to a smart-account implementation contract.
let opId = try await client.createEvmEip7702Delegation(
CreateEvmEip7702DelegationOptions(address: eoa, network: "base-sepolia")
)
let success = try await client.waitForEvmEip7702Delegation(
WaitForEvmEip7702DelegationOptions(delegationOperationId: opId)
)Sensitive actions (signing, sending, spend permissions, delegation) automatically gate on MFA when enabled for the project. You must register an MFA listener via MFAState — without one, those actions throw CDPCoreError.mfa(.listenerRequired, _).
let config = try await client.getMfaConfig() // MfaConfigState?
// Enrollment (TOTP returns authUrl + secret to provision the authenticator app)
let enroll = try await client.initiateMfaEnrollment(
InitiateMfaEnrollmentOptions(mfaMethod: .totp)
)
switch enroll {
case .totp(let authUrl, let secret): break // provision authenticator app
case .sms(let success): break
}
try await client.submitMfaEnrollment(
SubmitMfaEnrollmentOptions(code: "123456", mfaMethod: .totp)
)
// Verification (explicit, e.g. before a sensitive action)
let flowId = try await client.initiateMfaVerification(
InitiateMfaVerificationOptions(mfaMethod: .totp)
)
try await client.submitMfaVerification(
SubmitMfaVerificationOptions(code: "123456", mfaMethod: .totp)
)
await client.cancelMfaVerification()All SDK errors are cases of CDPCoreError:
| Case | Meaning |
|---|---|
.notInitialized(String) |
start() not called |
.notSignedIn(String) |
Operation requires an authenticated user |
.alreadySignedIn(String) |
Sign-in attempted while already authenticated |
.accountNotFound(String) |
The current user does not own the supplied address |
.inputValidation(String) |
Invalid argument (address format, missing field, conflicting options) |
.validation(String) |
Server-side validation error |
.mfa(MfaErrorCode, String) |
.superseded / .cancelled / .listenerRequired |
.swap(SwapErrorCode, String) |
.insufficientLiquidity / .insufficientAllowance / .insufficientBalance / .transactionSimulationFailed |
.customAuth(String) |
BYO-auth misconfiguration or JWT failure |
.api(statusCode:errorType:message:correlationId:) |
API returned an error |
.network(String) |
Transport-level failure |
.internal(String) |
Unexpected internal state |
Inspect specific cases with pattern matching:
do {
_ = try await client.getSwapPrice(options)
} catch let CDPCoreError.swap(code, message) where code == .insufficientLiquidity {
print("No liquidity: \(message)")
} catch {
print(error.localizedDescription)
}Set useMock: true to swap in MockWalletsAPIClient, which returns deterministic responses without making network calls — ideal for SwiftUI previews and unit tests.
let client = try WalletsClient(
config: CDPCoreConfig(projectId: "test", useMock: true)
)
await client.start()For richer fake responses (programmable per-call), implement the WalletsAPIClient protocol yourself and inject it via the apiClient: parameter on WalletsClient.init.
This project is licensed under the Apache 2.0 License — see the LICENSE file for details.
For feature requests, feedback, or questions, please reach out to us in the #cdp-sdk channel of the Coinbase Developer Platform Discord.
If you discover a security vulnerability within this SDK, please see our Security Policy for disclosure information.