Wallet

iOS Wallet

This guide covers wallet creation/address APIs, wallet selection, signing/sending, chain management, and RPC helpers.

Prerequisites

  • User should be authenticated (sdk.signIn() completed).
  • EVM chain routing requires chainId (recommended format: eip155:<number>).

Address & wallet APIs

let state = try await sdk.checkWallet()   // .exists | .migrationRequired | .notFound
let created = try await sdk.createWallet()
let primary = try await sdk.getAddress()
let byIndex = try await sdk.getAddress(index: 0)
let all = try await sdk.getAddresses()

createWallet() automatically handles the legacy wallet migration flow if a previous native app backup is detected. The user sees a "Wallet Found" prompt and can choose to recover (PIN input) or skip. Skipping throws CROSSxError.userRejected.

createWallet options

// Default: auto-migration enabled
let wallet = try await sdk.createWallet()

// Disable auto-migration — throws CROSSxError.migrationRequired if migration is needed
let wallet = try await sdk.createWallet(migrateAutomatically: false)
  • migrateAutomatically: true (default) — shows migration UI when a legacy backup is detected.
  • migrateAutomatically: false — throws CROSSxError.migrationRequired instead of showing UI, allowing the app to handle migration separately.

Wallet status check

Check the wallet state before performing wallet operations:

let status = try await sdk.checkWallet()

switch status {
case .exists:
    // Wallet is ready
    let addr = try await sdk.getAddress()
case .migrationRequired:
    // Legacy backup found — migration needed
    let wallet = try await sdk.createWallet()
case .notFound:
    // No wallet — create a new one
    let wallet = try await sdk.createWallet()
}

Wallet selector

Show an HD wallet selection modal. Pass currentAddress to highlight the currently active wallet.

let selected = try await sdk.selectWallet(currentAddress: activeAddress)
if let wallet = selected {
    // user picked a wallet
    let sig = try await sdk.signMessage("hello", chainId: chainId, from: wallet.address)
}
  • Tapping "add a wallet" derives the next HD wallet index automatically.
  • Returns nil if the user dismisses the modal without selecting.

Chain management

Query chains registered for the project (no authentication required):

// All chains
let chains = try await sdk.getChains()
for chain in chains {
    print("\(chain.chainId) → \(chain.rpcUrl)")
}

// Single chain
let chain = try await sdk.getChain(chainId: "eip155:612044")
print(chain.rpcUrl)

// RPC URL only
let rpcUrl = try await sdk.getRpcUrl(for: "eip155:612044")

Throws CROSSxError.unsupportedChain if the chain is not registered for the project.

Sign message / typed data

These APIs open the SDK confirmation UI.

let chainId = "eip155:612044"

// EIP-191 personal_sign
let signMessageResp = try await sdk.signMessage(
    "Hello CROSSx",
    chainId: chainId
)

// EIP-712 on-chain typed data (chainId required in domain)
let signTypedResp = try await sdk.signTypedData(
    """{"types":{},"primaryType":"Permit","domain":{"chainId":612044},"message":{}}""",
    chainId: chainId
)

// EIP-712 off-chain typed data (no domain chainId needed)
let signOffchainResp = try await sdk.signTypedDataOffchain(
    """{"types":{},"primaryType":"MyType","domain":{},"message":{"key":"value"}}"""
)

On-chain vs off-chain typed data:

  • signTypedData(_:chainId:) — use when typedData.domain.chainId exists and must match the chain. Routes to POST /mnemonic/sign-typed-data/:chainId.
  • signTypedDataOffchain(_:) — use when there is no domain chainId. Internally uses chainId "0".

Both methods also accept Data input instead of String:

let jsonData: Data = typedDataString.data(using: .utf8)!
let resp = try await sdk.signTypedData(jsonData, chainId: chainId)

Sign transaction

let tx = UnsignedTransaction(
    chainId: "eip155:612044",
    from: "0xYourAddress",
    to: "0xRecipient",
    value: "0xde0b6b3a7640000", // 1 ETH in wei (hex)
    data: "0x"
)

let signTxResp = try await sdk.signTransaction(
    tx,
    chainId: tx.chainId!
)

Send transaction

let sendResp = try await sdk.sendTransaction(
    tx,
    chainId: tx.chainId!
)
let txHash = sendResp.txHash

sendTransaction and sendTransactionAndWait automatically resolve missing nonce, gasLimit, and gas price fields via RPC before showing the confirmation dialog.

Send + wait for receipt in one call

let result = try await sdk.sendTransactionAndWait(
    tx,
    chainId: tx.chainId!,
    options: PollingOptions(intervalMs: 2000, timeoutMs: 60_000)
)
let receipt = result.receipt
print(receipt.status)  // "0x1" = success, "0x0" = reverted

Android-compatible API

sendTransactionWithWaitForReceipt provides the same functionality with an Android-compatible signature:

let receipt = try await sdk.sendTransactionWithWaitForReceipt(
    tx,
    chainId: tx.chainId!,
    timeoutMs: 30_000,
    pollIntervalMs: 1_000
)
print(receipt.transactionHash)

Manual receipt polling

let receipt = try await sdk.waitForTransaction(
    txHash: "0x...",
    chainId: "eip155:612044",
    options: PollingOptions(intervalMs: 2000, timeoutMs: 60_000)
)

Android-compatible polling

let receipt = try await sdk.waitForTxAndGetReceipt(
    txHash: "0x...",
    chainId: "eip155:612044",
    timeoutMs: 30_000,
    pollIntervalMs: 1_000
)

RPC helpers

All RPC helpers route directly to the chain's node via its RPC URL (fetched automatically from getChain).

let chainId = "eip155:612044"

// Generic JSON-RPC call (reads and eth_call only)
let rpcResult = try await sdk.walletRpc(
    request: JsonRpcRequest(method: "eth_call", params: [
        .object(["to": "0xContract", "data": "0x70a08231..."]),
        .string("latest")
    ]),
    chainId: chainId
)

// Native balance (eth_getBalance)
let balanceHex = try await sdk.getBalance(
    address: "0xYourAddress",
    chainId: chainId
)

// ERC20 token balance (eth_call + balanceOf)
let tokenBalance = try await sdk.getTokenBalance(
    contractAddress: "0xTokenContract",
    ownerAddress: "0xYourAddress",
    chainId: chainId
)

// Nonce (eth_getTransactionCount)
let nonceHex = try await sdk.getNonce(
    address: "0xYourAddress",
    chainId: chainId,
    blockTag: "pending"
)

// Legacy gas price (eth_gasPrice)
let gasPrice = try await sdk.getGasPrice(chainId: chainId)

// EIP-1559 priority fee (eth_maxPriorityFeePerGas)
let priorityFee = try await sdk.getMaxPriorityFeePerGas(chainId: chainId)

// Base fee from latest block — nil for legacy chains (eth_getBlockByNumber)
let baseFee = try await sdk.getBaseFeePerGas(chainId: chainId)

// Gas estimation (eth_estimateGas)
let gasLimit = try await sdk.estimateGas(tx, chainId: chainId)

// Transaction receipt
let receipt = try await sdk.getTransactionReceipt(
    txHash: "0x...",
    chainId: chainId
)

All return values are hex strings (e.g. "0xde0b6b3a7640000" for Wei values). getTransactionReceipt returns nil if the transaction is still pending. getBaseFeePerGas returns nil for legacy (non-EIP-1559) chains. Use walletRpc for reads and calls. Use dedicated APIs for signing and broadcasting transactions.

Wallet password and biometrics

The SDK may show a PIN entry modal before signing and sending. After verification once, the PIN is reused for later operations.

let can = sdk.canUseBiometric()
let enabled = sdk.isBiometricEnabled()
try await sdk.setBiometricEnabled(true)

© 2025 NEXUS Co., Ltd. All Rights Reserved.