# Transaction Flow

> End-to-end lifecycle of a private transaction in Senddy.

Source: https://www.senddy.com/docs/technical/transaction-flow
Last modified: 2026-08-02T02:33:05.000Z

---

## Overview

A private transaction in Senddy goes through six phases: **note selection**, **proof generation**, **attestation**, **relay**, **on-chain execution**, and **sync**. The diagram below shows the full flow.

### Base deposit and shield flow

Before private spending begins, a Base deposit enters through a one-time EOA:

```mermaid
sequenceDiagram
    participant Funder
    participant EOA as Ephemeral EOA
    participant App
    participant Relay
    participant USDC
    participant Pool

    App->>App: Derive fresh EOA + random depositId
    Funder->>EOA: Transfer USDC
    App->>App: Multicall balanceOf + deposits(depositId)
    App->>EOA: Sign ReceiveWithAuthorization
    App->>Relay: Submit validated deposit operation
    Relay->>Pool: depositWithEIP3009
    Pool->>USDC: receiveWithAuthorization
    Pool->>Pool: Record depositId and ephemeral depositor
    App->>EOA: Sign ShieldAuth
    App->>Relay: Submit shield
    Relay->>Pool: executeShieldAttested
    Pool->>Pool: Consume deposit and emit commitment
    App->>App: Retire EOA
```

The random `depositId` gives the client a constant-time on-chain lookup. It does not scan historical blocks: active deposit records are reconciled through a single multicall, and an existing unconsumed deposit can resume shielding after a reload or relay timeout.

```mermaid
sequenceDiagram
    participant User
    participant App
    participant ZK Prover
    participant Attestor
    participant Relay
    participant Pool Contract
    participant Subgraph

    User->>App: Send (recipient, amount)

    rect rgba(42, 169, 255, 0.06)
    Note over App: Note Selection
    App->>App: Fetch unspent notes (UTXOs)
    App->>App: Select notes & compute change
    App->>App: Build Merkle paths from subgraph
    end

    rect rgba(42, 169, 255, 0.10)
    Note over App,ZK Prover: Proof Generation
    App->>ZK Prover: Generate spend proof (Noir / UltraHonk)
    ZK Prover-->>App: Proof + nullifiers + output commitments
    end

    rect rgba(42, 169, 255, 0.06)
    Note over App,Attestor: Attestation
    App->>Attestor: Request attestation
    Attestor-->>App: Signed attestation
    end

    rect rgba(42, 169, 255, 0.10)
    Note over App,Pool Contract: On-chain Execution
    App->>Relay: Submit transaction (Gelato)
    Relay->>Pool Contract: Execute spend calldata
    Pool Contract->>Pool Contract: Verify ZK proof
    Pool Contract->>Pool Contract: Record nullifiers (prevent double-spend)
    Pool Contract->>Pool Contract: Append new commitments to Merkle tree
    end

    rect rgba(42, 169, 255, 0.06)
    Note over App,Subgraph: Sync
    Pool Contract-->>Subgraph: Commitment & Nullifier events
    Subgraph-->>App: New commitments & nullifiers
    App->>App: Decrypt memos & update local notes
    App-->>User: Balance updated
    end
```

## Phase Breakdown

### 1. Note Selection

When the user initiates a send, the app fetches all **unspent notes** from local storage (IndexedDB). Notes are sorted by value and selected greedily — largest first — to cover the target amount. If the selected notes exceed the amount, a **change note** is created for the remainder.

For each selected note, the app fetches the current commitment tree from the [subgraph](/docs/technical/subgraph) and builds a **Merkle path** proving membership.

### 2. Proof Generation

The app generates a zero-knowledge proof using a [Noir circuit](/docs/technical/circuits) compiled with UltraHonk. The proof attests to:

- **Ownership** — the sender knows the secret key for each input note
- **Existence** — each input note's commitment exists in the on-chain Merkle tree
- **Conservation** — the sum of inputs equals the sum of outputs (no value created or destroyed)
- **Correctness** — nullifiers and output commitments are correctly derived

The circuit supports up to 3 inputs (spend3) or 9 inputs (spend9) for note consolidation.

### 3. Attestation

The proof is sent to the **attestor** — a trusted enclave that verifies compliance requirements and signs the transaction. The attestor's signature is required by the [pool contract](/docs/technical/smart-contracts) before accepting any spend.

### 4. On-chain Execution

The signed transaction is submitted through **Gelato Relay**, so the user never needs to hold ETH for gas. The relay calls the pool contract, which:

1. Verifies the ZK proof
2. Checks the attestor signature
3. Records each nullifier (preventing double-spend)
4. Appends new output commitments to the Merkle tree

### 5. Sync

After the transaction is confirmed, the **sync engine** detects the new on-chain events via the subgraph. Both the sender and recipient's clients:

- Fetch new commitments and nullifiers
- Attempt to decrypt attached memos using their view keys
- Store any decrypted notes locally and update balances
