Skip to content

iOS: Peer Communication

Set up WebRTC peer-to-peer communication between your iOS wallet and dApps after successful registration or authentication.

Overview

Once authentication is complete, the startSignaling function establishes a WebRTC data channel for direct communication between the dApp (offerer) and your wallet (answerer). This enables:

  • Transaction signing requests from dApps
  • Real-time communication without centralized intermediaries
  • Authenticated messaging using the established Liquid Auth session

Implementation

Complete Signaling Setup

import WebRTC
import LiquidAuthSDK
func startSignaling(
origin: String,
requestId: String
) async throws {
let signalService = SignalService.shared
// Initialize the signal service with the origin
signalService.start(url: origin, httpClient: URLSession.shared)
// Configure ICE servers for NAT traversal
let iceServers = [
// Google STUN servers
RTCIceServer(
urlStrings: [
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302",
"stun:stun3.l.google.com:19302",
"stun:stun4.l.google.com:19302",
]
),
// Nodely TURN servers for more reliable connections
RTCIceServer(
urlStrings: [
"turn:global.turn.nodely.network:80?transport=tcp",
"turns:global.turn.nodely.network:443?transport=tcp",
"turn:eu.turn.nodely.io:80?transport=tcp",
"turns:eu.turn.nodely.io:443?transport=tcp",
"turn:us.turn.nodely.io:80?transport=tcp",
"turns:us.turn.nodely.io:443?transport=tcp",
],
username: "liquid-auth",
credential: "sqmcP4MiTKMT4TGEDSk9jgHY"
),
]
// Connect as answerer (wallets are always answerers)
signalService.connectToPeer(
requestId: requestId,
type: "answer",
origin: origin,
iceServers: iceServers,
onMessage: { [weak self] message in
print("💬 Received message: \(message)")
Task {
await self?.handleIncomingMessage(message)
}
},
onStateChange: { state in
print("🔄 Connection state changed: \(state)")
if state == "open" {
print("✅ Data channel is OPEN")
// Send initial ping to confirm connection
signalService.sendMessage("ping")
}
}
)
}

Message Handling

Processing Incoming Messages

/// Example implementation of message handler for ARC27 transaction requests
struct ExampleMessageHandler: LiquidAuthMessageHandler {
// Add your wallet integration here (e.g., reference to your signing API)
func handleMessage(_ message: String) async -> String? {
print("📨 Handling incoming message")
// Try to decode and determine message type
if isARC27Message(message) {
return await handleARC27Transaction(message)
}
// Handle other message types here in the future
print("Unknown message type, ignoring")
return nil
}
private func isARC27Message(_ message: String) -> Bool {
guard let cborData = Utility.decodeBase64Url(message),
let cbor = try? CBOR.decode([UInt8](cborData)),
let dict = cbor.asSwiftObject() as? [String: Any],
let reference = dict["reference"] as? String
else {
return false
}
return reference == "arc0027:sign_transactions:request"
}
private func handleARC27Transaction(_ message: String) async -> String? {
// 1. Decode base64url CBOR
guard let cborData = Utility.decodeBase64Url(message),
let cbor = try? CBOR.decode([UInt8](cborData)),
let dict = cbor.asSwiftObject() as? [String: Any]
else {
print("Failed to decode CBOR message")
return nil
}
// 2. Extract ARC27 fields
guard let reference = dict["reference"] as? String,
reference == "arc0027:sign_transactions:request",
let params = dict["params"] as? [String: Any],
let txns = params["txns"] as? [[String: Any]],
let requestId = dict["id"] as? String
else {
print("Invalid ARC27 request format")
return nil
}
// 3. Request user approval for transaction signing
let userApproved = await requestUserApprovalForSigning()
guard userApproved else {
print("User denied transaction signing")
return nil
}
// 4. Sign each transaction with wallet's logic
var signedTxns: [String] = []
for txnObj in txns {
guard let txnBase64Url = txnObj["txn"] as? String,
let txnBytes = Utility.decodeBase64Url(txnBase64Url)
else {
print("Failed to decode transaction")
continue
}
// Wallet-specific transaction signing
if let signature = await signTransaction(txnBytes) {
signedTxns.append(signature)
}
}
// 5. Build ARC27 response
let response: [String: Any] = [
"id": UUID().uuidString,
"reference": "arc0027:sign_transactions:response",
"requestId": requestId,
"result": [
"providerId": params["providerId"] ?? "liquid-auth-ios-example",
"stxns": signedTxns,
],
]
// 6. Encode and return
guard let cborResponse = try? CBOR.encodeMap(response) else {
print("Failed to encode ARC27 response")
return nil
}
return Data(cborResponse).base64URLEncodedString()
}
private func requestUserApprovalForSigning() async -> Bool {
// Request user verification for transaction signing
return await requireUserVerification(reason: "Approve transaction signing")
}
private func signTransaction(_ txnBytes: Data) async -> String? {
// TODO: Implement Algorand transaction signing logic for your wallet
return nil
}
}

ARC-27 specifies an Algorand standard for message schemas used in communication between clients and providers. For full details, see the ARC-27 standard documentation.

Security Considerations

  1. Validate message origins - Ensure messages come from authenticated dApps
  2. User confirmation required - Never auto-sign transactions without user approval
  3. Timeout handling - Implement timeouts for user interactions
  4. Connection security - WebRTC provides encryption, but validate message integrity
  5. Resource management - Properly clean up WebRTC resources

Next Steps