> For the complete documentation index, see [llms.txt](https://hinkal-team.gitbook.io/hinkal/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hinkal-team.gitbook.io/hinkal/protocol/solana-program/instructions.md).

# Instructions

The user-facing instructions exposed by `lib.rs`. They fall into four groups: **proof staging**, **transact / transfer / swap** (proof-bearing), **proofless deposit**, and **admin / lifecycle**.

#### Proof staging

Solana transactions cannot fit a Groth16 proof, all public signals, encrypted outputs, and a multi-hop CPI batch in one message. Hinkal stages them into PDAs first, then references those PDAs from the final proof-bearing instruction. All staging PDAs are seeded by `(deployer, signer, id)` so a single signer can run multiple flows in flight.

| Instruction                                                              | Account it populates      | Fields                                                                                  |
| ------------------------------------------------------------------------ | ------------------------- | --------------------------------------------------------------------------------------- |
| `init_nullifier(nullifier)`                                              | `NullifierAccount`        | `is_used = false`. Required because `init_if_needed` on every nullifier exhausts stack. |
| `close_nullifier(nullifier)`                                             | (closes)                  | Relayers only. Returns rent to signer. Reverts if `is_used`.                            |
| `store_proof_part1(id, proof_a, proof_b, proof_c, public_signals_part1)` | `ProofAccountPart1`       | Proof bytes + first slice of public signals (≤ 20).                                     |
| `store_proof_part2(id, public_signals_part2)`                            | `ProofAccountPart2`       | Remaining public signals (≤ 100).                                                       |
| `store_encrypted_outputs(id, encrypted_outputs)`                         | `EncryptedOutputsAccount` | Up to 2 encrypted UTXO blobs.                                                           |
| `store_instructions(id, instructions)`                                   | `InstructionsAccount`     | CPI batch for swaps; unused for transact/transfer.                                      |

`merge_proof_data.rs` reads the three staged PDAs and reassembles a `MergedProofData` struct for the proof pipeline.

#### Transact (deposit / withdraw)

**`transact2(id, forbid_deposit, in_nullifiers, relayer_fee, dimensions=1×2×1)`**

**`transact6(id, forbid_deposit, in_nullifiers, relayer_fee, dimensions=1×6×1)`**

Bridges a wallet's public balance to or from the shielded vault.

* If `mint` is `None` → SOL flow (`perform_transact_funds`); otherwise → SPL flow (`perform_transact_tokens`).
* `forbid_deposit = true` rejects flows whose net `amount_change` is non-negative.
* Net positive `amount_change` → `deposit_funds` from signer to vault.
* Net negative → vault transfers `|amount_change| - relayer_fee` to recipient and `relayer_fee` to signer (signer is the relayer).
* The `1×6×1` variant supports up to 6 input nullifiers per leg, used when consolidating fragmented UTXOs.
* After movement, `validate_*_balance_diff` enforces the closing balance equation.

#### Transfer (shielded → shielded)

**`transfer2(id, in_nullifiers, relayer_fee, dimensions=1×2×2)`**

**`transfer6(id, in_nullifiers, relayer_fee, dimensions=1×6×2)`**

Moves value between two shielded UTXOs without exiting the vault. Internally calls `transact_common` with `forbid_deposit = true` and recipient pinned to the system program (no on-chain recipient). The only fund movement is paying `relayer_fee` from the vault to the signer.

#### Swap (shielded → DEX → shielded)

**`swap2(id, swapper_account_additional_seed, in_nullifiers, relayer_fee, variable_rate, dimensions=2×2×1)`**

**`swap6(id, swapper_account_additional_seed, in_nullifiers, relayer_fee, variable_rate, dimensions=2×6×1)`**

Routes shielded value through any program reachable via CPI. Both variants share `swap_common`:

1. `validate_swap_atas` - the supplied `mint_from` / `mint_to` and their ATAs must be consistent (no SOL pretending to be a token, etc.).
2. `process_proof_and_state` with `on_chain_creation = [false, true]` - the input leg uses proof-derived nullifiers/commitments; the output leg's commitment is built on-chain from the actual swap result.
3. `swapper_account_additional_seed` from the proof must match the seed used to derive the swapper PDA (`SwapperAccountAdditionalSeedMismatch`).
4. `determine_swap_mode` → `TokenToToken | TokenToSol | SolToToken`.
5. Vault → swapper PDA: input liquidity is moved to the swapper (which is the CPI authority).
6. `perform_swap` invokes the staged `InstructionsAccount` against `remaining_accounts`. Account index `255` is replaced with the swapper PDA at execution time and the call is signed via `invoke_signed` with the swapper seeds. Self-CPI back into Hinkal is rejected (`SelfCPIForbidden`).
7. `swapped_amount` = balance delta on the destination ATA / SOL account.
8. `calculate_total_fee = relayer_fee + swapped_amount * variable_rate / 10000`. `variable_rate` is in basis points × 10 (i.e. denominator 10\_000).
9. Fee is paid to the signer; the remainder is the UTXO amount. A new commitment is built on-chain from `(amount, mint_to, stealth_address_structure, timestamp)` and inserted into the Merkle tree.
10. `validate_token_balance_diff` / `validate_sol_balance_diff` is run on both legs to enforce the closing equation.

#### Proofless deposits

**`proofless_deposit(amounts[], stealth_address_structure[], encrypted_blobs[], create_blocked_utxos, timestamp: Option<i64>)`**

1..N outputs in a single instruction. SOL or SPL, selected by whether `mint` is supplied. `timestamp` overrides the block clock for the resulting UTXO(s) when supplied (`None` falls back to the on-chain clock). When `create_blocked_utxos = true`, also emits `BlockedUtxosCreated { sender, mint, amounts }` so a downstream service can build "blocked" UTXOs (e.g. for cooldowns).

Calls `proofless_deposit_common`:

* `amount > 0` for every output.
* If `access_token_owner` is supplied as a signer, validate it against `AccessTokenStorageAccount.access_token_owner_key`. Otherwise, fall back to `is_access_token_required(total_amount, token_limit)` — if `total_amount` exceeds the per-mint limit, revert with `AccessTokenRequired`.
* Run the deposit (SOL via `deposit_funds`, SPL via `deposit_tokens`) and assert `balance_diff == total_amount`.
* For each output, build an on-chain UTXO via `build_onchain_utxo` and insert it into the Merkle tree (`process_onchain_commitments`).

#### Admin & lifecycle

| Instruction                                | Authority            | Effect                                                                                                                      |
| ------------------------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `initialize`                               | `original_deployer`  | Allocates and seeds storage, both Merkle trees, ownership, and access-token storage. Funds the vault PDA with rent-minimum. |
| `transfer_ownership(new_owner)`            | current `owner`      | Sets `pending_owner`. Emits `OwnershipTransferred`.                                                                         |
| `accept_ownership`                         | `pending_owner`      | Promotes pending → current. Emits `OwnershipAccepted`.                                                                      |
| `update_relayer_status(status)`            | `owner`              | Adds or removes a `relayer` from the whitelist. Emits `RelayerStatusUpdated`.                                               |
| `init_token_limit`                         | anyone (rent payer)  | Allocates a per-`(deployer, mint)` `TokenLimitStorageAccount` with `amount_limit = 0`.                                      |
| `update_token_limit(amount)`               | `owner`              | Sets the per-mint deposit/withdraw threshold. Emits `TokenLimitUpdated`.                                                    |
| `add_access_token(access_key)`             | `access_token_owner` | Creates the `AccesskeyAccount` lock and inserts `access_key` into the access-token Merkle tree. Emits `NewAccessKeyAdded`.  |
| `change_access_token_owner_key(new_owner)` | `owner`              | Rotates `access_token_owner_key`. Emits `AccessTokenOwnerChanged`.                                                          |
