ZingPay Docs
esc

    Protocol Architecture

    ZingPay is an escrow and payment layer built on Solana that abstracts away public keys, replacing them with standardized phone numbers. It utilizes Program Derived Addresses (PDAs) and client-side cryptographic hashing to ensure privacy and security.


    1. Smart Contract (Anchor)

    Framework: Anchor v0.32.1 (Rust edition 2021)

    Program ID: 8ik9hQSoHoEnnzDz2ifBjjNK8PBEAQwgcgJpuRYsgRMs (Devnet)

    The core solpay program facilitates secure SOL transfers mapped to phone numbers. Funds are held in a programmatic escrow until the designated recipient claims them.

    Key instructions:

    InstructionDescription
    registerInitializes a Registry PDA mapping a phone hash to a wallet
    sendCreates an Escrow PDA and transfers SOL into it
    claimReleases escrowed SOL to the verified claimant

    2. Privacy & Hashing Integration

    To ensure on-chain privacy, plaintext phone numbers are strictly prohibited anywhere in the protocol.

    The hashing pipeline has two stages:

    1

    Normalization

    Raw user input is strictly parsed to the E.164 international standard using libphonenumber-js. This ensures that +1 (555) 123-4567 and 15551234567 both resolve to the same canonical form: +15551234567.

    2

    SHA-256 Hashing

    The normalized E.164 string is hashed via the native Web Crypto API (crypto.subtle.digest('SHA-256', ...)), producing a 32-byte array. This hash is the only phone-related data that ever touches the Solana blockchain.

    // hash.ts (simplified)
    async function hashPhone(phone: string): Promise<Uint8Array> {
      const normalized = parsePhoneNumber(phone).format('E.164');
      const encoded = new TextEncoder().encode(normalized);
      const buffer = await crypto.subtle.digest('SHA-256', encoded);
      return new Uint8Array(buffer);
    }
    

    Security Note: SHA-256 is a one-way function. Even if someone reads the hash on-chain, they cannot reverse it to discover the original phone number.


    3. Program Derived Addresses (PDAs)

    The protocol uses deterministic seeds based on the hashed phone number to derive two critical account types:

    Registry PDA

    Seeds: ["registry", phoneHash]
    

    Maps a hashed phone number to an initialized registered user or their designated wallet address. This is the "phonebook" of ZingPay. It answers the question: "does this phone number have a registered wallet?"

    Escrow PDA

    Seeds: ["escrow", senderPublicKey, phoneHash]
    

    A transient holding account that locks funds from a specific sender until the owner of the phoneHash signs the claim transaction. This design means:

    • Multiple senders can escrow funds to the same phone number independently
    • Each escrow is isolated. One sender's funds cannot interfere with another's
    • Funds remain locked until explicitly claimed or reclaimed
    // program.ts: PDA derivation
    const [registryPda] = PublicKey.findProgramAddressSync(
      [Buffer.from("registry"), phoneHash],
      PROGRAM_ID
    );
    
    const [escrowPda] = PublicKey.findProgramAddressSync(
      [Buffer.from("escrow"), sender.toBuffer(), phoneHash],
      PROGRAM_ID
    );
    

    Architecture Diagram

    The complete flow from sender to receiver:

    Sender (Wallet)
        |
        |  E.164 + SHA-256
        v
    Phone Hash (32 bytes)
        |
        |  Seed
        v
    Registry PDA              Escrow PDA
    (maps hash to wallet)     (holds SOL, derived from sender + hash)
                                   |
                                   |  claim()
                                   v
                              Receiver
                              (ephemeral keypair generated in browser)