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 throwsCROSSxError.userRejected.
createWallet options
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— throwsCROSSxError.migrationRequiredinstead 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
nilif 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 whentypedData.domain.chainIdexists and must match the chain. Routes toPOST /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
sendTransactionandsendTransactionAndWaitautomatically resolve missingnonce,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" = revertedAndroid-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).getTransactionReceiptreturnsnilif the transaction is still pending.getBaseFeePerGasreturnsnilfor legacy (non-EIP-1559) chains. UsewalletRpcfor 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)Updated 1 day ago