Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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

FundTransferRolloverWithdraw

  1. Fund: Convert ERC20 to encrypted balance
  2. Transfer: Send hidden amounts with ZK proofs
  3. Rollover: Claim pending incoming transfers
  4. 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:

  1. Brute force: Iterate \(g^i\) for \(i = 0, 1, 2, \ldots\) until matching \(g^b\)
  2. Baby-step Giant-step: More efficient \(O(\sqrt{n})\) algorithm
  3. 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 key
  • audit_balance: Same balance encrypted for global auditor's key
  • pending: 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_balance is 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\):

  1. Public inputs: \(b\) (revealed in ERC20 transfer), \(y\) (user's public key)
  2. Encryption: \(\text{Enc}[y](b, 1) = (g^b y, g)\)
  3. 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:

  1. Create encryptions for sender, receiver, and auditor
  2. Generate ZK proofs to validate the transaction
  3. 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:

  1. Prevents balance corruption: Malicious actors can't modify someone's main balance
  2. Enables atomic proofs: Senders prove against a known balance state
  3. 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:

  1. Ownership: Knowledge of \(x\) such that \(y = g^x\) (POE)
  2. Blinding: Knowledge of \(r\) such that \(R = g^r\) (POE)
  3. Sender encryption: \(L = g^b y^r\) (PED)
  4. Third-party encryption: \(\bar{L} = g^b \bar{y}^r\) (PED)
  5. 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 proofs
  • verify_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 Exponent
  • she::protocols::poe2 - Double exponent proofs
  • she::protocols::poeN - N-exponent proofs
  • she::protocols::bit - Bit proofs (OR construction)
  • she::protocols::range - Range proofs via bit decomposition
  • she::protocols::ElGamal - ElGamal encryption proofs
  • she::protocols::SameEncryption - Same message proofs
  • she::protocols::SameEncryptionUnknownRandom - Without known randomness

Performance

TypeScript (Client-Side)

OperationTime
Fund proof~50ms
Transfer proof2-3s
Withdraw proof1-2s
Rollover proof~10ms
Decryption (with hint)< 1ms
Decryption (brute-force 1M)~100ms

Cairo (On-Chain)

OperationCairo 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

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 x given g^x
  • Computational Diffie-Hellman: Hard to compute g^(xy) from g^x and g^y
  • Stark Curve Security: 256-bit security level

Performance

Typical performance on modern hardware:

OperationTime
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

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\):

  1. Compute \(g^b = L / R^x = L / (g^r)^x = (g^b \cdot y^r) / (g^{rx}) = g^b\)
  2. 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:

  1. POE for \(R\): Prove \(R = g^r\) (knowledge of \(r\))
  2. 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_mul operations
  • 2 ec_add operations

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:

  1. Commitment: Prover commits to random values
  2. Challenge: Verifier provides challenge (computed via Fiat-Shamir)
  3. 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:

  • prefix binds the proof to a specific context (nonce, contract address, etc.)
  • \(A_i\) are the commitment points
  • Hash is 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_mul operations
  • 1 ec_add operation
  • ~2,500 Cairo steps

Usage in Tongo

POE is used for:

  1. Ownership Proofs: Prove account ownership during fund/transfer/withdraw
  2. Blinding Factor: Prove knowledge of randomness in transfers
  3. 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

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_mul operations
  • 3 ec_add operations
  • ~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\):

  1. Commit to each bit: \(V_i = g^{b_i} \cdot h^{r_i}\)
  2. Prove each \(b_i \in {0, 1}\) using bit proof
  3. Verify commitment consistency: \(\prod_{i=0}^{31} V_i^{2^i} = g^b \cdot h^r\)

Next Steps

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:

  1. Transfer amounts: \(b \in [0, 2^{32})\)
  2. Remaining balances: \(b_{\text{after}} = b_{\text{before}} - b_{\text{transfer}} \in [0, 2^{32})\)
  3. Withdrawal amounts: \(b \in [0, 2^{32})\)
  4. No negative balances: Prevents underflow attacks

Next Steps

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:

  1. Transfers: Prove amount encrypted for sender, receiver, and auditor is identical
  2. Auditing: Prove audit ciphertext matches transfer amount
  3. Viewing Keys: Prove additional encryptions match transfer amount

Next Steps

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:

  • Account class for managing Tongo accounts
  • Operation objects for transactions (FundOperation, TransferOperation, etc.)
  • State management and decryption utilities
  • Event querying and transaction history

Package Information

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

  1. Install the SDK
  2. Follow the Quick Start guide
  3. Learn about Accounts and Operations

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 SDKStarknet.jsNode.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

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:

  1. Fund - Convert ERC20 tokens to encrypted balance
  2. Transfer - Send confidential transfers
  3. Rollover - Claim pending incoming transfers
  4. 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:

  1. Fund - Convert ERC20 tokens to encrypted balance
  2. Transfer - Send encrypted amounts to another account
  3. Withdraw - Convert encrypted balance back to ERC20
  4. Rollover - Claim pending incoming transfers
  5. 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

  1. Generates ZK proof of funding
  2. Creates encrypted balance ciphertext
  3. Optionally creates audit ciphertext (if auditor is set)
  4. 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

  1. Encrypts transfer amount for recipient
  2. Encrypts new balance for sender
  3. Generates ZK proof that:
    • Sender knows their private key
    • Transfer amount ≤ sender balance
    • New balance is correctly computed
  4. Creates encrypted pending balance for recipient
  5. 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

  1. Generates ZK proof that:
    • User knows their private key
    • Withdrawal amount ≤ current balance
    • New balance is correctly computed
  2. Creates encrypted new balance
  3. Creates AE hint for new balance
  4. Transfers ERC20 tokens to destination address

Important Notes

  • The to address 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

  1. Reads current balance and pending balance
  2. Generates ZK proof of knowledge of private key
  3. Computes new balance = old balance + pending
  4. Creates encrypted new balance
  5. 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

  1. Withdraws entire balance to specified address
  2. Zeroes out encrypted balance
  3. 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 withdraw for 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:

  1. Generates the proof using the SHE library
  2. Includes it in the operation object
  3. 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:

OperationCairo StepsRelative Cost
Fund~50KLow
Transfer~120KMedium
Rollover~80KLow
Withdraw~80KLow
Ragequit~80KLow

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:

  • g is the Stark curve generator
  • y is 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

The easiest way to get decrypted balances:

const state = await account.state();
console.log(state);
// { balance: 5000n, pending: 500n, nonce: 2n }

This method:

  1. Fetches raw state from contract
  2. Decrypts AE hints (if available)
  3. Decrypts CipherBalances using hints
  4. 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:

  1. Prevent replay attacks: Each operation increments nonce
  2. Order operations: Nonce must match expected value
  3. Key derivation: Used in AE hint encryption

Nonce Behavior

  • Starts at 0n for 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: bigint or Uint8Array
  • 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^privateKey where g is 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 });

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

  1. Hardware Wallet Integration

    • Store derivation parameters in hardware wallet
    • Sign derivation message when needed
  2. Encrypted Backup

    • Encrypt private key with strong password
    • Store encrypted backup securely
    • Test recovery before relying on it
  3. 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

  1. Generation

    • Use cryptographically secure randomness
    • Or derive deterministically from wallet
  2. Usage

    • Never log or print the key
    • Keep in memory only when needed
    • Clear from memory after use
  3. Storage

    • Encrypt at rest
    • Use secure key management systems
    • Regular security audits
  4. 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

  1. Approve Tongo contract to spend ERC20 tokens
  2. Contract transfers tokens from your wallet
  3. Encrypted balance increases
  4. ZK proof verifies the operation
  5. 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:

  1. ERC20 Approval: Allow Tongo to spend tokens
  2. 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

  1. Sender balance decreases
  2. Recipient pending increases
  3. 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

  1. Encrypted balance is decreased
  2. ERC20 tokens are transferred to destination address
  3. Amount is converted using contract rate
  4. 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:

  1. Starknet Account: For signing transactions and paying gas
  2. 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 Uint8Array
  • contractAddress: Tongo contract address
  • provider: 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>
    );
}