Implementing QR Code Scanner for Crypto Addresses in a Mobile App
A QR scanner is the entry point for the recipient's address. The task is not only to recognize the QR but to parse the URI scheme, validate the address, and auto-fill send form fields. A parsing error means the user enters wrong data and loses funds.
Choosing a Scanning Library
iOS — AVFoundation with AVCaptureMetadataOutput natively. For a more convenient API — VisionKit (iOS 16+) with DataScannerViewController. The latter requires less code and supports both QR and text simultaneously.
// iOS 16+ — DataScannerViewController
import VisionKit
let scanner = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.qr])],
qualityLevel: .balanced,
recognizesMultipleItems: false,
isHighFrameRateTrackingEnabled: false,
isPinchToZoomEnabled: true,
isGuidanceEnabled: true,
isHighlightingEnabled: true
)
scanner.delegate = self
present(scanner, animated: true)
try? scanner.startScanning()
Android — ML Kit Barcode Scanning (com.google.mlkit:barcode-scanning). Works on-device without internet, faster than ZXing on modern devices.
// Android — ML Kit scanning
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
val scanner = BarcodeScanning.getClient(options)
// Pass ImageProxy from CameraX to scanner.process()
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.rawValue?.let { parseQRContent(it) }
}
Parsing URI and Auto-Filling
The scanner returns a string. It can be a plain address or a URI per BIP-21/EIP-681. The parser must understand both.
// Android — parsing crypto URI
fun parseQRContent(content: String): QRParseResult {
// Plain Ethereum address (EIP-55 checksum or lowercase)
if (content.matches(Regex("^0x[0-9a-fA-F]{40}$"))) {
return QRParseResult(chain = "ethereum", address = content)
}
// EIP-681: ethereum:0xAddress@chainId?value=...
if (content.startsWith("ethereum:")) {
val uri = URI(content)
val address = uri.schemeSpecificPart.substringBefore("@").substringBefore("?")
val chainId = uri.schemeSpecificPart.substringAfter("@").substringBefore("?").toLongOrNull() ?: 1
val params = parseQueryParams(uri.query)
return QRParseResult(
chain = "ethereum",
address = address,
chainId = chainId,
value = params["value"],
contractAddress = params["address"] // for ERC-20 transfer
)
}
// BIP-21: bitcoin:address?amount=...
if (content.startsWith("bitcoin:")) {
val address = content.removePrefix("bitcoin:").substringBefore("?")
val amount = parseQueryParams(content.substringAfter("?"))["amount"]
return QRParseResult(chain = "bitcoin", address = address, amount = amount)
}
return QRParseResult(error = "Unknown format")
}
Address Validation After Parsing
After extracting the address, verify its correctness before filling the field:
- Ethereum/EVM: checksum via EIP-55, 42 characters length (with
0x) - Bitcoin: decode base58check or bech32 — invalid checksum returns an error
- Solana: base58, 32 bytes (43–44 characters)
Invalid address — show an error immediately on the scanner screen, don't proceed.
Warning About Substitution
After pasting the address from QR, show a truncated view (first 6 + last 4 characters) with a request to verify visually. This takes 2 seconds but prevents losses from QR-hijacking attacks (QR substitution in physical space).
Timeline: 1 day for scanning via ML Kit / DataScannerViewController, parsing BIP-21 and EIP-681 URI, validating addresses, auto-filling send form fields.







