Tongo Documentation
Welcome to the official Tongo documentation. Tongo is a confidential payment system for ERC20 tokens on Starknet, providing privacy-preserving transactions while maintaining auditability and compliance features.
What is Tongo?
Tongo wraps any ERC20 token with ElGamal encryption, enabling private transfers while maintaining full auditability. Built on zero-knowledge proofs and homomorphic encryption over the Stark curve, Tongo enables users to transact with hidden amounts while preserving the ability to verify transaction validity.
Key Features:
- No Trusted Setup: Built entirely on elliptic curve cryptography
- Hidden Amounts: All transfer amounts are encrypted
- Native Starknet Integration: Leverages Starknet's elliptic curve operations
- Flexible Compliance: Global auditor support and selective disclosure
Documentation Structure
Protocol Documentation
Learn about the underlying protocol, cryptography, and how Tongo works:
- Introduction to confidential payments
- Encryption system and ZK proofs
- Transfer protocol
- Auditing and compliance
- Cairo contracts
SDK Documentation
Get started building with the Tongo TypeScript SDK:
- Installation and quick start
- Core concepts and architecture
- Step-by-step guides
- Complete API reference
- Real-world examples
Quick Links
- GitHub: github.com/fatlabsxyz/tongo
- npm Package: @fatsolutions/tongo-sdk
- Website: tongo.cash
Getting Started
If you're a developer looking to integrate Tongo into your application, start with the SDK Quick Start.
If you want to understand the protocol and cryptography, begin with the Protocol Introduction.
Introduction to Tongo
Tongo is a confidential payment system for ERC20 tokens on Starknet, providing privacy-preserving transactions while maintaining auditability and compliance features. Built on ElGamal encryption and zero-knowledge proofs, Tongo enables users to transact with hidden amounts while preserving the ability to verify transaction validity.
What Makes Tongo Different
No Trusted Setup
Unlike many ZK systems (Zcash, Tornado Cash), Tongo requires no trusted ceremony. All cryptography is built on the discrete logarithm assumption over the Stark curve, with no hidden trapdoors or setup parameters.
Native Starknet Integration
Tongo leverages Starknet's native elliptic curve operations, making verification extremely efficient (~120K Cairo steps per transfer) compared to other privacy solutions that require expensive proof verification.
Flexible Compliance
The protocol supports multiple compliance models:
- Global auditor: All transactions encrypted for regulatory oversight
- Selective disclosure: Optional viewing keys per transaction
- Ex-post proving: Retroactive transaction disclosure without revealing keys
How It Works
1. Key Generation
Each user generates a keypair \((x, y = g^x)\) where \(g\) is the Stark curve generator. The public key \(y\) serves as their account identifier.
2. Encrypted Balances
Balances are stored as ElGamal ciphertexts:
$$\text{Enc}[y](b, r) = (g^b y^r, g^r)$$
The encryption is additively homomorphic, allowing balance updates without decryption.
3. Zero-Knowledge Proofs
All operations require proofs built from three primitives:
- POE (Proof of Exponent): Prove knowledge of discrete logs
- PED (Pedersen): Prove commitment correctness
- RAN (Range): Prove values are in valid ranges
4. Transaction Flow
Fund → Transfer → Rollover → Withdraw
- Fund: Convert ERC20 to encrypted balance
- Transfer: Send hidden amounts with ZK proofs
- Rollover: Claim pending incoming transfers
- Withdraw: Convert back to standard ERC20
Core Operations
Funding
Convert standard ERC20 tokens to encrypted balances:
const fundOp = await account.fund({ amount: 1000n });
await signer.execute([fundOp.approve!, fundOp.toCalldata()]);
Transfers
Send hidden amounts to other users:
const transferOp = await account.transfer({
to: recipientPubKey,
amount: 100n,
});
await signer.execute([transferOp.toCalldata()]);
Withdrawals
Convert back to standard ERC20 tokens:
const withdrawOp = await account.withdraw({
to: starknetAddress,
amount: 50n
});
await signer.execute([withdrawOp.toCalldata()]);
Security Model
Privacy Guarantees
- Balance confidentiality: Only key holders (account owners and auditors) can decrypt balances
- Transaction privacy: Transfer amounts are hidden from public view
- Unlinkability: Transactions don't reveal sender-receiver relationships
Integrity Guarantees
- No double spending: Range proofs prevent negative balances
- Conservation: Total supply is preserved (no money creation)
- Authenticity: Only key owners can spend their balances
Use Cases
Individual Privacy
- Personal transactions: Hide payment amounts from public view
- Salary payments: Confidential payroll systems
- Merchant payments: Private commercial transactions
Institutional Compliance
- Regulated environments: Auditor oversight with user privacy
- Cross-border payments: Compliance with multiple jurisdictions
- Corporate treasury: Internal transfers with audit trails
DeFi Integration
- Private AMM trading: Hidden trade sizes
- Confidential lending: Private collateral amounts
- DAO treasuries: Private governance token distributions
Getting Started
To start building with Tongo, proceed to the SDK Documentation for installation and usage guides.
To understand the cryptographic foundations, continue to the Encryption System chapter.
Encryption System
Tongo uses ElGamal encryption over elliptic curves to maintain confidential balances while enabling homomorphic operations on-chain.
ElGamal Encryption
Each user's balance is encrypted using a public key derived from their private key. The encryption function is defined as:
$$\begin{aligned} \text{Enc}[y]: [0, b_{\max}) \times \mathbb{F}_p &\rightarrow G^2 \ \text{Enc}[y]\left(b,r\right) &= (L, R) = (g^b y^r, g^r) \end{aligned}$$
Where:
- \(y = g^x\) is the user's public key (derived from private key \(x\))
- \(g\) is the generator of the Stark curve
- \(b\) is the balance amount in the range \([0, b_{\max})\)
- \(r\) is a random blinding factor
- \(p\) is the curve order
Additive Homomorphism
The key property that makes Tongo efficient is additive homomorphism. Given two encryptions under the same public key, their product is a valid encryption of the sum:
$$\text{Enc}[y]\left(b,r\right) \cdot \text{Enc}[y]\left(b',r'\right) = (g^{b+b'} y^{r+r'}, g^{r+r'}) = \text{Enc}[y]\left(b+b', r+r'\right)$$
This allows the contract to:
- Add encrypted amounts without decryption
- Subtract encrypted amounts homomorphically
- Update balances while maintaining privacy
Balance Decryption
To read their balance, a user recovers \(g^b\) using their private key \(x\):
$$g^b = \frac{L}{R^x} = \frac{g^b y^r}{(g^r)^x} = \frac{g^b (g^x)^r}{g^{rx}} = g^b$$
Since \(b\) is bounded by \([0, 2^{32})\), the discrete logarithm \(b\) can be computed efficiently through:
- Brute force: Iterate \(g^i\) for \(i = 0, 1, 2, \ldots\) until matching \(g^b\)
- Baby-step Giant-step: More efficient \(O(\sqrt{n})\) algorithm
- Pollard's rho: Probabilistic algorithm with similar complexity
A naïve JavaScript implementation can decrypt ~100k units per second, while optimized algorithms handle the full 32-bit range much faster.
Storage Architecture
The Tongo contract maintains multiple encrypted representations of each balance:
Primary Balances
balance: Current encrypted balance using user's public keyaudit_balance: Same balance encrypted for global auditor's keypending: Buffer for incoming transfers (anti-spam protection)
Recovery Balance
sym_balance: Symmetrically encrypted balance for fast recovery
The symmetric encryption uses a key derived from the user's private key, allowing instant balance recovery without discrete log computation. This is particularly useful for mobile wallets and cross-device synchronization.
Implementation Note: The
sym_balanceis not cryptographically enforced by the protocol since there's no way to prove the user provided the correct symmetric ciphertext. It's purely a convenience feature.
Security Properties
Computational Assumptions
- Discrete Log Problem: Hard to find \(x\) given \(g^x\)
- Decisional Diffie-Hellman: Hard to distinguish random group elements from valid encryptions
Practical Security
- 32-bit range: Balances limited to \([0, 2^{32})\) for efficient decryption
- Random blinding: Each encryption uses fresh randomness
- Curve security: Relies on Stark curve (ECDSA-256 security level)
Privacy Guarantees
- Balance confidentiality: Only the key holder can decrypt
- Transaction privacy: Transfer amounts remain hidden
- Unlinkability: Encrypted balances don't reveal relationships
Example: Fund Operation
When a user funds their account with amount \(b\):
- Public inputs: \(b\) (revealed in ERC20 transfer), \(y\) (user's public key)
- Encryption: \(\text{Enc}[y](b, 1) = (g^b y, g)\)
- Storage: Add to user's encrypted balance homomorphically
Note that \(r = 1\) is used for funding since the amount \(b\) is already public in the ERC20 transaction.
#![allow(unused)] fn main() { // Cairo implementation (simplified) let funded_cipher = CipherBalance { L: (curve_ops::multiply(G, b) + curve_ops::multiply(y, 1)), R: curve_ops::multiply(G, 1) }; // Add to existing balance homomorphically balance = cipher_add(balance, funded_cipher); }
The homomorphic addition is performed point-wise:
$$L_{\text{new}} = L_{\text{old}} \cdot L_{\text{fund}}$$
$$R_{\text{new}} = R_{\text{old}} \cdot R_{\text{fund}}$$
This mathematical elegance allows Tongo to update balances without ever revealing the underlying amounts, forming the foundation for all confidential operations in the protocol.
Transfer Protocol
The transfer operation is the core of Tongo's confidential payment system, allowing users to send encrypted amounts while proving transaction validity through zero-knowledge proofs.
Transfer Overview
When a user (sender) with public key \(y_s\) and balance \(b_0\) wants to transfer amount \(b < b_0\) to a receiver with public key \(y_r\), they must:
- Create encryptions for sender, receiver, and auditor
- Generate ZK proofs to validate the transaction
- Submit transaction with ciphertexts and proofs
The key insight is that balances are updated homomorphically without revealing the transfer amount.
Multi-Party Encryption
The sender creates (at least) three encryptions of the same amount \(b\) using the same blinding factor \(r\):
Sender Encryption
$$(\mathit{L_s}, \mathit{R_s}) = \text{Enc}[y_s](b, r) = (g^b y_s^r, g^r)$$
This will be subtracted from the sender's balance.
Receiver Encryption
$$(\mathit{L_r}, \mathit{R_r}) = \text{Enc}[y_r](b, r) = (g^b y_r^r, g^r)$$
This will be added to the receiver's pending balance.
Auditor Encryption
$$(\mathit{L_a}, \mathit{R_a}) = \text{Enc}[y_a](b, r) = (g^b y_a^r, g^r)$$
This provides an audit trail for compliance without revealing amounts.
Security Note: Using the same blinding factor \(r\) across all encryptions is safe for single-recipient transfers but could enable insider attacks in multi-recipient schemes. Tongo mitigates this by design.
Transaction Structure
#![allow(unused)] fn main() { struct Transfer { from: PubKey, // Sender's public key to: PubKey, // Receiver's public key L: StarkPoint, // L_s (sender encryption left) L_rec: StarkPoint, // L_r (receiver encryption left) L_audit: StarkPoint, // L_a (auditor encryption left) L_opt: Option<Array<(PubKey, StarkPoint)>>, // Additional viewing keys R: StarkPoint, // Shared R component proof: ProofOfTransfer, // ZK proof bundle } }
Required Zero-Knowledge Proofs
The sender must provide a comprehensive proof \(\pi_{\text{transfer}}\) demonstrating:
1. Ownership Proof (POE)
Prove knowledge of private key \(x\) such that \(y_s = g^x\):
$$\pi_{\text{ownership}}: {(g, y_s; x) : y_s = g^x}$$
2. Blinding Factor Proof (POE)
Prove knowledge of \(r\) such that \(R = g^r\):
$$\pi_{\text{blinding}}: {(g, R; r) : R = g^r}$$
3. Encryption Validity (PED)
Prove that \(L_s\) is correctly formed:
$$\pi_{\text{sender}}: {(g, y_s, L_s; b, r) : L_s = g^b y_s^r}$$
Prove that \(L_r\) uses the same \(b\) and \(r\):
$$\pi_{\text{receiver}}: {(g, y_r, L_r; b, r) : L_r = g^b y_r^r}$$
Prove that \(L_a\) uses the same \(b\) and \(r\):
$$\pi_{\text{auditor}}: {(g, y_a, L_a; b, r) : L_a = g^b y_a^r}$$
4. Range Proofs (RAN)
Prove the transfer amount is positive:
$$\pi_{\text{amount}}: {(g, h, V_b; b, r_b) : V_b = g^b h^{r_b} \land b \in [0, b_{\max})}$$
Prove the remaining balance is non-negative:
$$\pi_{\text{remaining}}: {(g, h, V_{b^\prime}; b^\prime, r_{b^\prime}) : V_{b^\prime} = g^{b^\prime} h^{r_{b^\prime}} \land b^\prime \in [0, b_{\max})}$$
Where \(b^\prime = b_0 - b\) is the sender's balance after the transfer.
Complete Transfer Proof
The full proof statement combines all requirements:
$$\begin{aligned} \pi_{\text{transfer}}: \{&(g, y_s, y_r, L_0, R_0, L_s, L_r, R; x, b, b^\prime, r) : \\ &y_s = g^x \\ &\land R = g^r \\ &\land L_s = g^b y_s^r \\ &\land L_r = g^b y_r^r \\ &\land b \in [0, b_{\max}) \\ &\land L_0/L_s = g^{b^\prime}(R_0/R)^x \\ &\land b^\prime \in [0, b_{\max})\} \end{aligned}$$
Where \((L_0, R_0)\) represents the sender's current encrypted balance.
Balance Updates
Upon successful proof verification, the contract performs homomorphic updates:
Sender Balance Update
#![allow(unused)] fn main() { // Subtract transfer amount from sender new_sender_balance = cipher_subtract(old_sender_balance, sender_cipher); }
Mathematically:
$$(L_0, R_0) \div (L_s, R_s) = (L_0/L_s, R_0/R_s)$$
Receiver Pending Update
#![allow(unused)] fn main() { // Add transfer amount to receiver's pending balance new_pending_balance = cipher_add(old_pending_balance, receiver_cipher); }
Mathematically:
$$(L_p, R_p) \cdot (L_r, R_r) = (L_p \cdot L_r, R_p \cdot R_r)$$
Anti-Spam Protection
Transfers are added to the receiver's pending balance rather than their main balance to prevent spam attacks. This design:
- Prevents balance corruption: Malicious actors can't modify someone's main balance
- Enables atomic proofs: Senders prove against a known balance state
- Requires explicit rollover: Receivers must claim pending transfers
The receiver later calls rollover() to merge pending transfers into their main balance.
Example Flow
// 1. Sender creates transfer
const transfer = await sender.transfer({
to: receiverPubKey,
amount: 100n,
});
// 2. Submit to contract
await signer.execute([transfer.toCalldata()]);
// 3. Receiver claims pending balance
const rollover = await receiver.rollover();
await signer.execute([rollover.toCalldata()]);
Security Considerations
Replay Protection
Each proof includes the sender's nonce and contract address in the Fiat-Shamir challenge computation, preventing proof reuse.
Range Proof Security
The 32-bit range proofs ensure:
- Transfer amounts are non-negative
- Remaining balances don't underflow
- No "money creation" attacks
Homomorphic Security
The ElGamal encryption scheme maintains semantic security even under homomorphic operations, ensuring transferred amounts remain confidential.
Auditing & Compliance
Tongo provides flexible auditing mechanisms that enable compliance without sacrificing user privacy. Through viewing keys and ex-post proving, regulators can verify transaction details while preserving confidentiality for all other parties.
Global Auditor
Concept
The Tongo contract can designate a global auditor with public key \(y_a\). All transfers are automatically encrypted for this auditor, providing a comprehensive audit trail without revealing amounts on-chain.
Auditor Encryption
For every transfer of amount \(b\), the sender creates three encryptions:
- Sender: \((L_s, R) = \text{Enc}[y_s](b, r)\) - subtracted from balance
- Receiver: \((L_r, R) = \text{Enc}[y_r](b, r)\) - added to pending
- Auditor: \((L_a, R) = \text{Enc}[y_a](b, r)\) - stored for audit
All three use the same blinding factor \(r\), proven via Pedersen commitment proofs.
Multi-Signature Auditing
For enhanced security, auditor keys can be distributed across multiple parties:
$$y_a = g^{a_1 + a_2} = g^{a_1} \cdot g^{a_2} = y_{a_1} \cdot y_{a_2}$$
Individual auditors can compute partial decryptions:
- Auditor 1: \(R^{a_1} = (g^r)^{a_1}\)
- Auditor 2: \(R^{a_2} = (g^r)^{a_2}\)
The balance is recovered by combining: \(g^b = L_a / (R^{a_1} \cdot R^{a_2})\)
This prevents any single auditor from unilaterally accessing transaction data.
Viewing Keys
Selective Disclosure
Users can optionally encrypt transfers for additional viewing keys beyond the global auditor:
#![allow(unused)] fn main() { struct Transfer { // ... other fields ... L_opt: Option<Array<(PubKey, StarkPoint)>>, // Additional viewing keys } }
Each viewing key \((y_v, L_v)\) represents:
$$L_v = g^b y_v^r$$
The sender proves each additional encryption is correctly formed using Pedersen commitment proofs.
Use Cases
- Compliance officers: Institutional oversight
- Tax authorities: Jurisdiction-specific reporting
- Internal auditing: Corporate governance
Privacy-Preserving Architecture
Viewing keys maintain privacy by:
- Only revealing amounts to authorized parties
- Not exposing transaction values on-chain
- Allowing granular access control per transaction
Ex-Post Proving
Motivation
After a transfer is completed, participants may need to prove a specific transaction detail to a third party without revealing their private keys. Ex-post proving enables this through cryptographic proofs.
Protocol
Consider a completed transfer with ciphertext \((TL, TR) = (g^{b_0} y^{r_0}, g^{r_0})\). To prove the transfer amount to a third party with public key \(\bar{y}\):
1. Revelation Phase
The sender creates new encryptions of the same amount:
- Sender encryption: \((L, R) = \text{Enc}[y](b, r)\)
- Third-party encryption: \((\bar{L}, R) = \text{Enc}[\bar{y}](b, r)\)
2. Consistency Proof
The sender proves the new encryptions contain the same amount as the original transfer:
$$\frac{TL}{L} = \left(\frac{TR}{R}\right)^x$$
This equality holds if and only if \(b_0 = b\) (the amounts match).
3. Required Proofs
The complete ex-post proof \(\pi_{\text{expost}}\) demonstrates:
- Ownership: Knowledge of \(x\) such that \(y = g^x\) (POE)
- Blinding: Knowledge of \(r\) such that \(R = g^r\) (POE)
- Sender encryption: \(L = g^b y^r\) (PED)
- Third-party encryption: \(\bar{L} = g^b \bar{y}^r\) (PED)
- Consistency: \(TL/L = (TR/R)^x\) (POE)
Mathematical Foundation
The consistency check works because:
$$\frac{TL}{L} = \frac{g^{b_0} y^{r_0}}{g^b y^r} = g^{b_0-b} y^{r_0-r}$$
$$\left(\frac{TR}{R}\right)^x = \left(\frac{g^{r_0}}{g^r}\right)^x = g^{(r_0-r)x} = y^{r_0-r}$$
These are equal only when \(b_0 = b\), proving amount consistency.
Off-Chain Verification
Ex-post proofs require no on-chain interaction:
- Transaction data is retrieved from chain state
- Proofs are generated and verified off-chain
- Only requires the original transaction hash as reference
Regulatory Compliance
AML/KYC Integration
Tongo supports various compliance frameworks:
Real-Time Monitoring
- Global auditor receives all transaction encryptions
- Automated threshold detection (encrypted amounts)
- Pattern analysis on transaction graphs
Selective Disclosure
- Users can voluntarily encrypt for compliance officers
- Jurisdiction-specific reporting requirements
- Time-limited viewing key access
Retroactive Investigation
- Ex-post proving enables transaction reconstruction
- User cooperation required for private key revelation
- Court-ordered disclosure mechanisms
Advanced Features
Threshold Auditing
Multiple auditors with threshold decryption:
$$y_a = \sum_{i=1}^n w_i \cdot y_{a_i}$$
Where \(w_i\) are threshold weights and \(t\) out of \(n\) auditors are required for decryption.
Zero-Knowledge Compliance
Prove compliance properties without revealing amounts:
- Range compliance: Prove transfer amount below threshold
- Velocity limits: Prove cumulative amounts within bounds
- Whitelist compliance: Prove recipient authorization
These advanced features demonstrate Tongo's flexibility in balancing privacy and regulatory requirements across diverse jurisdictions and use cases.
Cairo Contracts
The Tongo protocol is implemented as a Cairo smart contract on Starknet, leveraging native elliptic curve operations for efficient zero-knowledge proof verification.
Contract Architecture
Core Contract
The main Tongo contract implements the ITongo interface and manages all confidential payment operations:
#![allow(unused)] fn main() { #[starknet::interface] pub trait ITongo<TContractState> { fn fund(ref self: TContractState, fund: Fund); fn rollover(ref self: TContractState, rollover: Rollover); fn withdraw_all(ref self: TContractState, withdraw_all: WithdrawAll); fn withdraw(ref self: TContractState, withdraw: Withdraw); fn transfer(ref self: TContractState, transfer: Transfer); // State queries fn get_balance(self: @TContractState, y: PubKey) -> CipherBalance; fn get_audit(self: @TContractState, y: PubKey) -> CipherBalance; fn get_pending(self: @TContractState, y: PubKey) -> CipherBalance; fn get_nonce(self: @TContractState, y: PubKey) -> u64; fn get_state(self: @TContractState, y: PubKey) -> State; fn ERC20(self: @TContractState) -> ContractAddress; } }
Storage Structure
#![allow(unused)] fn main() { #[storage] struct Storage { balance: Map<PubKey, CipherBalance>, // Main encrypted balances audit_balance: Map<PubKey, CipherBalance>, // Auditor encrypted copies pending: Map<PubKey, CipherBalance>, // Incoming transfer buffer ae_balance: Map<PubKey, AEBalance>, // Fast decrypt hints ae_audit_balance: Map<PubKey, AEBalance>, // Fast decrypt audit hints nonce: Map<PubKey, u64>, // Replay protection } }
Events
The contract emits events for all operations to enable off-chain monitoring:
#![allow(unused)] fn main() { #[derive(Drop, starknet::Event)] struct TransferEvent { from: PubKey, to: PubKey, nonce: u64 } #[derive(Drop, starknet::Event)] struct FundEvent { to: PubKey, amount: felt252, nonce: u64 } #[derive(Drop, starknet::Event)] struct WithdrawEvent { from: PubKey, to: ContractAddress, amount: felt252, nonce: u64 } #[derive(Drop, starknet::Event)] struct RolloverEvent { account: PubKey, nonce: u64 } }
Data Structures
Core Types
#![allow(unused)] fn main() { // Public key as elliptic curve point struct PubKey { x: felt252, y: felt252 } // ElGamal ciphertext struct CipherBalance { CL: StarkPoint, // g^b * y^r (left component) CR: StarkPoint // g^r (right component) } // Authenticated encryption balance struct AEBalance { ciphertext: u512, nonce: u256 } // Complete account state struct State { balance: CipherBalance, pending: CipherBalance, audit: CipherBalance, nonce: u64, ae_balance: AEBalance, ae_audit_balance: AEBalance } }
Operation Structures
#![allow(unused)] fn main() { struct Fund { to: PubKey, amount: felt252, ae_hints: AEHints, proof: ProofOfFund } struct Transfer { from: PubKey, to: PubKey, L: StarkPoint, // Sender encryption left component L_bar: StarkPoint, // Receiver encryption left component L_audit: StarkPoint, // Auditor encryption left component R: StarkPoint, // Shared right component ae_hints: AEHints, proof: ProofOfTransfer } struct Withdraw { from: PubKey, amount: felt252, to: ContractAddress, // Starknet address to receive ERC20 ae_hints: AEHints, proof: ProofOfWithdraw } }
Verification System
Proof Verifier
The contract includes a sophisticated zero-knowledge proof verification system:
Main verification logic
verify_fund(): Validates funding proofs (POE)verify_transfer(): Validates transfer proofs (POE + PED + RAN)verify_withdraw(): Validates withdrawal proofs (POE + RAN)verify_range(): Bit-decomposition range proofsverify_pedersen(): Pedersen commitment proofs
Cryptographic utilities
- Point arithmetic operations
- Hash computations for Fiat-Shamir
- Curve parameter constants
Proof data structures
#![allow(unused)] fn main() { struct ProofOfFund { Ax: StarkPoint, sx: felt252 } struct ProofOfTransfer { ownership: ProofOfOwnership, blinding: ProofOfOwnership, sender_ped: ProofOfPedersen, receiver_ped: ProofOfPedersen, audit_ped: ProofOfPedersen, amount_range: ProofOfRange, remaining_range: ProofOfRange } }
Operations
1. Fund Operation
Converts ERC20 tokens to encrypted balances:
#![allow(unused)] fn main() { fn fund(ref self: TContractState, fund: Fund) { // Verify proof of ownership verify_fund(fund.proof, fund.to, fund.amount); // Create encrypted balance let cipher = encrypt_balance(fund.amount, fund.to); // Update storage self.balance.write(fund.to, cipher_add(self.balance.read(fund.to), cipher)); self.nonce.write(fund.to, self.nonce.read(fund.to) + 1); } }
2. Transfer Operation
Performs confidential transfers between accounts:
#![allow(unused)] fn main() { fn transfer(ref self: TContractState, transfer: Transfer) { // Verify comprehensive transfer proof verify_transfer(transfer.proof, /* public inputs */); // Update sender balance (subtract) let sender_cipher = CipherBalance { L: transfer.L, R: transfer.R }; let old_balance = self.balance.read(transfer.from); self.balance.write(transfer.from, cipher_subtract(old_balance, sender_cipher)); // Update receiver pending (add) let receiver_cipher = CipherBalance { L: transfer.L_rec, R: transfer.R }; let old_pending = self.pending.read(transfer.to); self.pending.write(transfer.to, cipher_add(old_pending, receiver_cipher)); // Update auditor balance let audit_cipher = CipherBalance { L: transfer.L_audit, R: transfer.R }; let old_audit = self.audit_balance.read(transfer.from); self.audit_balance.write(transfer.from, cipher_subtract(old_audit, audit_cipher)); } }
3. Rollover Operation
Merges pending transfers into main balance:
#![allow(unused)] fn main() { fn rollover(ref self: TContractState, rollover: Rollover) { // Verify ownership verify_ownership(rollover.proof, rollover.to); // Move pending to balance let pending = self.pending.read(rollover.to); let balance = self.balance.read(rollover.to); self.balance.write(rollover.to, cipher_add(balance, pending)); // Clear pending self.pending.write(rollover.to, CipherBalance { L: zero_point(), R: zero_point() }); } }
Security Features
Anti-Spam Protection
- Pending balance system: Incoming transfers go to separate pending storage
- Explicit rollover: Users must claim pending transfers
- Nonce protection: Prevents replay attacks
Range Proof Security
- 32-bit decomposition: Ensures amounts are in valid range [0, 2³²)
- Bit verification: Each bit proven to be 0 or 1 using OR proofs
- Overflow prevention: Prevents negative balances and money creation
Cryptographic Guarantees
- Discrete log assumption: Security based on Stark curve
- Fiat-Shamir: Makes interactive proofs non-interactive
- Context binding: Proofs tied to specific transactions
Deployment
Configuration
#![allow(unused)] fn main() { // Contract constructor fn constructor( ref self: ContractState, strk_address: ContractAddress, // ERC20 token to wrap view: PubKey // Global auditor public key ) { self.strk_address.write(strk_address); self.view.write(view); } }
Testing
Comprehensive test suite covers:
- Unit tests: Individual function verification
- Integration tests: Full operation flows
- Proof tests: ZK proof generation and verification
- Edge cases: Error conditions and boundary values
# Run all tests
scarb test
The Cairo implementation provides a secure, efficient foundation for confidential payments on Starknet.
Starknet Homomorphic Encryption (SHE)
The SHE library provides low-level cryptographic primitives for ElGamal encryption and zero-knowledge proof generation over the Stark elliptic curve. It serves as the foundation for all cryptographic operations in Tongo.
Dual Implementation
SHE is implemented in two languages for different environments:
TypeScript Implementation
Package: @fatsolutions/she
Version: 0.1.0
Use Case: Client-side proof generation and encryption
Location: /packages/typescript
The TypeScript implementation provides:
- Off-chain proof generation (fund, transfer, withdraw, rollover)
- ElGamal encryption and decryption
- All zero-knowledge protocols (POE, bit, range, same encryption)
- Used by the Tongo SDK for creating operations
Cairo Implementation
Package: she
Version: 0.3.0
Use Case: On-chain proof verification
Location: /packages/cairo
The Cairo implementation provides:
- Smart contract proof verification
- ElGamal ciphertext operations
- Optimized for Starknet VM execution
- Used by Tongo contracts for validating proofs
Architecture
Client Application
|
+-- Tongo SDK (@fatsolutions/tongo-sdk)
|
+-- SHE TypeScript (@fatsolutions/she)
- Proof generation
- Encryption/decryption
- Balance operations
|
| Submit transaction
v
Starknet Network
|
+-- Tongo Contract (Cairo)
|
+-- SHE Cairo (she)
- Proof verification
- Cipher operations
- Balance validation
Core Primitives
Both implementations provide the same cryptographic primitives:
ElGamal Encryption
Encrypt balances: \(\text{Enc}[y](b, r) = (g^b y^r, g^r)\)
TypeScript: Proof generation for funding, transfers Cairo: Ciphertext arithmetic and validation
Zero-Knowledge Proofs
POE (Proof of Exponent)
Prove \(y = g^x\) without revealing \(x\)
TypeScript: Generate proofs Cairo: Verify proofs on-chain
Range Proofs
Prove \(b \in [0, 2^{32})\) using bit decomposition
TypeScript: ~500ms generation (32-bit) Cairo: ~260K Cairo steps verification
Same Encryption
Prove two ciphertexts encrypt the same value
TypeScript: Generate multi-party proofs Cairo: Verify consistency on-chain
TypeScript API
Installation
npm install @fatsolutions/she
Basic Usage
import { GENERATOR as g, proveFund, decipherBalance } from "@fatsolutions/she";
// Generate a fund proof
const { inputs, proof, newBalance } = proveFund(
privateKey,
amount,
currentBalance,
currentCipher,
nonce
);
// Decrypt a balance
const balance = decipherBalance(privateKey, L, R);
Available Protocols
import {
proveFund,
proveTransfer,
proveWithdraw,
proveRollover,
proveRagequit,
prove_audit,
} from "@fatsolutions/she";
For detailed protocol documentation, see the SHE Cryptography section.
Cairo API
Contract Integration
use she::protocols::poe::verify as verify_poe;
use she::protocols::ElGamal::verify as verify_elgamal;
use she::protocols::range::verify as verify_range;
// Verify a fund proof
let valid = verify_poe(y, g, A, c, s);
Available Modules
she::protocols::poe- Proof of Exponentshe::protocols::poe2- Double exponent proofsshe::protocols::poeN- N-exponent proofsshe::protocols::bit- Bit proofs (OR construction)she::protocols::range- Range proofs via bit decompositionshe::protocols::ElGamal- ElGamal encryption proofsshe::protocols::SameEncryption- Same message proofsshe::protocols::SameEncryptionUnknownRandom- Without known randomness
Performance
TypeScript (Client-Side)
| Operation | Time |
|---|---|
| Fund proof | ~50ms |
| Transfer proof | 2-3s |
| Withdraw proof | 1-2s |
| Rollover proof | ~10ms |
| Decryption (with hint) | < 1ms |
| Decryption (brute-force 1M) | ~100ms |
Cairo (On-Chain)
| Operation | Cairo Steps |
|---|---|
| POE verification | ~2,500 |
| ElGamal verification | ~5,000 |
| Bit proof verification | ~8,000 |
| Range proof (32-bit) | ~260,000 |
| Full transfer verification | ~300,000 |
Repository Structure
she/
├── packages/
│ ├── typescript/ # TypeScript implementation
│ │ ├── src/
│ │ │ ├── protocols/ # ZK protocols
│ │ │ ├── types.ts # Type definitions
│ │ │ ├── constants.ts # Curve parameters
│ │ │ └── utils.ts # Crypto utilities
│ │ └── package.json
│ │
│ └── cairo/ # Cairo implementation
│ ├── src/
│ │ ├── protocols/ # ZK protocol verifiers
│ │ ├── lib.cairo # Main library
│ │ └── utils.cairo # Cairo utilities
│ └── Scarb.toml
│
├── pnpm-workspace.yaml
└── README.md
Development
TypeScript
cd packages/typescript
pnpm install
pnpm build
pnpm test
Cairo
cd packages/cairo
scarb build
scarb test
Next Steps
For cryptographic protocol details:
For SDK usage:
SHE Cryptography Library
The Starknet Homomorphic Encryption (SHE) library provides low-level cryptographic primitives for ElGamal encryption and zero-knowledge proof systems over the Stark elliptic curve.
Overview
SHE is the cryptographic foundation of Tongo, implementing:
- ElGamal Encryption: Additively homomorphic encryption over elliptic curves
- Zero-Knowledge Proofs: Sigma protocols for proving various statements
- Range Proofs: Proving values are within valid ranges using bit decomposition
- Fiat-Shamir Transform: Non-interactive proofs using Poseidon hash
Package Information
- Package:
@fatsolutions/she - Version: 0.1.0
- License: Apache-2.0
- Repository: github.com/fatlabsxyz/she
Key Features
Homomorphic Encryption
ElGamal encryption over the Stark curve enables:
- Encrypted balance storage
- Homomorphic addition and subtraction
- Privacy-preserving balance updates
Sigma Protocols
Implemented zero-knowledge proofs:
- POE: Proof of Exponent (knowledge of discrete log)
- POE2: Proof of double exponent
- POEN: Proof of N exponents
- Bit: Proof that committed value is 0 or 1
- Range: Proof that value is in [0, 2^n)
- SameEncryption: Proof that two ciphertexts encrypt the same value
Fiat-Shamir Transform
All proofs are non-interactive using:
- Poseidon hash function (Starknet-native)
- Challenge computation from commitment points
- Prefix-based proof binding
Core Protocols
1. ElGamal Encryption
Proves that (L, R) = (g^b * y^r, g^r) is a well-formed ElGamal ciphertext.
Protocol: Coupled POE + POE2
Use Case: Funding operations, proving correct encryption
2. Proof of Exponent (POE)
Proves knowledge of x such that y = g^x.
Protocol: Standard Schnorr protocol
Use Case: Ownership proofs, authentication
3. Bit Proofs
Proves that a commitment V = g^b * h^r has b ∈ {0, 1}.
Protocol: OR proof using simulated POE
Use Case: Range proof building blocks
4. Range Proofs
Proves that a value b is in [0, 2^n) using bit decomposition.
Protocol: Composition of n bit proofs
Use Case: Preventing negative balances, overflow protection
5. Same Encryption
Proves two ciphertexts (L1, R1) and (L2, R2) encrypt the same amount for different public keys.
Protocol: Two coupled ElGamal proofs with shared message
Use Case: Multi-party transfers, auditor encryption
Cryptographic Assumptions
SHE's security relies on:
- Discrete Logarithm Problem (DLP): Hard to find
xgiveng^x - Computational Diffie-Hellman: Hard to compute
g^(xy)fromg^xandg^y - Stark Curve Security: 256-bit security level
Performance
Typical performance on modern hardware:
| Operation | Time |
|---|---|
| Random scalar generation | < 1ms |
| Point multiplication | < 1ms |
| POE proof generation | ~10ms |
| POE verification | ~1ms |
| Bit proof generation | ~20ms |
| Range proof (32-bit) | ~500ms |
| ElGamal encryption | < 1ms |
| ElGamal decryption (brute force) | 1-100ms |
Next Steps
- Learn about ElGamal Encryption
- Understand Zero-Knowledge Proofs
- Explore POE Protocol
- Study Range Proofs
ElGamal Encryption
ElGamal encryption over elliptic curves provides the foundation for Tongo's confidential balances.
Mathematical Definition
The ElGamal encryption scheme over the Stark curve is defined as:
$$\text{Enc}[y](b, r) = (L, R) = (g^b \cdot y^r, g^r)$$
Where:
- \(g\) is the Stark curve generator
- \(y = g^x\) is the recipient's public key
- \(b\) is the message (balance amount)
- \(r\) is a random blinding factor
Properties
Additive Homomorphism
Given two ciphertexts encrypting \(b_1\) and \(b_2\):
$$\text{Enc}[y](b_1, r_1) \cdot \text{Enc}[y](b_2, r_2) = \text{Enc}[y](b_1 + b_2, r_1 + r_2)$$
This allows adding encrypted balances without decryption:
$$(L_1, R_1) \cdot (L_2, R_2) = (L_1 \cdot L_2, R_1 \cdot R_2)$$
Semantic Security
Each encryption uses fresh randomness \(r\), ensuring:
- Same amount encrypted twice produces different ciphertexts
- Ciphertexts reveal no information about the message
- Security based on Decisional Diffie-Hellman assumption
Decryption
To decrypt a ciphertext \((L, R)\) with private key \(x\):
- Compute \(g^b = L / R^x = L / (g^r)^x = (g^b \cdot y^r) / (g^{rx}) = g^b\)
- Solve discrete logarithm: find \(b\) such that \(g^b\) equals the result
Since \(b\) is bounded (e.g., \([0, 2^{32})\)), this can be computed efficiently using:
- Brute force: \(O(b)\) time
- Baby-step Giant-step: \(O(\sqrt{b})\) time and space
Zero-Knowledge Proof
Statement
Prove that \((L, R)\) is a well-formed ElGamal ciphertext:
$${(L, R, g, y; b, r) : L = g^b \cdot y^r \land R = g^r}$$
Protocol
The proof consists of two coupled sub-proofs:
- POE for \(R\): Prove \(R = g^r\) (knowledge of \(r\))
- POE2 for \(L\): Prove \(L = g^b \cdot y^r\) (knowledge of \(b\) and \(r\))
Prover (knows \(b\), \(r\)):
Choose random kb, kr
Compute AL = g^kb · y^kr
Compute AR = g^kr
Compute challenge c = Hash(prefix, AL, AR)
Compute sb = kb + c·b
Compute sr = kr + c·r
Send: (AL, AR, sb, sr)
Verifier (checks):
Recompute c = Hash(prefix, AL, AR)
Check: g^sr == AR · R^c [POE for R]
Check: g^sb · y^sr == AL · L^c [POE2 for L]
Implementation
Encryption (TypeScript)
import { ProjectivePoint } from "@scure/starknet";
function encrypt(
message: bigint,
publicKey: ProjectivePoint,
generator: ProjectivePoint,
randomness: bigint
): { L: ProjectivePoint; R: ProjectivePoint } {
const L = generator.multiply(message).add(publicKey.multiply(randomness));
const R = generator.multiply(randomness);
return { L, R };
}
Decryption (TypeScript)
function decrypt(
L: ProjectivePoint,
R: ProjectivePoint,
secretKey: bigint,
maxValue: bigint = 1000000n
): bigint {
// Compute g^b = L / R^x
const gb = L.subtract(R.multiply(secretKey));
// Brute force discrete log
const g = ProjectivePoint.BASE;
for (let i = 0n; i <= maxValue; i++) {
if (g.multiply(i).equals(gb)) {
return i;
}
}
throw new Error('Decryption failed: value not in range');
}
Proof Generation (TypeScript)
import * as ElGamal from "@fatsolutions/she/protocols";
function proveElGamal(
message: bigint,
random: bigint,
g: ProjectivePoint,
publicKey: ProjectivePoint,
prefix: bigint
): { inputs: ElGamalInputs; proof: ElGamalProof } {
return ElGamal.prove(message, random, g, publicKey, prefix);
}
Proof Verification (TypeScript)
function verifyElGamal(
inputs: ElGamalInputs,
proof: ElGamalProofWithPrefix
): boolean {
return ElGamal.verify_with_prefix(inputs, proof);
}
Security Analysis
Soundness
An adversary cannot forge a valid proof without knowing \(b\) and \(r\) because:
- POE for \(R\) requires knowledge of \(r\)
- POE2 for \(L\) requires knowledge of both \(b\) and \(r\)
- Challenge binding prevents proof manipulation
Zero-Knowledge
The proof reveals nothing about \(b\) or \(r\) beyond the public statement because:
- Commitments are perfectly hiding
- Responses are uniformly random mod curve order
- Simulation is indistinguishable from real proofs
Completeness
Honest provers always produce valid proofs because:
- Verification equations hold for correctly computed responses
- Challenge computation is deterministic
- All arithmetic is mod curve order
Cairo Implementation
The Cairo version in /packages/cairo/src/protocols/ElGamal.cairo provides on-chain verification:
// Simplified Cairo verification
fn verify_elgamal(
L: EcPoint,
R: EcPoint,
g: EcPoint,
y: EcPoint,
AL: EcPoint,
AR: EcPoint,
c: felt252,
sb: felt252,
sr: felt252
) -> bool {
// Verify R = g^r (POE)
let lhs_r = ec_mul(g, sr);
let rhs_r = ec_add(AR, ec_mul(R, c));
assert(lhs_r == rhs_r);
// Verify L = g^b · y^r (POE2)
let lhs_l = ec_add(ec_mul(g, sb), ec_mul(y, sr));
let rhs_l = ec_add(AL, ec_mul(L, c));
assert(lhs_l == rhs_l);
true
}
Cost Analysis
TypeScript (Off-Chain)
- Proof Generation: ~10-15ms
- Proof Verification: ~1-2ms
- 2 EC multiplications + 1 EC addition
Cairo (On-Chain)
- Verification: ~5,000 Cairo steps
- 2
ec_muloperations - 2
ec_addoperations
Next Steps
Zero-Knowledge Proofs
SHE implements a comprehensive suite of zero-knowledge proofs based on sigma protocols, made non-interactive using the Fiat-Shamir transform.
Sigma Protocols
All SHE proofs follow the sigma protocol pattern:
- Commitment: Prover commits to random values
- Challenge: Verifier provides challenge (computed via Fiat-Shamir)
- Response: Prover responds using witness and challenge
Fiat-Shamir Transform
Instead of interactive challenges, SHE computes challenges deterministically:
$$c = \text{Hash}(\text{prefix}, A_1, A_2, \ldots, A_n)$$
Where:
prefixbinds the proof to a specific context (nonce, contract address, etc.)- \(A_i\) are the commitment points
Hashis Poseidon (Starknet-native hash function)
Implementation
import { poseidonHashMany } from "@scure/starknet";
function compute_challenge(prefix: bigint, commitments: ProjectivePoint[]): bigint {
const arr: bigint[] = [prefix];
commitments.forEach(commit => {
const { x, y } = commit.toAffine();
arr.push(x, y);
});
return poseidonHashMany(arr) % CURVE_ORDER;
}
Proof Composition
Complex statements are built from basic protocols:
ElGamal Proof
= POE (for R) + POE2 (for L)
Transfer Proof
= POE (ownership) + POE (blinding) + Multiple ElGamal proofs + Range proofs
Same Encryption
= ElGamal proof 1 + ElGamal proof 2 (with shared message response)
Security Properties
Soundness
An adversary cannot forge proofs without knowing the witness because:
- Discrete logarithm assumption
- Challenge binding prevents manipulation
- Fiat-Shamir security (ROM model)
Zero-Knowledge
Proofs reveal nothing beyond the statement because:
- Responses are uniformly random
- Simulation is perfect (honest-verifier ZK)
- Fiat-Shamir preserves zero-knowledge
Non-Malleability
Proofs cannot be modified or replayed because:
- Challenge includes all public inputs
- Prefix binds to specific context
- Response depends on full transcript
Common Patterns
OR Proofs
Prove "bit is 0 OR bit is 1" without revealing which:
Real proof: Generate honestly for true case
Simulated proof: Fake proof for false case
Challenge split: c = c_real ⊕ c_simulated
AND Proofs
Prove multiple statements simultaneously:
Single challenge: c used for all sub-proofs
Independent responses: s_i for each statement
Batch verification: Check all equations
Next Steps
Proof of Exponent (POE)
The POE protocol proves knowledge of a discrete logarithm without revealing it.
Statement
Prove knowledge of \(x\) such that:
$$y = g^x$$
Where:
- \(g\) is a known generator point
- \(y\) is a known public point
- \(x\) is the secret witness
Protocol (Interactive)
Prover (knows \(x\)):
1. Choose random k
2. Compute A = g^k
3. Send A to verifier
4. Receive challenge c from verifier
5. Compute s = k + c·x (mod curve_order)
6. Send s to verifier
Verifier:
1. Receive A
2. Choose random c
3. Send c to prover
4. Receive s
5. Check: g^s == A · y^c
Verification Equation
The verification equation holds because:
$$g^s = g^{k + c \cdot x} = g^k \cdot g^{c \cdot x} = A \cdot (g^x)^c = A \cdot y^c$$
Non-Interactive (Fiat-Shamir)
Instead of interactive challenge, compute:
$$c = \text{Hash}(\text{prefix}, A)$$
This makes the proof non-interactive and bindable to context.
Implementation
TypeScript
import { ProjectivePoint } from "@scure/starknet";
import { compute_challenge, compute_s, generateRandom } from "./utils";
interface PoeInputs {
y: ProjectivePoint;
g: ProjectivePoint;
}
interface PoeProofWithPrefix {
A: ProjectivePoint;
prefix: bigint;
s: bigint;
}
// Prove knowledge of x such that y = g^x
function prove(
x: bigint,
g: ProjectivePoint,
prefix: bigint
): { inputs: PoeInputs; proof: PoeProofWithPrefix } {
const y = g.multiply(x);
const k = generateRandom();
const A = g.multiply(k);
const c = compute_challenge(prefix, [A]);
const s = compute_s(k, x, c);
return {
inputs: { y, g },
proof: { A, prefix, s }
};
}
// Verify POE proof
function verify(
y: ProjectivePoint,
g: ProjectivePoint,
A: ProjectivePoint,
c: bigint,
s: bigint
): boolean {
const lhs = g.multiply(s);
const rhs = A.add(y.multiply(c));
return lhs.equals(rhs);
}
Cairo
// Simplified Cairo verification
fn verify_poe(
y: EcPoint,
g: EcPoint,
A: EcPoint,
c: felt252,
s: felt252
) -> bool {
let lhs = ec_mul(g, s);
let rhs = ec_add(A, ec_mul(y, c));
lhs == rhs
}
Security
Soundness
An adversary cannot forge a proof without knowing \(x\) because:
- Finding \(s\) without \(x\) requires solving DLP
- Challenge binding prevents manipulation
- Response \(s\) is tied to specific \(A\) and \(c\)
Zero-Knowledge
The proof reveals nothing about \(x\) because:
- \(A\) is perfectly hiding (random \(k\))
- \(s\) is uniformly distributed mod curve order
- Simulator can produce indistinguishable transcripts
Proof of Zero-Knowledge
A simulator can generate valid-looking transcripts without knowing \(x\):
1. Choose random s and c
2. Compute A = g^s / y^c
3. Output (A, c, s)
This produces transcripts indistinguishable from real proofs.
Cost Analysis
Prover Complexity
- 2 scalar multiplications (k generation, s computation)
- 1 hash computation
- Time: ~10ms
Verifier Complexity
- 2 EC multiplications (g^s, y^c)
- 1 EC addition (A + y^c)
- 1 hash computation
- Time: ~1-2ms
On-Chain (Cairo)
- 2
ec_muloperations - 1
ec_addoperation - ~2,500 Cairo steps
Usage in Tongo
POE is used for:
- Ownership Proofs: Prove account ownership during fund/transfer/withdraw
- Blinding Factor: Prove knowledge of randomness in transfers
- Building Block: Component of more complex proofs (ElGamal, transfer)
Variants
POE2
Proves \(y = g_1^x \cdot g_2^z\) (two generators, two witnesses)
POEN
Proves \(y = \prod_{i=1}^{n} g_i^{x_i}\) (N generators, N witnesses)
Both are implemented in SHE for specific use cases.
Next Steps
- Bit Proofs - OR proofs for binary values
- Range Proofs - Composition of bit proofs
- ElGamal Encryption - Uses POE as building block
Bit Proofs
Bit proofs demonstrate that a committed value is either 0 or 1 using OR proof construction.
Statement
Prove that a commitment \(V = g^b \cdot h^r\) has \(b \in {0, 1}\):
$${(V, g, h; b, r) : V = g^b \cdot h^r \land (b = 0 \lor b = 1)}$$
OR Proof Construction
The proof works by showing:
- If \(b = 0\): Then \(V = h^r\) (prove with POE for \(r\))
- If \(b = 1\): Then \(V/g = h^r\) (prove with POE for \(r\))
Without revealing which case is true!
Protocol
Prover (knows \(b \in {0,1}\) and \(r\)):
For \(b = 0\):
Real proof for V = h^r:
k0 ← random
A0 = h^k0
Simulated proof for V/g = h^r:
s1, c1 ← random
A1 = h^s1 / (V/g)^c1
Combine:
c = Hash(prefix, A0, A1)
c0 = c ⊕ c1
s0 = k0 + c0·r
For \(b = 1\):
Simulated proof for V = h^r:
s0, c0 ← random
A0 = h^s0 / V^c0
Real proof for V/g = h^r:
k1 ← random
A1 = h^k1
Combine:
c = Hash(prefix, A0, A1)
c1 = c ⊕ c0
s1 = k1 + c1·r
Verifier:
Recompute c = Hash(prefix, A0, A1)
Compute c1 = c ⊕ c0
Check: h^s0 == A0 · V^c0 [POE for b=0 case]
Check: h^s1 == A1 · (V/g)^c1 [POE for b=1 case]
Simulated POE
Key technique: Generate fake proof for false case:
function simulatePOE(
y: ProjectivePoint,
gen: ProjectivePoint
): { A: ProjectivePoint; c: bigint; s: bigint } {
const s = generateRandom();
const c = generateRandom();
const A = gen.multiply(s).subtract(y.multiply(c));
return { A, c, s };
}
This produces a valid-looking transcript for any statement!
Implementation
TypeScript
function proveBit(
bit: 0 | 1,
random: bigint,
g: ProjectivePoint,
h: ProjectivePoint,
prefix: bigint
): { inputs: BitInputs; proof: BitProofWithPrefix } {
if (bit === 0) {
// Real proof for V = h^r
const V = h.multiply(random);
const V1 = V.subtract(g);
// Simulate proof for V/g = h^r
const { A: A1, c: c1, s: s1 } = simulatePOE(V1, h);
// Real proof for V = h^r
const k = generateRandom();
const A0 = h.multiply(k);
const c = compute_challenge(prefix, [A0, A1]);
const c0 = c ^ c1;
const s0 = compute_s(k, random, c0);
return {
inputs: { V, g, h },
proof: { A0, A1, prefix, c0, s0, s1 }
};
} else {
// Similar for bit = 1, but swap real and simulated
// ...
}
}
Verification
function verifyBit(
V: ProjectivePoint,
g: ProjectivePoint,
h: ProjectivePoint,
A0: ProjectivePoint,
A1: ProjectivePoint,
c: bigint,
c0: bigint,
s0: bigint,
s1: bigint
): boolean {
const c1 = c ^ c0;
// Verify b=0 case
if (!poe_verify(V, h, A0, c0, s0)) {
return false;
}
// Verify b=1 case
const V1 = V.subtract(g);
if (!poe_verify(V1, h, A1, c1, s1)) {
return false;
}
return true;
}
Cost Analysis
Prover
- 1 real POE proof
- 1 simulated POE proof
- 1 hash computation
- Time: ~20ms
Verifier
- 2 POE verifications
- 1 hash computation
- 4 EC multiplications
- 3 EC additions
- Time: ~2-3ms
Cairo
- 4
ec_muloperations - 3
ec_addoperations - ~8,000 Cairo steps per bit
Use in Range Proofs
Bit proofs are the building blocks for range proofs:
For a 32-bit value \(b = \sum_{i=0}^{31} b_i \cdot 2^i\):
- Commit to each bit: \(V_i = g^{b_i} \cdot h^{r_i}\)
- Prove each \(b_i \in {0, 1}\) using bit proof
- Verify commitment consistency: \(\prod_{i=0}^{31} V_i^{2^i} = g^b \cdot h^r\)
Next Steps
- Range Proofs - Composition of bit proofs
- POE Protocol - Understanding the base protocol
Range Proofs
Range proofs demonstrate that a value lies within a specific range using bit decomposition.
Statement
Prove that a value \(b\) satisfies \(b \in [0, 2^n)\) where \(n\) is the bit length:
$${(V, g, h; b, r) : V = g^b \cdot h^r \land b \in [0, 2^n)}$$
Binary Decomposition
Any value \(b < 2^n\) can be written as:
$$b = \sum_{i=0}^{n-1} b_i \cdot 2^i$$
Where each \(b_i \in {0, 1}\).
Protocol
1. Bit Commitments
For each bit \(b_i\), create a commitment with independent randomness \(r_i\):
$$V_i = g^{b_i} \cdot h^{r_i}$$
2. Bit Proofs
Generate a bit proof for each \(V_i\) showing \(b_i \in {0, 1}\).
3. Consistency Check
The verifier computes:
$$V_{\text{total}} = \prod_{i=0}^{n-1} V_i^{2^i} = \prod_{i=0}^{n-1} (g^{b_i} \cdot h^{r_i})^{2^i}$$
$$= g^{\sum_{i=0}^{n-1} b_i \cdot 2^i} \cdot h^{\sum_{i=0}^{n-1} r_i \cdot 2^i} = g^b \cdot h^r$$
If all bit proofs verify and \(V_{\text{total}} = V\), then \(b \in [0, 2^n)\).
Implementation
TypeScript
interface RangeInputs {
g: ProjectivePoint;
h: ProjectivePoint;
bit_size: number;
commitments: ProjectivePoint[]; // V_0, V_1, ..., V_{n-1}
}
interface RangeProof {
proofs: BitProofWithPrefix[]; // One bit proof per commitment
}
function proveRange(
b: bigint,
bit_size: number,
g: ProjectivePoint,
h: ProjectivePoint,
prefix: bigint
): { inputs: RangeInputs; proof: RangeProof; r: bigint } {
// Convert to binary
const bits = b
.toString(2)
.padStart(bit_size, '0')
.split('')
.map(Number)
.reverse(); // Little-endian
const commitments: ProjectivePoint[] = [];
const bitProofs: BitProofWithPrefix[] = [];
let r = 0n;
// Generate bit commitments and proofs
for (let i = 0; i < bit_size; i++) {
const r_i = generateRandom();
const { inputs, proof } = proveBit(
bits[i] as (0 | 1),
r_i,
g,
h,
prefix + BigInt(i)
);
commitments.push(inputs.V);
bitProofs.push(proof);
r = (r + r_i * (2n ** BigInt(i))) % CURVE_ORDER;
}
return {
inputs: { g, h, bit_size, commitments },
proof: { proofs: bitProofs },
r
};
}
Verification
function verifyRange(
inputs: RangeInputs,
proof: RangeProof
): ProjectivePoint | false {
const { g, h, bit_size, commitments } = inputs;
const { proofs } = proof;
// Verify each bit proof
let V_total = ProjectivePoint.ZERO;
for (let i = 0; i < bit_size; i++) {
const pow = 2n ** BigInt(i);
const V_i = commitments[i];
const proof_i = proofs[i];
if (!verifyBit({ V: V_i, g, h }, proof_i)) {
return false;
}
V_total = V_total.add(V_i.multiply(pow));
}
return V_total; // Should equal g^b · h^r
}
Cost Analysis
For n-bit range proof:
Prover
- n bit proof generations
- n random scalars
- Time: ~n × 20ms (640ms for 32-bit)
Verifier
- n bit proof verifications
- Consistency check
- Time: ~n × 2ms + 5ms (~70ms for 32-bit)
Cairo (On-Chain)
- n bit verifications
- Weighted commitment sum
- ~n × 8K + 5K Cairo steps (~260K for 32-bit)
Optimizations
Precomputed Tables
Cache generator multiples:
const gTable = g.precomputeWindow(8);
const hTable = h.precomputeWindow(8);
Batch Verification
Verify multiple range proofs together when possible.
Smaller Ranges
Use smaller bit sizes when possible:
- 16-bit: ~160ms proving, ~35ms verification
- 24-bit: ~480ms proving, ~50ms verification
- 32-bit: ~640ms proving, ~70ms verification
Usage in Tongo
Range proofs are used to ensure:
- Transfer amounts: \(b \in [0, 2^{32})\)
- Remaining balances: \(b_{\text{after}} = b_{\text{before}} - b_{\text{transfer}} \in [0, 2^{32})\)
- Withdrawal amounts: \(b \in [0, 2^{32})\)
- No negative balances: Prevents underflow attacks
Next Steps
- Bit Proofs - Understanding the building blocks
- POE Protocol - Base proof system
- ElGamal Encryption - Complete encryption system
Same Encryption Proof
Proves that two ciphertexts for different public keys encrypt the same value.
Statement
Prove that \((L_1, R_1)\) and \((L_2, R_2)\) encrypt the same message \(b\) for different keys \(y_1\) and \(y_2\):
$${(L_1, R_1, L_2, R_2, g, y_1, y_2; b, r_1, r_2) : L_1 = g^b \cdot y_1^{r_1} \land R_1 = g^{r_1} \land L_2 = g^b \cdot y_2^{r_2} \land R_2 = g^{r_2}}$$
Use Case
This proof is critical for Tongo transfers where the same amount must be:
- Encrypted for the sender (to subtract from balance)
- Encrypted for the receiver (to add to pending)
- Encrypted for the auditor (for compliance)
All three must encrypt the same \(b\) but use different randomness for each public key.
Protocol
Prover (knows \(b\), \(r_1\), \(r_2\)):
Choose random kb, kr1, kr2
Compute AL1 = g^kb · y1^kr1
Compute AR1 = g^kr1
Compute AL2 = g^kb · y2^kr2
Compute AR2 = g^kr2
Compute c = Hash(prefix, AL1, AR1, AL2, AR2)
Compute sb = kb + c·b
Compute sr1 = kr1 + c·r1
Compute sr2 = kr2 + c·r2
Send: (AL1, AR1, AL2, AR2, sb, sr1, sr2)
Verifier:
Recompute c = Hash(prefix, AL1, AR1, AL2, AR2)
Check ElGamal proof 1: g^sb · y1^sr1 == AL1 · L1^c AND g^sr1 == AR1 · R1^c
Check ElGamal proof 2: g^sb · y2^sr2 == AL2 · L2^c AND g^sr2 == AR2 · R2^c
Key Insight
Both proofs use the same \(s_b\) response! This proves:
- Both ciphertexts use the same message \(b\)
- Without revealing \(b\)
- While using independent randomness \(r_1 \neq r_2\)
Implementation
TypeScript
interface SameEncryptionInputs {
L1: ProjectivePoint;
R1: ProjectivePoint;
L2: ProjectivePoint;
R2: ProjectivePoint;
g: ProjectivePoint;
y1: ProjectivePoint;
y2: ProjectivePoint;
}
interface SameEncryptionProofWithPrefix {
AL1: ProjectivePoint;
AR1: ProjectivePoint;
AL2: ProjectivePoint;
AR2: ProjectivePoint;
prefix: bigint;
sb: bigint;
sr1: bigint;
sr2: bigint;
}
function proveSameEncryption(
g: ProjectivePoint,
y1: ProjectivePoint,
y2: ProjectivePoint,
message: bigint,
random1: bigint,
random2: bigint,
prefix: bigint
): { inputs: SameEncryptionInputs; proof: SameEncryptionProofWithPrefix } {
// Create ciphertexts
const L1 = g.multiply(message).add(y1.multiply(random1));
const R1 = g.multiply(random1);
const L2 = g.multiply(message).add(y2.multiply(random2));
const R2 = g.multiply(random2);
// Generate random values for commitments
const kb = generateRandom();
const kr1 = generateRandom();
const kr2 = generateRandom();
// Compute commitments
const AL1 = g.multiply(kb).add(y1.multiply(kr1));
const AR1 = g.multiply(kr1);
const AL2 = g.multiply(kb).add(y2.multiply(kr2));
const AR2 = g.multiply(kr2);
// Compute challenge
const c = compute_challenge(prefix, [AL1, AR1, AL2, AR2]);
// Compute responses (note: shared sb!)
const sb = compute_s(kb, message, c);
const sr1 = compute_s(kr1, random1, c);
const sr2 = compute_s(kr2, random2, c);
return {
inputs: { L1, R1, L2, R2, g, y1, y2 },
proof: { AL1, AR1, AL2, AR2, prefix, sb, sr1, sr2 }
};
}
Verification
function verifySameEncryption(
inputs: SameEncryptionInputs,
proof: SameEncryptionProofWithPrefix
): boolean {
const { L1, R1, L2, R2, g, y1, y2 } = inputs;
const { AL1, AR1, AL2, AR2, prefix, sb, sr1, sr2 } = proof;
const c = compute_challenge(prefix, [AL1, AR1, AL2, AR2]);
// Verify first ElGamal proof
if (!verifyElGamal(L1, R1, g, y1, AL1, AR1, c, sb, sr1)) {
return false;
}
// Verify second ElGamal proof
if (!verifyElGamal(L2, R2, g, y2, AL2, AR2, c, sb, sr2)) {
return false;
}
return true;
}
Security Analysis
Soundness
Adversary cannot forge proof without knowing \(b\) because:
- Requires valid ElGamal proofs for both ciphertexts
- Shared \(s_b\) forces same message value
- Challenge binding prevents manipulation
Zero-Knowledge
Proof reveals nothing about \(b\) or randomness because:
- Commitments are perfectly hiding
- Responses are uniformly random
- Can be simulated without witness
Cost Analysis
Prover
- 2 ElGamal proof generations
- Time: ~20-30ms
Verifier
- 2 ElGamal proof verifications
- 10 EC multiplications
- 6 EC additions
- Time: ~3-4ms
Cairo
- ~10,000 Cairo steps
Variants
Same Encryption Unknown Random
Proves same encryption when randomness is not known to prover.
Use Case: Ex-post proofs where original randomness was lost.
See sameEncryptionUnknownRandom.ts for implementation.
Usage in Tongo
Same Encryption proofs are used in:
- Transfers: Prove amount encrypted for sender, receiver, and auditor is identical
- Auditing: Prove audit ciphertext matches transfer amount
- Viewing Keys: Prove additional encryptions match transfer amount
Next Steps
- ElGamal Encryption - Understanding the base encryption
- Bit Proofs - Range proof building blocks
- POE Protocol - Base proof system
Tongo TypeScript SDK
The Tongo TypeScript SDK provides a comprehensive interface for building confidential payment applications on Starknet. It handles key management, encryption, proof generation, and transaction serialization.
Features
- Simple API: High-level methods for all Tongo operations
- Type Safety: Full TypeScript support with complete type definitions
- Proof Generation: Automatic ZK proof creation for all operations
- Encryption Handling: Transparent management of encrypted balances
- Starknet Integration: Seamless integration with Starknet wallets and providers
Architecture
The SDK consists of two main layers:
1. SHE (Starknet Homomorphic Encryption)
Low-level cryptographic primitives for:
- ElGamal encryption over the Stark curve
- Zero-knowledge proof generation (POE, PED, RAN)
- Homomorphic balance operations
2. Tongo SDK
High-level application interface providing:
Accountclass for managing Tongo accounts- Operation objects for transactions (
FundOperation,TransferOperation, etc.) - State management and decryption utilities
- Event querying and transaction history
Package Information
- Package:
@fatsolutions/tongo-sdk - Current Version: 1.2.0
- License: Apache-2.0
- Repository: github.com/fatlabsxyz/tongo
Quick Links
- Installation - Install the SDK
- Quick Start - Your first Tongo transaction
- Core Concepts - Understand the fundamentals
- API Reference - Complete API documentation
- Examples - Real-world code examples
Supported Networks
The SDK works on:
- Starknet Mainnet - Production deployments
- Starknet Sepolia - Testnet for development
Deployed Tongo Contracts:
- Mainnet:
0x0415f2c3b16cc43856a0434ed151888a5797b6a22492ea6fd41c62dbb4df4e6c(USDC wrapper) - Sepolia:
0x00b4cca30f0f641e01140c1c388f55641f1c3fe5515484e622b6cb91d8cee585(STRK wrapper, 1:1 rate)
Prerequisites
- Node.js: v18 or higher
- Starknet.js: v8.0.0 or higher (peer dependency)
- TypeScript: v5.0 or higher (recommended)
Next Steps
Installation
Using npm
npm install @fatsolutions/tongo-sdk
Using pnpm
pnpm add @fatsolutions/tongo-sdk
Using yarn
yarn add @fatsolutions/tongo-sdk
Peer Dependencies
The Tongo SDK requires Starknet.js as a peer dependency. If you don't have it installed:
npm install starknet
Version Compatibility
| Tongo SDK | Starknet.js | Node.js |
|---|---|---|
| 1.2.0 | ^8.0.0 | >=18 |
TypeScript Support
The SDK is written in TypeScript and includes full type definitions. No additional @types packages are needed.
TypeScript Configuration
Ensure your tsconfig.json includes:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true
}
}
Verification
Verify your installation:
import { Account } from "@fatsolutions/tongo-sdk";
console.log("Tongo SDK loaded successfully!");
Next Steps
- Quick Start Guide - Create your first Tongo transaction
- Core Concepts - Learn about Accounts and Operations
Quick Start
This guide will walk you through your first Tongo confidential transfer in just a few minutes.
Prerequisites
- Node.js v18+ installed
- A Starknet account with some testnet ETH (for gas fees)
- Basic familiarity with TypeScript and Starknet
Setup
Install the required packages:
npm install @fatsolutions/tongo-sdk starknet
Basic Workflow
A typical Tongo workflow involves four steps:
- Fund - Convert ERC20 tokens to encrypted balance
- Transfer - Send confidential transfers
- Rollover - Claim pending incoming transfers
- Withdraw - Convert back to ERC20 tokens
Complete Example
Step 1: Setup Provider and Signer
import { Account as TongoAccount } from "@fatsolutions/tongo-sdk";
import { Account, RpcProvider } from "starknet";
// Setup Starknet provider (Sepolia testnet)
const provider = new RpcProvider({
nodeUrl: "https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_8/YOUR_API_KEY",
specVersion: "0.8.1",
});
// Your Starknet account (for paying gas fees)
const signer = new Account({
provider,
address: "YOUR_STARKNET_ADDRESS",
signer: "YOUR_PRIVATE_KEY"
});
// Tongo contract address on Sepolia
// This contract wraps STRK with a 1:1 rate (1 STRK = 1 Tongo STRK)
const tongoAddress = "0x00b4cca30f0f641e01140c1c388f55641f1c3fe5515484e622b6cb91d8cee585";
Important: Your Starknet account must have:
- Testnet ETH for gas fees
- STRK tokens (this contract wraps STRK on Sepolia testnet)
- Get testnet STRK from: https://starknet-faucet.vercel.app/
Step 2: Create Tongo Accounts
// Create two Tongo accounts with different private keys
const account1PrivateKey = 82130983n; // Your Tongo private key
const account2PrivateKey = 12930923n; // Recipient's Tongo private key
const account1 = new TongoAccount(account1PrivateKey, tongoAddress, provider);
const account2 = new TongoAccount(account2PrivateKey, tongoAddress, provider);
console.log("Account 1 Public Key:", account1.publicKey);
console.log("Account 2 Public Key:", account2.publicKey);
Note: Tongo private keys are separate from Starknet private keys. See Key Management for details.
Step 3: Fund Account 1
// Fund account 1 with 100 Tongo units
// Note: Tongo uses 32-bit balances, amounts are in Tongo units (not full STRK decimals)
const fundOp = await account1.fund({ amount: 100n });
// Populate the ERC20 approval transaction
await fundOp.populateApprove();
// Execute both approval and fund transactions
const fundTx = await signer.execute([
fundOp.approve!, // Approve Tongo contract to spend tokens
fundOp.toCalldata() // Fund operation
]);
console.log("Fund transaction:", fundTx.transaction_hash);
// Wait for transaction confirmation
await provider.waitForTransaction(fundTx.transaction_hash);
Step 4: Check Balance
// Get encrypted state
const state1 = await account1.rawState();
// Decrypt balance
const balance = account1.decryptCipherBalance(state1.balance);
console.log("Account 1 balance:", balance); // 100n
// Or use the convenience method
const decryptedState = await account1.state();
console.log("Decrypted state:", decryptedState);
// { balance: 100n, pending: 0n, nonce: 1n }
Step 5: Transfer to Account 2
// Transfer 25 Tongo units from account 1 to account 2
const transferOp = await account1.transfer({
to: account2.publicKey,
amount: 25n
});
const transferTx = await signer.execute(transferOp.toCalldata());
console.log("Transfer transaction:", transferTx.transaction_hash);
await provider.waitForTransaction(transferTx.transaction_hash);
Step 6: Check Account States
// Check sender balance (should be reduced)
const state1After = await account1.state();
console.log("Account 1 after transfer:", state1After);
// { balance: 75n, pending: 0n, nonce: 2n }
// Check recipient pending balance (not yet rolled over)
const state2 = await account2.state();
console.log("Account 2 state:", state2);
// { balance: 0n, pending: 25n, nonce: 0n }
Step 7: Rollover (Claim Transfer)
// Account 2 must rollover to claim the pending transfer
const rolloverOp = await account2.rollover();
const rolloverTx = await signer.execute(rolloverOp.toCalldata());
console.log("Rollover transaction:", rolloverTx.transaction_hash);
await provider.waitForTransaction(rolloverTx.transaction_hash);
// Check account 2 balance again
const state2After = await account2.state();
console.log("Account 2 after rollover:", state2After);
// { balance: 25n, pending: 0n, nonce: 1n }
Step 8: Withdraw
// Withdraw 10 Tongo units back to ERC20
const withdrawOp = await account2.withdraw({
to: signer.address, // Withdraw to your Starknet address
amount: 10n
});
const withdrawTx = await signer.execute(withdrawOp.toCalldata());
console.log("Withdraw transaction:", withdrawTx.transaction_hash);
await provider.waitForTransaction(withdrawTx.transaction_hash);
// Final balance check
const state2Final = await account2.state();
console.log("Account 2 final state:", state2Final);
// { balance: 15n, pending: 0n, nonce: 2n }
Key Concepts
- Tongo Account: A separate keypair for confidential transactions (different from Starknet account)
- Encrypted Balance: Your balance is stored encrypted on-chain
- Pending Balance: Incoming transfers must be "rolled over" to be spendable
- Nonce: Each account has a nonce that increments with each operation
- Tongo Units: Amounts are in Tongo units (32-bit integers, max value: 2^32 - 1)
- 1:1 Rate: This Tongo contract uses a 1:1 conversion rate with STRK
- Not Full Decimals: Due to 32-bit limit, use integer amounts like 100n, not 10^18 for 1 STRK
Next Steps
Accounts
In Tongo, an Account represents a confidential payment account with encrypted balances. Each account is identified by a public key derived from a private key.
Account Class
The Account class is the main interface for interacting with Tongo. It manages:
- Private/Public keypair: For encryption and signing
- Encrypted state: Balance, pending, and nonce
- Operations: Creating fund, transfer, withdraw, and rollover operations
- Decryption: Decrypting encrypted balances
Creating an Account
From a Private Key
import { Account as TongoAccount } from "@fatsolutions/tongo-sdk";
import { RpcProvider } from "starknet";
const provider = new RpcProvider({
nodeUrl: "YOUR_RPC_URL",
specVersion: "0.8.1",
});
// Tongo contract on Sepolia (wraps STRK with 1:1 rate)
const tongoAddress = "0x00b4cca30f0f641e01140c1c388f55641f1c3fe5515484e622b6cb91d8cee585";
// Create account with bigint private key
const privateKey = 82130983n;
const account = new TongoAccount(privateKey, tongoAddress, provider);
From Bytes
You can also create an account from a Uint8Array:
const privateKeyBytes = new Uint8Array([...]);
const account = new TongoAccount(privateKeyBytes, tongoAddress, provider);
Account Properties
Public Key
The public key is automatically derived from the private key:
console.log(account.publicKey);
// { x: bigint, y: bigint }
Tongo Address
Get the base58-encoded public key (Tongo address):
const address = account.tongoAddress();
console.log(address);
// "Um6QEVHZaXkii8hWzayJf6PBWrJCTuJomAst75Zmy12"
You can also get a Tongo address from any private key without creating an Account:
import { Account as TongoAccount } from "@fatsolutions/tongo-sdk";
const address = TongoAccount.tongoAddress(privateKey);
Account State
Raw State
Get the encrypted state directly from the contract:
const rawState = await account.rawState();
console.log(rawState);
/*
{
balance: CipherBalance, // Encrypted balance
pending: CipherBalance, // Encrypted pending balance
audit: CipherBalance | undefined,
nonce: bigint,
aeBalance: AEBalance | undefined,
aeAuditBalance: AEBalance | undefined
}
*/
Decrypted State
Get the decrypted balance and pending:
const state = await account.state();
console.log(state);
/*
{
balance: bigint, // Decrypted balance
pending: bigint, // Decrypted pending
nonce: bigint // Account nonce
}
*/
Nonce
Get just the account nonce:
const nonce = await account.nonce();
console.log(nonce); // 1n
Account Operations
Accounts can create various operations. Each method returns an operation object:
// Fund operation
const fundOp = await account.fund({ amount: 1000n });
// Transfer operation
const transferOp = await account.transfer({
to: recipientPublicKey,
amount: 100n
});
// Withdraw operation
const withdrawOp = await account.withdraw({
to: starknetAddress,
amount: 50n
});
// Rollover operation
const rolloverOp = await account.rollover();
See Operations for detailed information about each operation type.
Token Conversions
Tongo contracts wrap ERC20 tokens with a configurable rate. The Account class provides utilities for conversions:
Get Contract Rate
const rate = await account.rate();
console.log(rate); // e.g., 1n for 1:1 ratio
Convert ERC20 to Tongo Amount
const erc20Amount = 1000000n; // 1 USDC (6 decimals)
const tongoAmount = await account.erc20ToTongo(erc20Amount);
console.log(tongoAmount);
Warning: This method is for display purposes only and may not be exact due to rounding.
Convert Tongo to ERC20 Amount
const tongoAmount = 1000n;
const erc20Amount = await account.tongoToErc20(tongoAmount);
console.log(erc20Amount);
Encryption and Decryption
Decrypt Cipher Balance
Decrypt an encrypted balance:
const rawState = await account.rawState();
const balance = account.decryptCipherBalance(rawState.balance);
console.log(balance); // 5000n
With a hint for faster decryption:
const hint = 5000n; // Known or estimated balance
const balance = account.decryptCipherBalance(rawState.balance, hint);
Decrypt AE Balance
Decrypt an AE-encrypted hint:
const rawState = await account.rawState();
if (rawState.aeBalance) {
const hint = await account.decryptAEBalance(
rawState.aeBalance,
rawState.nonce
);
console.log(hint); // The decrypted hint
}
Key Concepts
Private vs Public Key
- Private Key: Secret value (bigint or Uint8Array) that you must keep safe
- Public Key: Derived from private key, serves as your account identifier
- Tongo Address: Base58-encoded public key for easy sharing
Tongo Account vs Starknet Account
- Tongo Account: For confidential transactions, separate keypair
- Starknet Account: For paying gas fees and interacting with Starknet
You need both:
- A Starknet Account to sign and submit transactions
- A Tongo Account to create confidential operations
Balance vs Pending
- Balance: Your spendable, confirmed balance
- Pending: Incoming transfers that haven't been rolled over yet
You must call rollover() to move pending transfers into your spendable balance.
Security Considerations
Private Key Storage
Your Tongo private key should be:
- Stored securely (encrypted at rest)
- Never shared or transmitted
- Derived deterministically from your Starknet wallet (see Key Management)
Account Recovery
If you lose your Tongo private key:
- You cannot decrypt your balance
- You cannot spend your funds
- There is no recovery mechanism
Always back up your private key!
Next Steps
Operations
Operations are objects that represent Tongo transactions. Each operation encapsulates the cryptographic proofs, encrypted data, and calldata needed for a specific action.
Operation Types
Tongo supports five core operations:
- Fund - Convert ERC20 tokens to encrypted balance
- Transfer - Send encrypted amounts to another account
- Withdraw - Convert encrypted balance back to ERC20
- Rollover - Claim pending incoming transfers
- Ragequit - Emergency withdrawal of entire balance
Operation Workflow
All operations follow a similar pattern:
// 1. Create the operation
const operation = await account.someOperation({...params});
// 2. Convert to calldata
const calldata = operation.toCalldata();
// 3. Execute with Starknet signer
const tx = await signer.execute([calldata]);
// 4. Wait for confirmation
await provider.waitForTransaction(tx.transaction_hash);
Fund Operation
Converts ERC20 tokens into encrypted Tongo balance.
Creating a Fund Operation
const fundOp = await account.fund({ amount: 1000n });
Approving ERC20 Spending
Fund operations require ERC20 approval. The SDK provides a helper:
// Populate the approval transaction
await fundOp.populateApprove();
// Execute both approval and fund
await signer.execute([
fundOp.approve!, // ERC20 approval
fundOp.toCalldata() // Fund operation
]);
What Happens
- Generates ZK proof of funding
- Creates encrypted balance ciphertext
- Optionally creates audit ciphertext (if auditor is set)
- Creates AE hint for faster decryption
Transfer Operation
Sends encrypted amounts between Tongo accounts.
Creating a Transfer Operation
const transferOp = await account.transfer({
to: recipientPublicKey, // Recipient's public key
amount: 100n // Amount to transfer
});
What Happens
- Encrypts transfer amount for recipient
- Encrypts new balance for sender
- Generates ZK proof that:
- Sender knows their private key
- Transfer amount ≤ sender balance
- New balance is correctly computed
- Creates encrypted pending balance for recipient
- Creates AE hints for both parties
Result
- Sender: Balance reduced, nonce incremented
- Recipient: Pending balance increased (must rollover)
Withdraw Operation
Converts encrypted Tongo balance back to ERC20 tokens.
Creating a Withdraw Operation
const withdrawOp = await account.withdraw({
to: starknetAddress, // Destination address (hex string)
amount: 500n // Amount to withdraw
});
What Happens
- Generates ZK proof that:
- User knows their private key
- Withdrawal amount ≤ current balance
- New balance is correctly computed
- Creates encrypted new balance
- Creates AE hint for new balance
- Transfers ERC20 tokens to destination address
Important Notes
- The
toaddress receives actual ERC20 tokens - Amount is converted using the contract's rate
- Balance is reduced immediately
Rollover Operation
Claims pending incoming transfers and moves them to spendable balance.
Creating a Rollover Operation
const rolloverOp = await account.rollover();
What Happens
- Reads current balance and pending balance
- Generates ZK proof of knowledge of private key
- Computes new balance = old balance + pending
- Creates encrypted new balance
- Resets pending to zero
When to Rollover
You must rollover when:
- You've received a transfer (pending > 0)
- You want to spend received funds
- Before withdrawing received funds
Example Flow
// Check state before rollover
const stateBefore = await account.state();
console.log(stateBefore); // { balance: 0n, pending: 500n, nonce: 0n }
// Rollover
const rolloverOp = await account.rollover();
await signer.execute([rolloverOp.toCalldata()]);
// Check state after rollover
const stateAfter = await account.state();
console.log(stateAfter); // { balance: 500n, pending: 0n, nonce: 1n }
Ragequit Operation
Emergency operation to withdraw entire balance at once.
Creating a Ragequit Operation
const ragequitOp = await account.ragequit({
to: starknetAddress // Destination for all funds
});
What Happens
- Withdraws entire balance to specified address
- Zeroes out encrypted balance
- Generates proof of balance ownership
When to Use
- Emergency situations
- Account closure
- When you want to exit Tongo completely
Warning: Ragequit withdraws ALL funds. Use
withdrawfor partial withdrawals.
Operation Objects
Common Interface
All operation objects implement:
interface IOperation {
type: OperationType;
toCalldata(): Call;
}
Converting to Calldata
Every operation can be converted to Starknet calldata:
const operation = await account.transfer({...});
const calldata = operation.toCalldata();
// calldata is a Call object:
// {
// contractAddress: string,
// entrypoint: string,
// calldata: string[]
// }
Executing Operations
With a Starknet signer:
// Single operation
await signer.execute([operation.toCalldata()]);
// Multiple operations (e.g., approve + fund)
await signer.execute([
operation1.toCalldata(),
operation2.toCalldata()
]);
Proofs
Every operation (except rollover in some cases) includes a zero-knowledge proof. The SDK automatically:
- Generates the proof using the SHE library
- Includes it in the operation object
- Serializes it for the contract
You don't need to worry about proof generation—it's all handled internally.
Error Handling
Operations can fail during creation:
try {
const transferOp = await account.transfer({
to: recipientPubKey,
amount: 9999999n // More than balance
});
} catch (error) {
console.error("Operation failed:", error.message);
// "You dont have enough balance"
}
Common errors:
"You dont have enough balance"- Insufficient funds for transfer/withdraw"Your pending ammount is 0"- Trying to rollover with no pending balance"You dont have enought balance"[sic] - Withdraw amount exceeds balance
Gas Costs
Approximate gas costs on Starknet:
| Operation | Cairo Steps | Relative Cost |
|---|---|---|
| Fund | ~50K | Low |
| Transfer | ~120K | Medium |
| Rollover | ~80K | Low |
| Withdraw | ~80K | Low |
| Ragequit | ~80K | Low |
Actual costs vary based on network conditions and transaction complexity.
Next Steps
Encrypted State
Tongo stores all account balances as encrypted ciphertexts on-chain. This page explains how encrypted state works and how to decrypt it.
State Structure
Each Tongo account has the following encrypted state on-chain:
interface RawAccountState {
balance: CipherBalance; // Encrypted spendable balance
pending: CipherBalance; // Encrypted pending (incoming) balance
audit: CipherBalance | undefined; // Optional audit ciphertext
nonce: bigint; // Account nonce (not encrypted)
aeBalance?: AEBalance; // AE-encrypted hint for balance
aeAuditBalance?: AEBalance; // AE-encrypted hint for audit
}
CipherBalance
A CipherBalance is an ElGamal ciphertext representing an encrypted amount:
interface CipherBalance {
L: ProjectivePoint; // g^amount * y^randomness
R: ProjectivePoint; // g^randomness
}
Where:
gis the Stark curve generatoryis the recipient's public key- The amount is encrypted homomorphically
Properties
- Additively Homomorphic: Can add encrypted balances without decryption
- Semantically Secure: Same amount encrypted twice looks different
- Decryption Required: Must brute-force or use hints to recover the amount
AEBalance
An AEBalance is a ChaCha20-encrypted hint for faster decryption:
interface AEBalance {
c0: bigint; // Ciphertext part 1
c1: bigint; // Ciphertext part 2
c2: bigint; // Ciphertext part 3
}
Why AE Hints?
Decrypting ElGamal ciphertexts requires brute-force search. AE hints provide:
- Instant decryption with the symmetric key
- Verification against the ElGamal ciphertext
- Fallback to brute-force if hint is unavailable
Decryption Methods
Method 1: Using state() (Recommended)
The easiest way to get decrypted balances:
const state = await account.state();
console.log(state);
// { balance: 5000n, pending: 500n, nonce: 2n }
This method:
- Fetches raw state from contract
- Decrypts AE hints (if available)
- Decrypts CipherBalances using hints
- Falls back to brute-force if needed
Method 2: Manual Decryption
For more control, decrypt manually:
// Get raw state
const rawState = await account.rawState();
// Decrypt balance with hint
let balanceAmount: bigint;
if (rawState.aeBalance) {
const hint = await account.decryptAEBalance(
rawState.aeBalance,
rawState.nonce
);
balanceAmount = account.decryptCipherBalance(rawState.balance, hint);
} else {
// Brute-force without hint
balanceAmount = account.decryptCipherBalance(rawState.balance);
}
// Decrypt pending (no hint for pending)
const pendingAmount = account.decryptCipherBalance(rawState.pending);
console.log({ balance: balanceAmount, pending: pendingAmount });
Method 3: Brute-Force Range
If you know the approximate range:
// This is what happens internally when no hint is available
// The SDK uses Baby-step Giant-step algorithm
const amount = account.decryptCipherBalance(cipherBalance);
Note: The SDK implements an efficient Baby-step Giant-step algorithm from the SHE library. Decryption without hints can still be fast for reasonable ranges.
State Updates
After Fund
const fundOp = await account.fund({ amount: 1000n });
// ... execute transaction ...
const state = await account.state();
// {
// balance: 1000n, // Increased
// pending: 0n,
// nonce: 1n // Incremented
// }
After Transfer (Sender)
const transferOp = await account.transfer({
to: recipientPubKey,
amount: 100n
});
// ... execute transaction ...
const state = await account.state();
// {
// balance: 900n, // Decreased
// pending: 0n,
// nonce: 2n // Incremented
// }
After Transfer (Recipient)
// Recipient's state after receiving transfer
const state = await recipientAccount.state();
// {
// balance: 0n, // Unchanged
// pending: 100n, // Increased!
// nonce: 0n // Unchanged
// }
After Rollover
const rolloverOp = await account.rollover();
// ... execute transaction ...
const state = await account.state();
// {
// balance: 100n, // pending moved to balance
// pending: 0n, // Reset to zero
// nonce: 1n // Incremented
// }
After Withdraw
const withdrawOp = await account.withdraw({
to: address,
amount: 50n
});
// ... execute transaction ...
const state = await account.state();
// {
// balance: 50n, // Decreased
// pending: 0n,
// nonce: 2n // Incremented
// }
Nonce Management
The nonce is not encrypted and serves multiple purposes:
- Prevent replay attacks: Each operation increments nonce
- Order operations: Nonce must match expected value
- Key derivation: Used in AE hint encryption
Nonce Behavior
- Starts at
0nfor new accounts - Increments by 1 for each operation
- Cannot be modified directly
- Must match on-chain value for operations to succeed
const nonce = await account.nonce();
console.log(nonce); // 0n (new account)
// After first operation
await account.fund({...});
const newNonce = await account.nonce();
console.log(newNonce); // 1n
Audit Ciphertexts
If a global auditor is configured, each operation creates an audit ciphertext:
const rawState = await account.rawState();
if (rawState.audit) {
// Audit ciphertext exists
// Only the auditor can decrypt this
}
For Auditors
If you have the auditor private key:
import { decipherBalance } from "@fatsolutions/she";
const auditAmount = decipherBalance(
auditorPrivateKey,
rawState.audit.L,
rawState.audit.R
);
console.log("Audited amount:", auditAmount);
Performance Considerations
AE Hints vs Brute-Force
- With AE hint: Instant decryption (< 1ms)
- Without hint: Depends on range, typically < 100ms for balances up to 1M
When Hints Are Available
AE hints are created for:
- Balance after fund
- Balance after transfer (sender)
- Balance after rollover
- Balance after withdraw
- Pending balance does NOT have hints (use brute-force)
Optimizing Decryption
If you're decrypting frequently:
// Cache decrypted values
let cachedBalance = await account.state();
// Only refresh when needed
async function refreshBalance() {
cachedBalance = await account.state();
return cachedBalance;
}
// Use cached value for display
console.log("Balance:", cachedBalance.balance);
Error Scenarios
Corrupted Ciphertexts
If a ciphertext is malformed:
try {
const amount = account.decryptCipherBalance(corruptedCipher);
} catch (error) {
console.error("Decryption failed:", error);
// Ciphertext might be corrupted or tampered with
}
Hint Mismatch
If the AE hint doesn't match the CipherBalance:
const hint = await account.decryptAEBalance(aeBalance, nonce);
const verified = account.decryptCipherBalance(balance, hint);
// SDK internally verifies:
// assertBalance(privateKey, hint, L, R)
// If verification fails, falls back to brute-force
Next Steps
Key Management
Proper key management is crucial for Tongo applications. This page covers best practices for generating, storing, and deriving Tongo private keys.
Key Types
Tongo Private Key
- Purpose: Decrypt balances and authorize Tongo operations
- Format:
bigintorUint8Array - Range: Must be within the Stark curve scalar field
- Example:
82130983n
Tongo Public Key
- Purpose: Account identifier for receiving transfers
- Format:
{ x: bigint, y: bigint }(elliptic curve point) - Derivation: Computed as
g^privateKeywheregis the curve generator
Tongo Address
- Purpose: Human-readable account identifier
- Format: Base58-encoded public key
- Example:
"Um6QEVHZaXkii8hWzayJf6PBWrJCTuJomAst75Zmy12"
Key Generation Strategies
Strategy 1: Random Generation
Generate a completely random private key:
import { getRandomValues } from "crypto";
function generateRandomTongoKey(): bigint {
// Generate random bytes
const bytes = new Uint8Array(32);
getRandomValues(bytes);
// Convert to bigint
const privateKey = BigInt('0x' + Buffer.from(bytes).toString('hex'));
// Ensure it's within the curve order
const CURVE_ORDER = BigInt('0x800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f');
return privateKey % CURVE_ORDER;
}
const randomKey = generateRandomTongoKey();
Pros:
- Maximum entropy
- No dependencies
Cons:
- Must be securely stored and backed up
- No deterministic recovery
Strategy 2: Deterministic Derivation from Starknet Wallet
Derive your Tongo key deterministically from your Starknet wallet signature:
import { AccountInterface, TypedData, hash } from "starknet";
const CURVE_ORDER = BigInt('0x800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f');
async function deriveTongoPrivateKey(account: AccountInterface): Promise<bigint> {
const accountAddress = account.address;
const chainId = (account as any).chainId || 'SN_SEPOLIA';
// Create typed data for signing
const typedData: TypedData = {
domain: {
name: 'Tongo Key Derivation',
version: '1',
chainId: chainId,
},
types: {
StarkNetDomain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'felt' },
],
Message: [
{ name: 'action', type: 'felt' },
{ name: 'wallet', type: 'felt' },
],
},
primaryType: 'Message',
message: {
action: 'tongo-keygen-v1',
wallet: accountAddress,
},
};
// Sign the typed data
const signature = await account.signMessage(typedData);
// Extract r and s from signature
const { r, s } = extractSignatureComponents(signature);
// Hash using Poseidon
const privateKeyHex = hash.computePoseidonHashOnElements([r, s]);
let privateKey = BigInt(privateKeyHex);
// Reduce modulo curve order
privateKey = privateKey % CURVE_ORDER;
// Ensure non-zero
if (privateKey === BigInt(0)) {
throw new Error('Derived private key is zero');
}
return privateKey;
}
function extractSignatureComponents(signature: any): { r: bigint; s: bigint } {
if (Array.isArray(signature)) {
// Argent X format: [1, 0, r, s, ...]
if (signature.length >= 4 && BigInt(signature[0]) === BigInt(1)) {
return {
r: BigInt(signature[2]),
s: BigInt(signature[3]),
};
}
// Standard format: [r, s]
if (signature.length >= 2) {
return {
r: BigInt(signature[0]),
s: BigInt(signature[1]),
};
}
}
if (signature && typeof signature === 'object' && 'r' in signature && 's' in signature) {
return {
r: BigInt(signature.r),
s: BigInt(signature.s),
};
}
throw new Error('Invalid signature format');
}
Pros:
- Deterministic (same wallet → same Tongo key)
- No need to store Tongo private key separately
- Can be recovered from wallet signature
Cons:
- Requires user to sign a message
- Depends on wallet connection
Usage in React
import { useAccount } from '@starknet-react/core';
import { useState } from 'react';
function useTongoKey() {
const { account } = useAccount();
const [tongoKey, setTongoKey] = useState<bigint | null>(null);
const [loading, setLoading] = useState(false);
async function deriveKey() {
if (!account) {
throw new Error('No wallet connected');
}
setLoading(true);
try {
const key = await deriveTongoPrivateKey(account);
setTongoKey(key);
return key;
} finally {
setLoading(false);
}
}
return { tongoKey, deriveKey, loading };
}
Storage Best Practices
Never Do This
// DON'T: Store in localStorage unencrypted
localStorage.setItem('tongoKey', privateKey.toString());
// DON'T: Hard-code in source
const PRIVATE_KEY = 82130983n;
// DON'T: Send over network
fetch('/api/save-key', { body: privateKey });
Recommended Approaches
For Web Applications
// Derive from wallet each time
async function getTongoAccount(walletAccount, provider) {
const privateKey = await deriveTongoPrivateKey(walletAccount);
return new TongoAccount(privateKey, tongoAddress, provider);
}
// Or encrypt before storing
import { encrypt, decrypt } from 'your-encryption-library';
function saveEncryptedKey(privateKey: bigint, password: string) {
const encrypted = encrypt(privateKey.toString(), password);
localStorage.setItem('tongoKey_encrypted', encrypted);
}
function loadEncryptedKey(password: string): bigint | null {
const encrypted = localStorage.getItem('tongoKey_encrypted');
if (!encrypted) return null;
const decrypted = decrypt(encrypted, password);
return BigInt(decrypted);
}
For Backend Applications
// Use environment variables
const TONGO_PRIVATE_KEY = BigInt(process.env.TONGO_PRIVATE_KEY!);
// Or use a secret management service
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
async function getTongoKey() {
const client = new SecretManagerServiceClient();
const [version] = await client.accessSecretVersion({
name: 'projects/my-project/secrets/tongo-key/versions/latest',
});
const payload = version.payload?.data?.toString();
return BigInt(payload!);
}
Key Backup and Recovery
Backup Strategies
-
Hardware Wallet Integration
- Store derivation parameters in hardware wallet
- Sign derivation message when needed
-
Encrypted Backup
- Encrypt private key with strong password
- Store encrypted backup securely
- Test recovery before relying on it
-
Multi-Signature Schemes
- Split key using Shamir's Secret Sharing
- Require M-of-N shares to recover
- Distribute shares to trusted parties
Recovery Checklist
If using deterministic derivation:
- Can access Starknet wallet
- Know which wallet was used
- Can sign messages with wallet
If using random generation:
- Have encrypted backup
- Remember encryption password
- Can access backup location
Security Considerations
Key Lifecycle
-
Generation
- Use cryptographically secure randomness
- Or derive deterministically from wallet
-
Usage
- Never log or print the key
- Keep in memory only when needed
- Clear from memory after use
-
Storage
- Encrypt at rest
- Use secure key management systems
- Regular security audits
-
Disposal
- Securely wipe from memory
- Delete encrypted backups if desired
- Withdraw all funds first
Common Pitfalls
Weak entropy: Using predictable values like timestamps or counters
Key reuse: Using same key across different chains or applications
Insecure storage: Storing keys in plain text or weakly encrypted
No backup: Losing access to funds if key is lost
Best practice: Use wallet-derived keys with secure backup
Testing Keys
For development and testing only:
// Test keys (NEVER use in production!)
const TEST_KEYS = {
alice: 82130983n,
bob: 12930923n,
charlie: 55555555n,
};
// Use only on testnets
const testAccount = new TongoAccount(
TEST_KEYS.alice,
SEPOLIA_TONGO_ADDRESS,
sepoliaProvider
);
Multi-Account Management
Managing multiple Tongo accounts:
interface TongoAccountInfo {
name: string;
privateKey: bigint;
address: string;
}
class TongoWallet {
private accounts: Map<string, TongoAccountInfo> = new Map();
async addAccount(name: string, privateKey: bigint) {
const account = new TongoAccount(privateKey, tongoAddress, provider);
const address = account.tongoAddress();
this.accounts.set(name, {
name,
privateKey,
address
});
}
getAccount(name: string): TongoAccount | null {
const info = this.accounts.get(name);
if (!info) return null;
return new TongoAccount(info.privateKey, tongoAddress, provider);
}
listAccounts(): string[] {
return Array.from(this.accounts.keys());
}
}
Next Steps
Funding Accounts
This guide shows you how to convert ERC20 tokens into encrypted Tongo balances.
Overview
Funding deposits ERC20 tokens into a Tongo account, creating an encrypted balance.
What Happens
- Approve Tongo contract to spend ERC20 tokens
- Contract transfers tokens from your wallet
- Encrypted balance increases
- ZK proof verifies the operation
- AE hint created for faster decryption
Basic Example
import { Account as TongoAccount } from "@fatsolutions/tongo-sdk";
import { Account, RpcProvider } from "starknet";
// Create fund operation
const fundOp = await tongoAccount.fund({ amount: 1000n });
// Populate ERC20 approval
await fundOp.populateApprove();
// Execute both transactions
const tx = await signer.execute([
fundOp.approve!, // ERC20 approval
fundOp.toCalldata() // Fund operation
]);
// Wait and check balance
await provider.waitForTransaction(tx.transaction_hash);
const state = await tongoAccount.state();
console.log("New balance:", state.balance); // 1000n
Understanding Approval
Fund operations require two steps:
- ERC20 Approval: Allow Tongo to spend tokens
- Fund Call: Transfer tokens and update balance
// SDK creates approval automatically
await fundOp.populateApprove();
// Now fundOp.approve contains the approval transaction
Token Conversion
// Check conversion rate
const rate = await tongoAccount.rate();
console.log("Rate:", rate); // e.g., 1n
// Convert between ERC20 and Tongo amounts
const erc20Amount = await tongoAccount.tongoToErc20(1000n);
const tongoAmount = await tongoAccount.erc20ToTongo(erc20Amount);
Error Handling
try {
const fundOp = await tongoAccount.fund({ amount: 1000000n });
await fundOp.populateApprove();
await signer.execute([fundOp.approve!, fundOp.toCalldata()]);
} catch (error) {
console.error("Fund failed:", error.message);
}
Next Steps
Private Transfers
Send encrypted amounts between Tongo accounts without revealing the transfer amount.
Basic Transfer
const transferOp = await senderAccount.transfer({
to: recipientAccount.publicKey,
amount: 100n
});
const tx = await signer.execute([transferOp.toCalldata()]);
await provider.waitForTransaction(tx.transaction_hash);
Transfer Flow
- Sender balance decreases
- Recipient pending increases
- Recipient must rollover to claim
// After transfer, recipient checks state
const state = await recipientAccount.state();
console.log(state.pending); // 100n
// Rollover to claim
const rolloverOp = await recipientAccount.rollover();
await signer.execute([rolloverOp.toCalldata()]);
Error Handling
try {
await account.transfer({ to: recipientPubKey, amount: 999999n });
} catch (error) {
console.error(error.message); // "You dont have enough balance"
}
Next Steps
Withdrawals
Convert encrypted Tongo balance back to ERC20 tokens.
Basic Withdrawal
const withdrawOp = await account.withdraw({
to: starknetAddress, // Destination address
amount: 500n // Amount to withdraw
});
const tx = await signer.execute([withdrawOp.toCalldata()]);
await provider.waitForTransaction(tx.transaction_hash);
What Happens
- Encrypted balance is decreased
- ERC20 tokens are transferred to destination address
- Amount is converted using contract rate
- Nonce is incremented
Complete Example
// Check balance before withdrawal
const stateBefore = await account.state();
console.log("Balance:", stateBefore.balance); // 1000n
// Withdraw 500 tokens to my wallet
const withdrawOp = await account.withdraw({
to: signer.address,
amount: 500n
});
const tx = await signer.execute([withdrawOp.toCalldata()]);
await provider.waitForTransaction(tx.transaction_hash);
// Check balance after withdrawal
const stateAfter = await account.state();
console.log("Balance:", stateAfter.balance); // 500n
console.log("Nonce:", stateAfter.nonce); // Incremented
Error Handling
try {
await account.withdraw({ to: address, amount: 999999n });
} catch (error) {
console.error(error.message); // "You dont have enought balance"
}
Next Steps
Rollover
Claim pending incoming transfers and move them to your spendable balance.
What is Rollover?
When you receive a transfer, it goes to your pending balance. You must rollover to move it to your spendable balance.
// After receiving a transfer
const state = await account.state();
console.log(state.balance); // 0n
console.log(state.pending); // 100n (received transfer)
// Rollover to claim
const rolloverOp = await account.rollover();
await signer.execute([rolloverOp.toCalldata()]);
// Now it's spendable
const newState = await account.state();
console.log(newState.balance); // 100n
console.log(newState.pending); // 0n
When to Rollover
Roll over when:
- You received a transfer (pending > 0)
- You want to spend received funds
- You want to withdraw received funds
Complete Example
// Check for pending balance
const state = await account.state();
if (state.pending > 0n) {
console.log(`Claiming $${state.pending} pending tokens`);$$
// Create rollover operation
const rolloverOp = await account.rollover();
// Execute transaction
const tx = await signer.execute([rolloverOp.toCalldata()]);
await provider.waitForTransaction(tx.transaction_hash);
console.log("Rollover complete!");
// Verify new balance
const newState = await account.state();
console.log("New balance:", newState.balance);
}
Error Handling
try {
await account.rollover();
} catch (error) {
console.error(error.message); // "Your pending ammount is 0"
}
Next Steps
Wallet Integration
Integrate Tongo with Starknet wallets like Argent X and Braavos.
Overview
Tongo requires two types of accounts:
- Starknet Account: For signing transactions and paying gas
- Tongo Account: For confidential operations (separate keypair)
Deriving Tongo Keys from Wallet
Best practice: Derive Tongo keys deterministically from wallet signatures.
import { AccountInterface, TypedData, hash } from "starknet";
const CURVE_ORDER = BigInt('0x800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f');
async function deriveTongoPrivateKey(account: AccountInterface): Promise<bigint> {
// Create typed data for signing
const typedData: TypedData = {
domain: {
name: 'Tongo Key Derivation',
version: '1',
chainId: account.chainId || 'SN_SEPOLIA',
},
types: {
StarkNetDomain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'felt' },
],
Message: [
{ name: 'action', type: 'felt' },
{ name: 'wallet', type: 'felt' },
],
},
primaryType: 'Message',
message: {
action: 'tongo-keygen-v1',
wallet: account.address,
},
};
// Sign with wallet
const signature = await account.signMessage(typedData);
// Extract r and s
const { r, s } = extractSignatureComponents(signature);
// Hash with Poseidon
const privateKeyHex = hash.computePoseidonHashOnElements([r, s]);
let privateKey = BigInt(privateKeyHex);
// Reduce modulo curve order
privateKey = privateKey % CURVE_ORDER;
if (privateKey === BigInt(0)) {
throw new Error('Derived private key is zero');
}
return privateKey;
}
function extractSignatureComponents(signature: any): { r: bigint; s: bigint } {
if (Array.isArray(signature)) {
// Argent X format: [1, 0, r, s, ...]
if (signature.length >= 4 && BigInt(signature[0]) === BigInt(1)) {
return { r: BigInt(signature[2]), s: BigInt(signature[3]) };
}
// Standard format: [r, s]
if (signature.length >= 2) {
return { r: BigInt(signature[0]), s: BigInt(signature[1]) };
}
}
if (signature?.r && signature?.s) {
return { r: BigInt(signature.r), s: BigInt(signature.s) };
}
throw new Error('Invalid signature format');
}
React Integration
import { useAccount } from '@starknet-react/core';
import { useState } from 'react';
function useTongoAccount() {
const { account } = useAccount();
const [tongoKey, setTongoKey] = useState<bigint | null>(null);
async function deriveKey() {
if (!account) throw new Error('No wallet connected');
const key = await deriveTongoPrivateKey(account);
setTongoKey(key);
// Create Tongo account
const tongoAccount = new TongoAccount(key, tongoAddress, provider);
return tongoAccount;
}
return { tongoKey, deriveKey };
}
Complete Wallet Flow
import { connect } from 'get-starknet';
async function setupTongoWithWallet() {
// 1. Connect Starknet wallet
const starknetWallet = await connect();
await starknetWallet.enable();
// 2. Get wallet account
const account = await starknetWallet.account;
// 3. Derive Tongo private key
const tongoPrivateKey = await deriveTongoPrivateKey(account);
// 4. Create Tongo account
const tongoAccount = new TongoAccount(
tongoPrivateKey,
tongoAddress,
provider
);
// 5. Use both accounts
// - Starknet account for signing transactions
// - Tongo account for creating operations
return { starknetAccount: account, tongoAccount };
}
Benefits of Wallet Derivation
- Deterministic: Same wallet always generates same Tongo key
- No Storage: Don't need to store Tongo private key
- Recoverable: Can recover from wallet alone
- Secure: Uses wallet's signature for entropy
Next Steps
Transaction History
Query and display transaction history for Tongo accounts.
Overview
The SDK provides methods to fetch different types of events for an account.
Event Types
- Fund: ERC20 deposited
- Transfer Out: Sent to another account
- Transfer In: Received from another account
- Rollover: Pending claimed
- Withdraw: Converted back to ERC20
- Ragequit: Emergency withdrawal
Fetching All Events
// Get complete transaction history from block 0
const history = await account.getTxHistory(0);
console.log(history);
// [
// { type: 'fund', amount: 1000n, tx_hash: '0x...', block_number: 12345 },
// { type: 'transferOut', amount: 100n, to: 'Um6Q...', ... },
// { type: 'rollover', amount: 50n, ... },
// ...
// ]
Fetching Specific Event Types
// Fund events
const fundEvents = await account.getEventsFund(initialBlock);
// Transfer out events
const transfersOut = await account.getEventsTransferOut(initialBlock);
// Transfer in events
const transfersIn = await account.getEventsTransferIn(initialBlock);
// Rollover events
const rollovers = await account.getEventsRollover(initialBlock);
// Withdraw events
const withdrawals = await account.getEventsWithdraw(initialBlock);
// Ragequit events
const ragequits = await account.getEventsRagequit(initialBlock);
Event Interfaces
// Fund event
interface AccountFundEvent {
type: 'fund';
tx_hash: string;
block_number: number;
nonce: bigint;
amount: bigint;
}
// Transfer out event
interface AccountTransferOutEvent {
type: 'transferOut';
tx_hash: string;
block_number: number;
nonce: bigint;
amount: bigint;
to: string; // Base58 Tongo address
}
// Transfer in event
interface AccountTransferInEvent {
type: 'transferIn';
tx_hash: string;
block_number: number;
nonce: bigint;
amount: bigint;
from: string; // Base58 Tongo address
}
Display Example
async function displayHistory(account: TongoAccount) {
const history = await account.getTxHistory(0);
for (const event of history) {
const date = new Date(event.block_number * 12000); // Approximate
console.log(`[${date.toISOString()}] $${event.type}`);$$
switch (event.type) {
case 'fund':
console.log(` Deposited $${event.amount}`);$$
break;
case 'transferOut':
console.log(` Sent ${event.amount} to $${event.to}`);$$
break;
case 'transferIn':
console.log(` Received ${event.amount} from $${event.from}`);$$
break;
case 'rollover':
console.log(` Claimed $${event.amount}`);$$
break;
case 'withdraw':
console.log(` Withdrew ${event.amount} to $${event.to}`);$$
break;
}
console.log(` TX: $${event.tx_hash}`);$$
}
}
React Component Example
function TransactionHistory({ account }: { account: TongoAccount }) {
const [history, setHistory] = useState<AccountEvents[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadHistory() {
const events = await account.getTxHistory(0);
setHistory(events.sort((a, b) => b.block_number - a.block_number));
setLoading(false);
}
loadHistory();
}, [account]);
if (loading) return <div>Loading...</div>;
return (
<div>
<h2>Transaction History</h2>
{history.map((event, i) => (
<div key={i}>
<strong>{event.type}</strong>
<span>Amount: {event.amount.toString()}</span>
<a href={`https://starkscan.co/tx/$${event.tx_hash}`}>$$
View on StarkScan
</a>
</div>
))}
</div>
);
}
Performance Considerations
// Cache the last block queried
let lastBlock = 0;
async function updateHistory(account: TongoAccount) {
// Only fetch new events since last check
const newEvents = await account.getTxHistory(lastBlock);
// Update last block
if (newEvents.length > 0) {
lastBlock = Math.max(...newEvents.map(e => e.block_number));
}
return newEvents;
}
Next Steps
Account Class API Reference
Complete API reference for the Account class.
Constructor
new Account(
pk: BigNumberish | Uint8Array,
contractAddress: string,
provider: RpcProvider
): Account
Parameters:
pk: Private key as bigint or Uint8ArraycontractAddress: Tongo contract addressprovider: Starknet RPC provider
Example:
const account = new TongoAccount(privateKey, tongoAddress, provider);
Static Methods
tongoAddress()
Get Tongo address from a private key without creating an Account.
static tongoAddress(pk: BigNumberish | Uint8Array): TongoAddress
Properties
publicKey
publicKey: PubKey
The account's public key (elliptic curve point).
pk
pk: bigint
The account's private key (internal, don't access directly).
State Methods
state()
Get decrypted account state.
async state(): Promise<AccountState>
Returns:
{
balance: bigint;
pending: bigint;
nonce: bigint;
}
rawState()
Get encrypted account state.
async rawState(): Promise<RawAccountState>
Returns:
{
balance: CipherBalance;
pending: CipherBalance;
audit?: CipherBalance;
nonce: bigint;
aeBalance?: AEBalance;
aeAuditBalance?: AEBalance;
}
nonce()
Get account nonce.
async nonce(): Promise<bigint>
tongoAddress()
Get base58-encoded Tongo address.
tongoAddress(): TongoAddress
Operation Methods
fund()
Create a fund operation.
async fund(details: FundDetails): Promise<FundOperation>
Parameters:
interface FundDetails {
amount: bigint;
}
transfer()
Create a transfer operation.
async transfer(details: TransferDetails): Promise<TransferOperation>
Parameters:
interface TransferDetails {
amount: bigint;
to: PubKey;
}
withdraw()
Create a withdraw operation.
async withdraw(details: WithdrawDetails): Promise<WithdrawOperation>
Parameters:
interface WithdrawDetails {
to: string; // Starknet address
amount: bigint;
}
rollover()
Create a rollover operation.
async rollover(): Promise<RollOverOperation>
ragequit()
Create a ragequit operation.
async ragequit(details: RagequitDetails): Promise<RagequitOperation>
Parameters:
interface RagequitDetails {
to: string; // Starknet address
}
Utility Methods
rate()
Get contract conversion rate.
async rate(): Promise<bigint>
erc20ToTongo()
Convert ERC20 amount to Tongo amount (approximate).
async erc20ToTongo(erc20Amount: bigint): Promise<bigint>
tongoToErc20()
Convert Tongo amount to ERC20 amount (exact).
async tongoToErc20(tongoAmount: bigint): Promise<bigint>
Decryption Methods
decryptCipherBalance()
Decrypt a CipherBalance.
decryptCipherBalance(cipher: CipherBalance, hint?: bigint): bigint
decryptAEBalance()
Decrypt an AEBalance hint.
async decryptAEBalance(aeBalance: AEBalance, accountNonce: bigint): Promise<bigint>
Event Methods
getTxHistory()
Get complete transaction history.
async getTxHistory(initialBlock: number): Promise<AccountEvents[]>
getEventsFund()
Get fund events.
async getEventsFund(initialBlock: number): Promise<AccountFundEvent[]>
getEventsTransferOut()
Get transfer out events.
async getEventsTransferOut(initialBlock: number): Promise<AccountTransferOutEvent[]>
getEventsTransferIn()
Get transfer in events.
async getEventsTransferIn(initialBlock: number): Promise<AccountTransferInEvent[]>
getEventsRollover()
Get rollover events.
async getEventsRollover(initialBlock: number): Promise<AccountRolloverEvent[]>
getEventsWithdraw()
Get withdraw events.
async getEventsWithdraw(initialBlock: number): Promise<AccountWithdrawEvent[]>
getEventsRagequit()
Get ragequit events.
async getEventsRagequit(initialBlock: number): Promise<AccountRagequitEvent[]>
Audit Methods
generateExPost()
Generate ex-post proof for a transfer.
generateExPost(to: PubKey, cipher: CipherBalance): ExPost
verifyExPost()
Verify an ex-post proof.
verifyExPost(expost: ExPost): bigint
Operations API Reference
All operation classes and their methods.
Common Interface
All operations implement:
interface IOperation {
type: OperationType;
toCalldata(): Call;
}
FundOperation
class FundOperation {
type: OperationType.Fund;
approve?: Call;
async populateApprove(): Promise<void>;
toCalldata(): Call;
}
TransferOperation
class TransferOperation {
type: OperationType.Transfer;
toCalldata(): Call;
}
WithdrawOperation
class WithdrawOperation {
type: OperationType.Withdraw;
toCalldata(): Call;
}
RollOverOperation
class RollOverOperation {
type: OperationType.Rollover;
toCalldata(): Call;
}
RagequitOperation
class RagequitOperation {
type: OperationType.Ragequit;
toCalldata(): Call;
}
Usage
// Create operation
const op = await account.transfer({...});
// Convert to calldata
const calldata = op.toCalldata();
// Execute
await signer.execute([calldata]);
Types & Interfaces
TypeScript type definitions used in the SDK.
Core Types
PubKey
interface PubKey {
x: bigint;
y: bigint;
}
Elliptic curve point representing a public key.
TongoAddress
type TongoAddress = string & { __type: "tongo" };
Base58-encoded public key string.
CipherBalance
interface CipherBalance {
L: ProjectivePoint;
R: ProjectivePoint;
}
ElGamal ciphertext for encrypted balances.
AEBalance
interface AEBalance {
c0: bigint;
c1: bigint;
c2: bigint;
}
ChaCha20-encrypted hint for faster decryption.
Account Types
AccountState
interface AccountState {
balance: bigint;
pending: bigint;
nonce: bigint;
}
Decrypted account state.
RawAccountState
interface RawAccountState {
balance: CipherBalance;
pending: CipherBalance;
audit?: CipherBalance;
nonce: bigint;
aeBalance?: AEBalance;
aeAuditBalance?: AEBalance;
}
Encrypted account state from contract.
Operation Parameter Types
FundDetails
interface FundDetails {
amount: bigint;
}
TransferDetails
interface TransferDetails {
amount: bigint;
to: PubKey;
}
WithdrawDetails
interface WithdrawDetails {
to: string; // Starknet address
amount: bigint;
}
RagequitDetails
interface RagequitDetails {
to: string; // Starknet address
}
Event Types
AccountEvents
type AccountEvents =
| AccountFundEvent
| AccountTransferOutEvent
| AccountTransferInEvent
| AccountRolloverEvent
| AccountWithdrawEvent
| AccountRagequitEvent;
AccountFundEvent
interface AccountFundEvent {
type: 'fund';
tx_hash: string;
block_number: number;
nonce: bigint;
amount: bigint;
}
AccountTransferOutEvent
interface AccountTransferOutEvent {
type: 'transferOut';
tx_hash: string;
block_number: number;
nonce: bigint;
amount: bigint;
to: string; // Tongo address
}
AccountTransferInEvent
interface AccountTransferInEvent {
type: 'transferIn';
tx_hash: string;
block_number: number;
nonce: bigint;
amount: bigint;
from: string; // Tongo address
}
Utility Functions
derivePublicKey
function derivePublicKey(privateKey: bigint): PubKey
pubKeyAffineToBase58
function pubKeyAffineToBase58(pub: PubKey): TongoAddress
pubKeyBase58ToAffine
function pubKeyBase58ToAffine(b58string: string): { x: bigint; y: bigint }
Complete Workflow Example
End-to-end example: Fund → Transfer → Rollover → Withdraw.
Setup
import { Account as TongoAccount } from "@fatsolutions/tongo-sdk";
import { Account, RpcProvider } from "starknet";
const provider = new RpcProvider({
nodeUrl: "https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_8/YOUR_API_KEY",
specVersion: "0.8.1",
});
const signer = new Account({
provider,
address: "YOUR_ADDRESS",
signer: "YOUR_PRIVATE_KEY"
});
// Tongo contract address on Sepolia
// This contract wraps STRK with a 1:1 rate (1 STRK = 1 Tongo STRK)
const tongoAddress = "0x00b4cca30f0f641e01140c1c388f55641f1c3fe5515484e622b6cb91d8cee585";
// Create two Tongo accounts
const account1 = new TongoAccount(82130983n, tongoAddress, provider);
const account2 = new TongoAccount(12930923n, tongoAddress, provider);
Important Prerequisites:
- Your Starknet account (
YOUR_ADDRESS) must have:
- Testnet ETH for gas fees
- STRK tokens (this contract wraps STRK on Sepolia)
- Get both from: https://starknet-faucet.vercel.app/
- The funding operation will approve the Tongo contract to spend your STRK tokens
1. Fund Account 1
console.log("=== Funding Account 1 ===");
// Fund with 100 Tongo units (Tongo uses 32-bit balances)
const fundOp = await account1.fund({ amount: 100n });
await fundOp.populateApprove();
const fundTx = await signer.execute([
fundOp.approve!,
fundOp.toCalldata()
]);
console.log("Fund TX:", fundTx.transaction_hash);
await provider.waitForTransaction(fundTx.transaction_hash);
const state1 = await account1.state();
console.log("Account 1 balance:", state1.balance); // 100n
2. Transfer to Account 2
console.log("=== Transferring to Account 2 ===");
// Transfer 25 Tongo units to account 2
const transferOp = await account1.transfer({
to: account2.publicKey,
amount: 25n
});
const transferTx = await signer.execute(transferOp.toCalldata());
console.log("Transfer TX:", transferTx.transaction_hash);
await provider.waitForTransaction(transferTx.transaction_hash);
// Check sender
const state1After = await account1.state();
console.log("Account 1 balance:", state1After.balance); // 75n
// Check recipient
const state2 = await account2.state();
console.log("Account 2 pending:", state2.pending); // 25n
3. Rollover Account 2
console.log("=== Rolling Over Account 2 ===");
const rolloverOp = await account2.rollover();
const rolloverTx = await signer.execute(rolloverOp.toCalldata());
console.log("Rollover TX:", rolloverTx.transaction_hash);
await provider.waitForTransaction(rolloverTx.transaction_hash);
const state2After = await account2.state();
console.log("Account 2 balance:", state2After.balance); // 25n
console.log("Account 2 pending:", state2After.pending); // 0n
4. Withdraw from Account 2
console.log("=== Withdrawing from Account 2 ===");
// Withdraw 10 Tongo units back to ERC20
const withdrawOp = await account2.withdraw({
to: signer.address,
amount: 10n
});
const withdrawTx = await signer.execute(withdrawOp.toCalldata());
console.log("Withdraw TX:", withdrawTx.transaction_hash);
await provider.waitForTransaction(withdrawTx.transaction_hash);
const state2Final = await account2.state();
console.log("Account 2 final balance:", state2Final.balance); // 15n
Key Concepts
- Tongo Units: Amounts are in Tongo units (32-bit integers, max: 4,294,967,295)
- 1:1 Rate: This contract wraps STRK with a 1:1 conversion rate
- 32-bit Limit: Cannot use full STRK decimal amounts (10^18), use smaller integers
- Fund Operation: Requires both approval and fund call (use array with both)
- Other Operations: Single call each (transfer, rollover, withdraw)
Complete Script
async function completeWorkflow() {
// 1. Fund with 100 Tongo units
const fundOp = await account1.fund({ amount: 100n });
await fundOp.populateApprove();
const fundTx = await signer.execute([fundOp.approve!, fundOp.toCalldata()]);
await provider.waitForTransaction(fundTx.transaction_hash);
// 2. Transfer 25 Tongo units
const transferOp = await account1.transfer({
to: account2.publicKey,
amount: 25n
});
const transferTx = await signer.execute(transferOp.toCalldata());
await provider.waitForTransaction(transferTx.transaction_hash);
// 3. Rollover
const rolloverOp = await account2.rollover();
const rolloverTx = await signer.execute(rolloverOp.toCalldata());
await provider.waitForTransaction(rolloverTx.transaction_hash);
// 4. Withdraw 10 Tongo units
const withdrawOp = await account2.withdraw({
to: signer.address,
amount: 10n
});
const withdrawTx = await signer.execute(withdrawOp.toCalldata());
await provider.waitForTransaction(withdrawTx.transaction_hash);
console.log("Complete workflow finished!");
}
React Integration Example
Using Tongo SDK in a React application.
Setup with React Hooks
import { useAccount, useProvider } from '@starknet-react/core';
import { Account as TongoAccount } from '@fatsolutions/tongo-sdk';
import { useState, useEffect } from 'react';
function useTongoAccount() {
const { account } = useAccount();
const { provider } = useProvider();
const [tongoAccount, setTongoAccount] = useState<TongoAccount | null>(null);
const [balance, setBalance] = useState<bigint>(0n);
const [pending, setPending] = useState<bigint>(0n);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (account && provider) {
const tongoPrivateKey = 82130983n;
// Tongo contract on Sepolia (wraps STRK with 1:1 rate)
const tongoAddress = "0x00b4cca30f0f641e01140c1c388f55641f1c3fe5515484e622b6cb91d8cee585";
const tAccount = new TongoAccount(tongoPrivateKey, tongoAddress, provider);
setTongoAccount(tAccount);
refreshBalance();
}
}, [account, provider]);
async function refreshBalance() {
if (!tongoAccount) return;
setLoading(true);
try {
const state = await tongoAccount.state();
setBalance(state.balance);
setPending(state.pending);
} finally {
setLoading(false);
}
}
return { tongoAccount, balance, pending, loading, refreshBalance };
}
Transfer Component
function TransferForm() {
const { tongoAccount, refreshBalance } = useTongoAccount();
const { account } = useAccount();
const [amount, setAmount] = useState('');
const [loading, setLoading] = useState(false);
async function handleTransfer() {
if (!tongoAccount || !account) return;
setLoading(true);
try {
const transferOp = await tongoAccount.transfer({
to: recipientPubKey,
amount: BigInt(amount)
});
const tx = await account.execute(transferOp.toCalldata());
await provider.waitForTransaction(tx.transaction_hash);
await refreshBalance();
alert('Transfer successful!');
} catch (error) {
alert(`Failed: $${error.message}`);$$
} finally {
setLoading(false);
}
}
return (
<div>
<input value={amount} onChange={(e) => setAmount(e.target.value)} />
<button onClick={handleTransfer} disabled={loading}>Send</button>
</div>
);
}
Balance Display
function BalanceDisplay() {
const { balance, pending, loading } = useTongoAccount();
return (
<div>
<h2>Balance</h2>
{loading ? <p>Loading...</p> : (
<>
<p>Balance: {balance.toString()}</p>
<p>Pending: {pending.toString()}</p>
</>
)}
</div>
);
}