> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cdp.coinbase.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

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](https://docs.cdp.coinbase.com/).

The SDK ships a single library target — `CDPCore` — exposing an actor-based,
`async`/`await` API.

## Quickstart

This guide will help you get started with `CDPCore`. You'll learn how to install the
package, initialize the SDK, and make your first API call.

### Requirements

* Swift 5.9+ (Xcode 15+)
* iOS 16+ / macOS 13+

### Installation

Add the SDK to your Swift package dependencies:

```swift lines theme={null}
// Package.swift
dependencies: [
    .package(url: "https://github.com/coinbase/cdp-swift", from: "0.1.0"),
]
```

Then add the product to your target:

```swift lines theme={null}
.target(
    name: "YourApp",
    dependencies: [
        .product(name: "CDPCore", package: "cdp-swift"),
    ]
)
```

### Gather your CDP Project information

1. Sign in or create an account on the [CDP Portal](https://portal.cdp.coinbase.com)
2. On your dashboard, select a project from the dropdown at the top, and copy the Project ID

### Initialize the SDK

Before calling any methods in the SDK, you must first create a `WalletsClient` and call
`start()`. `WalletsClient` is an `actor` — every public method is `async`. Calling
`start()` restores any persisted session and registers the default Apple platform
services (Keychain, crypto, OAuth).

```swift lines theme={null}
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)
    }
}
```

For OAuth redirects, forward incoming URLs to `handleOAuthCode(url:)` via `.onOpenURL`
(see [OAuth](#oauth)).

#### Configuration

`CDPCoreConfig` controls SDK behaviour. Only `projectId` is required.

```swift lines theme={null}
let config = CDPCoreConfig(
    projectId: "your-project-id",
    customAuth: nil,                  // BYO identity provider — see Custom 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()`.

#### Account configuration

You can configure the SDK to create different types of accounts for new users.

**Smart Account configuration:**

```swift lines theme={null}
let config = CDPCoreConfig(
    projectId: "your-project-id",
    ethereum: EthereumConfig(createOnLogin: .smart) // Creates Smart Accounts instead of EOAs
)
```

When `ethereum.createOnLogin` is set to `.smart`, the SDK will:

1. Automatically create an EOA (Externally Owned Account) as the owner
2. Create a Smart Account owned by that EOA
3. Make both accounts available on the user object

**Solana account configuration:**

```swift lines theme={null}
let config = CDPCoreConfig(
    projectId: "your-project-id",
    solana: SolanaConfig(createOnLogin: true) // Creates Solana accounts
)
```

**Deferred account creation:**

Omit `createOnLogin` entirely to prevent automatic account creation and instead create
accounts manually when needed (see [Create Accounts Manually](#create-accounts-manually)).

```swift lines theme={null}
let config = CDPCoreConfig(
    projectId: "your-project-id"
    // No ethereum or solana createOnLogin configuration
)
```

### Sign In a User

The SDK supports five sign-in flows: Email OTP, SMS OTP, OAuth (Google/Apple/Telegram/…),
Sign-In With Ethereum (SIWE), and developer-issued JWT (see
[Custom Authentication](#sign-in-with-custom-authentication)).

#### Email OTP

```swift lines theme={null}
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)")
```

#### SMS OTP

```swift lines theme={null}
let flow = try await client.signInWithSms(SignInWithSmsOptions(phoneNumber: "+14155552671"))
let verified = try await client.verifySmsOTP(
    VerifySmsOTPOptions(flowId: flow.flowId, otp: "123456")
)
```

#### OAuth

`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
[Initialize the SDK](#initialize-the-sdk)).

```swift lines theme={null}
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):

```swift lines theme={null}
let result = try await client.verifyOAuth(
    VerifyOAuthOptions(flowId: flowId, code: code, providerType: .google)
)
```

Observe in-progress OAuth state:

```swift lines theme={null}
await client.onOAuthStateChange { state in
    // state?.status: .pending | .success | .error
}
```

#### Sign-In With Ethereum (SIWE)

```swift lines theme={null}
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)
)
```

### Link Additional Authentication Methods

Once a user is authenticated, you can link additional auth methods to their account. This
allows users to sign in using multiple methods (email, SMS, OAuth providers) with the same
embedded wallet.

```swift lines theme={null}
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()
```

### Sign In with Custom Authentication

If you're using a third-party identity provider (Auth0, Firebase, AWS Cognito, or any
OIDC-compliant provider), you can authenticate users with JWTs from your provider.

#### Prerequisites

Before using custom authentication:

1. **Configure your identity provider in the CDP Portal**:
   * Navigate to [Embedded Wallet Configuration](https://portal.cdp.coinbase.com/products/embedded-wallets)
   * Click on the Custom auth tab
   * Add your JWKS endpoint URL (e.g., `https://your-domain.auth0.com/.well-known/jwks.json`)
   * Configure your JWT issuer and audience

2. **Provide a `customAuth` closure** when initializing the SDK. The closure returns a
   fresh JWT from your identity provider, and the SDK invokes it automatically whenever a
   fresh bearer token is needed.

```swift lines theme={null}
let config = CDPCoreConfig(
    projectId: "your-project-id",
    customAuth: CustomAuth { try await myIdentityProvider.currentJwt() }
)
let client = try WalletsClient(config: config)
await client.start()
```

#### Authenticate a User

Once configured, call `authenticateWithJWT()` to authenticate the user:

```swift lines theme={null}
let result = try await client.authenticateWithJWT()
print("Is new user: \(result.isNewUser)")

// The user is now signed in and wallets are created based on your config
let user = await client.getCurrentUser()
print("EVM Address:", user?.evmAccountObjects?.first?.address ?? "none")
```

#### How it works

1. Your user signs in to your identity provider (Auth0, Firebase, Cognito, etc.)
2. You call `authenticateWithJWT()`, which internally invokes your `customAuth` closure
3. The SDK sends the JWT to CDP's backend, which validates it against your configured JWKS
4. If valid, the user is authenticated and wallets are auto-created based on your configuration
5. The `customAuth` closure is called automatically whenever the SDK needs a fresh token

### View User Information

Once the end user has signed in, you can read their session and account information.

```swift lines theme={null}
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)

await client.onAuthStateChange { user in
    // Called on sign-in / sign-out / token refresh.
}
```

Existing accounts are exposed on the `User`:

```swift lines theme={null}
let user = await client.getCurrentUser()
user?.evmAccountObjects        // [EndUserEvmAccount]?
user?.evmSmartAccountObjects   // [EndUserEvmSmartAccount]?
user?.solanaAccountObjects     // [EndUserSolanaAccount]?
```

To end a session:

```swift lines theme={null}
try await client.signOut()                                  // clears session
await client.resetSession()                                 // nuclear: Keychain + cookies + state
```

### Multi-Factor Authentication

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, _)`.

The SDK supports two MFA methods:

* **TOTP (Time-based One-Time Password)**: Users enroll using authenticator apps like
  Google Authenticator or Authy
* **SMS**: Users receive verification codes via text message to their phone number

> **Important**: Users must be authenticated (signed in) before they can enroll in MFA or
> perform MFA verification.

#### Check MFA configuration

```swift lines theme={null}
let config = try await client.getMfaConfig()        // MfaConfigState?
```

#### MFA enrollment flow

The enrollment flow consists of two steps. For TOTP, enrollment returns an `authUrl` and
`secret` to provision the authenticator app.

```swift lines theme={null}
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)
)
```

#### MFA verification flow

When performing sensitive operations that require MFA verification, use the verification
flow:

```swift lines theme={null}
let flowId = try await client.initiateMfaVerification(
    InitiateMfaVerificationOptions(mfaMethod: .totp)
)
try await client.submitMfaVerification(
    SubmitMfaVerificationOptions(code: "123456", mfaMethod: .totp)
)
await client.cancelMfaVerification()
```

### Create Accounts Manually

If you configured your SDK without `createOnLogin`, you can manually create accounts for
authenticated users when needed. This gives you full control over when accounts are
created. All three methods accept an optional `idempotencyKey: String`.

```swift lines theme={null}
let eoa = try await client.createEvmEoaAccount()                 // EndUserEvmAccount
let smart = try await client.createEvmSmartAccount()             // EndUserEvmSmartAccount (requires an EOA owner)
let solana = try await client.createSolanaAccount()              // EndUserSolanaAccount
```

### Sign Messages and Typed Data

End users can sign EVM messages, hashes, and typed data to generate signatures for various
onchain applications. All signing operations are MFA-gated when the project enables MFA —
see [Multi-Factor Authentication](#multi-factor-authentication).

#### EVM message / hash

```swift lines theme={null}
let msg = try await client.signEvmMessage(
    SignEvmMessageOptions(evmAccount: address, message: "Hello, CDP")
)

let hash = try await client.signEvmHash(
    SignEvmHashOptions(evmAccount: address, hash: "0xabc…")
)
```

#### EVM transaction (EIP-1559)

```swift lines theme={null}
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.
```

#### EVM typed data (EIP-712)

```swift lines theme={null}
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 message / transaction

Solana payloads are passed through as base64.

```swift lines theme={null}
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)
)
```

### Send an EVM Transaction

We support signing and sending an EVM transaction in a single call. Network enums are
available via `SendEvmTransactionNetwork`, `SendEvmUsdcNetwork`, `EvmUserOperationNetwork`,
`SendSolanaTransactionNetwork`, and `SendSolanaUsdcNetwork`.

```swift lines theme={null}
let tx = EvmTransaction(to: "0x…", value: "1000000000000000")
let res = try await client.sendEvmTransaction(
    SendEvmTransactionOptions(
        evmAccount: address,
        network: .baseSepolia,
        transaction: tx
    )
)
// res.transactionHash
```

USDC helpers (auto-encodes ERC-20 `transfer`):

```swift lines theme={null}
try await client.sendEvmUsdc(SendEvmUsdcOptions(
    evmAccount: address,
    to: recipient,
    amount: "1.5",                     // decimal string, e.g. "1.5" USDC
    network: .baseSepolia
))
```

### Smart Account Operations

Smart Accounts provide advanced account abstraction features, including user operations
and paymaster support.

#### Send user operations

```swift lines theme={null}
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
```

#### Get user operation status

```swift lines theme={null}
let status = try await client.getUserOperation(
    GetUserOperationOptions(
        userOperationHash: hash,
        evmSmartAccount: smartAccountAddress,
        network: .baseSepolia
    )
)
// status.status.rawValue, status.transactionHash
```

Smart-account USDC convenience:

```swift lines theme={null}
try await client.sendEvmSmartAccountUsdc(SendEvmSmartAccountUsdcOptions(
    evmSmartAccount: smartAccountAddress,
    to: recipient,
    amount: "1.5",
    network: .baseSepolia,
    useCdpPaymaster: true
))
```

### Send a Solana Transaction

```swift lines theme={null}
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
))
```

### Swaps

```swift lines theme={null}
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`.

### Spend Permissions

Spend permissions allow Smart Accounts to delegate spending authority to other accounts
within specified limits and time periods. This requires
`EthereumConfig(enableSpendPermissions: true)` and an EVM smart account.

```swift lines theme={null}
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
))
```

### Delegation

Developer-key delegation lets your backend perform certain actions on behalf of the user.

```swift lines theme={null}
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`, and `revokeDelegationForAddress`.

### EIP-7702

Delegate an EOA to a smart-account implementation contract.

```swift lines theme={null}
let opId = try await client.createEvmEip7702Delegation(
    CreateEvmEip7702DelegationOptions(address: eoa, network: "base-sepolia")
)

let success = try await client.waitForEvmEip7702Delegation(
    WaitForEvmEip7702DelegationOptions(delegationOperationId: opId)
)
```

### Error Handling

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:

```swift lines theme={null}
do {
    _ = try await client.getSwapPrice(options)
} catch let CDPCoreError.swap(code, message) where code == .insufficientLiquidity {
    print("No liquidity: \(message)")
} catch {
    print(error.localizedDescription)
}
```

### Testing Your Integration

Set `useMock: true` to swap in `MockWalletsAPIClient`, which returns deterministic
responses without making network calls — ideal for SwiftUI previews and unit tests.

```swift lines theme={null}
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`.

## What to read next

* [Wallet API v2](https://docs.cdp.coinbase.com/wallet-api-v2/docs/welcome)
* [API Reference](https://docs.cdp.coinbase.com/api-v2/docs/welcome)
