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:
| Instruction | Description |
|---|---|
register | Initializes a Registry PDA mapping a phone hash to a wallet |
send | Creates an Escrow PDA and transfers SOL into it |
claim | Releases 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:
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.
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)