Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/frol/near-connect-ios/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Message signing allows users to cryptographically sign arbitrary messages off-chain without submitting a transaction. This is useful for authentication, proof of ownership, and secure messaging. NEAR Connect iOS implements the NEP-413 standard for message signing.

Sign a Message

Sign an off-chain message with the connected wallet:
do {
    let result = try await walletManager.signMessage(
        message: "Welcome to NEAR Connect!",
        recipient: "myapp.near",
        nonce: nil  // Auto-generated if nil
    )
    
    print("Account: \(result.accountId ?? "")")
    print("Public Key: \(result.publicKey ?? "")")
    print("Signature: \(result.signature ?? "")")
} catch {
    print("Signing failed: \(error.localizedDescription)")
}

Method Parameters

public func signMessage(
    message: String,      // Message to sign
    recipient: String,    // Intended recipient (e.g., your app's contract)
    nonce: Data?          // Optional 32-byte nonce (auto-generated if nil)
) async throws -> MessageSignResult

Parameters Explained

  • message: The human-readable message to sign (e.g., “Sign in to MyApp”)
  • recipient: The intended recipient account ID (typically your app’s contract or domain)
  • nonce: A 32-byte random nonce to prevent replay attacks. If nil, a secure random nonce is generated automatically
The nonce is automatically generated using iOS’s secure random number generator (SecRandomCopyBytes). You only need to provide a custom nonce if implementing a specific authentication protocol.

Message Sign Result

The signMessage() method returns a MessageSignResult:
public struct MessageSignResult {
    public let accountId: String?     // Signer's account ID
    public let publicKey: String?     // Public key used for signing
    public let signature: String?     // Signed message payload (JSON)
}
The signature field contains a JSON-encoded NEP-413 payload:
{
  "accountId": "alice.near",
  "publicKey": "ed25519:...",
  "signature": "ed25519:...",
  "message": "Welcome to NEAR Connect!",
  "recipient": "myapp.near",
  "nonce": "base64-encoded-nonce",
  "callbackUrl": "..."
}

Connect & Sign Message (Single Step)

Combine wallet connection and message signing in one user interaction:
do {
    let result = try await walletManager.connectAndSignMessage(
        message: "Sign in to NEAR Connect Demo",
        recipient: "near-connect-demo.near",
        nonce: nil
    )
    
    // Account info
    print("Connected: \(result.account.accountId)")
    print("Wallet: \(result.account.walletId)")
    
    // Signed message (if wallet supports it)
    if let signedMessage = result.signedMessage {
        print("Signed message: \(signedMessage)")
    } else {
        print("Wallet does not support sign-in-and-sign")
    }
} catch {
    print("Connect & sign failed: \(error.localizedDescription)")
}
Use connectAndSignMessage() for authentication flows. It presents a single wallet approval screen for both connection and message signing, improving UX.

Complete Example: Sign-In Flow

Here’s the real sign-in-and-sign implementation from the example app:
import SwiftUI
import NEARConnect

struct SignInAndSignMessageDemoView: View {
    @EnvironmentObject var walletManager: NEARWalletManager
    @State private var message = "Sign in to NEAR Connect Demo"
    @State private var recipient = "near-connect-demo.near"
    @State private var isProcessing = false
    @State private var showError = false
    @State private var errorMessage = ""
    
    var body: some View {
        Form {
            Section {
                Text("Connect a wallet and sign a message in a single step. The wallet presents one approval screen for both sign-in and message signing.")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            
            Section(header: Text("Message Parameters")) {
                VStack(alignment: .leading, spacing: 4) {
                    Text("Message")
                        .font(.caption)
                        .foregroundColor(.secondary)
                    TextEditor(text: $message)
                        .font(.system(.body, design: .monospaced))
                        .frame(height: 60)
                }
                
                TextField("Recipient", text: $recipient)
                    .textInputAutocapitalization(.never)
                    .autocorrectionDisabled()
            }
            
            if walletManager.isSignedIn {
                Section {
                    HStack {
                        Text("Connected as")
                            .foregroundColor(.secondary)
                        Spacer()
                        Text(walletManager.currentAccount?.accountId ?? "")
                            .font(.caption)
                    }
                    
                    Button(action: {
                        walletManager.disconnect()
                    }) {
                        Text("Disconnect first to test connect flow")
                            .foregroundColor(.red)
                    }
                }
            }
            
            Section {
                Button(action: connectAndSign) {
                    HStack {
                        Spacer()
                        if isProcessing {
                            ProgressView()
                                .padding(.trailing, 8)
                        }
                        Text(isProcessing ? "Connecting..." : "Connect & Sign Message")
                        Spacer()
                    }
                }
                .disabled(isProcessing || message.isEmpty || recipient.isEmpty || walletManager.isSignedIn)
            }
            
            Section {
                Text("A random nonce is generated automatically to prevent replay attacks.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
        .alert("Error", isPresented: $showError) {
            Button("OK", role: .cancel) { }
        } message: {
            Text(errorMessage)
        }
    }
    
    private func connectAndSign() {
        Task {
            isProcessing = true
            defer { isProcessing = false }
            
            do {
                let signInResult = try await walletManager.connectAndSignMessage(
                    message: message,
                    recipient: recipient
                )
                
                var output = "account: \(signInResult.account.accountId)"
                output += ", wallet: \(signInResult.account.walletId)"
                
                if let pk = signInResult.account.publicKey {
                    output += ", publicKey: \(pk)"
                }
                
                if let sig = signInResult.signedMessage {
                    output += "\nsignedMessage: \(sig)"
                }
                
                print("✅ \(output)")
            } catch {
                errorMessage = error.localizedDescription
                showError = true
            }
        }
    }
}

Verify Signed Messages

To verify a signed message on your backend, implement NEP-413 verification:
// Example: Sending signed message to backend for verification
func verifyWithBackend(signedMessage: String) async throws -> Bool {
    struct VerifyRequest: Codable {
        let signedMessage: String
    }
    
    let request = VerifyRequest(signedMessage: signedMessage)
    let jsonData = try JSONEncoder().encode(request)
    
    var urlRequest = URLRequest(url: URL(string: "https://api.myapp.com/verify")!)
    urlRequest.httpMethod = "POST"
    urlRequest.httpBody = jsonData
    urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    let (responseData, _) = try await URLSession.shared.data(for: urlRequest)
    
    struct VerifyResponse: Codable {
        let valid: Bool
    }
    
    let response = try JSONDecoder().decode(VerifyResponse.self, from: responseData)
    return response.valid
}

// Usage
let result = try await walletManager.signMessage(
    message: "Authenticate to MyApp",
    recipient: "myapp.near"
)

if let signature = result.signature {
    let isValid = try await verifyWithBackend(signedMessage: signature)
    if isValid {
        print("✅ Signature verified")
    } else {
        print("❌ Invalid signature")
    }
}

Backend Verification (NEP-413)

Your backend should verify the signature according to NEP-413:
  1. Parse the signed message JSON
  2. Verify the signature matches the public key
  3. Check the nonce hasn’t been used (prevent replay)
  4. Validate the recipient matches your app
  5. Check timestamp (if implementing expiration)
const { verifySignature } = require('near-api-js/lib/utils/key_pair');
const bs58 = require('bs58');

async function verifyNEP413(signedMessage) {
  const payload = JSON.parse(signedMessage);
  
  // 1. Extract components
  const { accountId, publicKey, signature, message, recipient, nonce } = payload;
  
  // 2. Verify recipient matches your app
  if (recipient !== 'myapp.near') {
    throw new Error('Invalid recipient');
  }
  
  // 3. Reconstruct the signed payload (NEP-413 format)
  const messageToVerify = {
    message,
    recipient,
    nonce: Buffer.from(nonce, 'base64'),
    callbackUrl: payload.callbackUrl || ''
  };
  
  const messageBytes = JSON.stringify(messageToVerify);
  
  // 4. Verify signature
  const publicKeyObj = PublicKey.from(publicKey);
  const signatureBytes = bs58.decode(signature.replace('ed25519:', ''));
  
  const isValid = publicKeyObj.verify(
    Buffer.from(messageBytes),
    signatureBytes
  );
  
  if (!isValid) {
    throw new Error('Invalid signature');
  }
  
  // 5. Check nonce hasn't been used (implement nonce store)
  await checkAndStoreNonce(nonce);
  
  return { valid: true, accountId };
}

Use Cases

Authentication

// Sign in with NEAR
let result = try await walletManager.connectAndSignMessage(
    message: "Sign in to MyApp",
    recipient: "myapp.near"
)

// Send to backend for session creation
if let signature = result.signedMessage {
    let sessionToken = try await authenticateWithBackend(signature: signature)
    UserDefaults.standard.set(sessionToken, forKey: "session_token")
}

Proof of Ownership

// Prove ownership of NEAR account
let timestamp = Int(Date().timeIntervalSince1970)
let result = try await walletManager.signMessage(
    message: "I own this account at \(timestamp)",
    recipient: "verification.near"
)

Secure Messaging

// Sign a message for off-chain communication
let result = try await walletManager.signMessage(
    message: "Hello, this is a secure message from me!",
    recipient: "chat.near"
)

if let signature = result.signature {
    // Send message + signature to recipient
    await sendMessage(content: message, signature: signature)
}

Error Handling

do {
    let result = try await walletManager.signMessage(
        message: message,
        recipient: recipient
    )
    print("✅ Message signed successfully")
} catch NEARError.notSignedIn {
    print("❌ Please connect a wallet first")
} catch NEARError.operationInProgress {
    print("❌ Another operation is in progress")
} catch NEARError.walletError(let message) {
    if message.contains("rejected") {
        print("User rejected signing")
    } else {
        print("❌ Wallet error: \(message)")
    }
} catch {
    print("❌ Unexpected error: \(error.localizedDescription)")
}
Always verify signed messages on your backend. Never trust client-side signature verification for authentication or authorization decisions.

Custom Nonce

Provide a custom nonce for specific protocols:
// Generate custom nonce
func generateCustomNonce() -> Data {
    var nonce = Data(count: 32)
    _ = nonce.withUnsafeMutableBytes {
        SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!)
    }
    return nonce
}

let customNonce = generateCustomNonce()
let result = try await walletManager.signMessage(
    message: "Custom protocol message",
    recipient: "protocol.near",
    nonce: customNonce
)

Next Steps

Build docs developers (and LLMs) love