├── .env.example ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Cargo.lock ├── Cargo.toml ├── README.md ├── docs ├── FAQ.md ├── auditor_rotation.md ├── block_explorers.md ├── product_guide.md ├── project_structure.md ├── recipes.md ├── setup.md ├── wallet_guide.md ├── wallet_guide_balances.md └── wallet_guide_transfers.md ├── ingredients ├── apply_pending_balance │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── deposit_tokens │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── global_auditor_assert │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── mint_tokens │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── setup_mint │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── setup_mint_confidential │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── setup_participants │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── setup_token_account │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── transfer │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── withdraw_tokens │ ├── Cargo.toml │ └── src │ └── lib.rs ├── recipes ├── Cargo.toml └── src │ └── lib.rs ├── runtime_output.env └── utils ├── Cargo.toml └── src ├── gcp.rs ├── jito.rs └── lib.rs /.env.example: -------------------------------------------------------------------------------- 1 | # Solana Confidential Transfers Cookbook Environment Configuration 2 | 3 | # ==== REQUIRED CONFIGURATION ==== 4 | # RPC_URL: Required for all recipes and ingredients 5 | RPC_URL="https://api.devnet.solana.com" 6 | 7 | # ==== RECOMMENDED CONFIGURATION ==== 8 | # While not strictly required (will be auto-generated if missing), 9 | # maintaining a consistent fee payer in your .env file is highly recommended. 10 | # This prevents you from having to fund a new fee payer each time runtime_output.env is deleted. 11 | # The auto-generated fee payer will be stored in runtime_output.env, not .env. 12 | fee_payer_keypair=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 13 | 14 | # ==== TURNKEY INTEGRATION (Optional) ==== 15 | # Required only when running basic_transfer_recipe_turnkey 16 | # Can be omitted if not using Turnkey functionality 17 | TURNKEY_API_PUBLIC_KEY="your_api_public_key" 18 | TURNKEY_API_PRIVATE_KEY="your_api_private_key" 19 | TURNKEY_ORGANIZATION_ID="your_organization_id" 20 | TURNKEY_SENDER_PRIVATE_KEY_ID="your_solana_sender_private_key_id" 21 | TURNKEY_SENDER_PUBLIC_KEY="your_solana_sender_public_key" 22 | TURNKEY_RECEIVER_PRIVATE_KEY_ID="your_solana_receiver_private_key_id" 23 | TURNKEY_RECEIVER_PUBLIC_KEY="your_solana_receiver_public_key" 24 | 25 | # ==== GOOGLE CLOUD KMS INTEGRATION (Optional) ==== 26 | # Required only when running basic_transfer_recipe_gcp 27 | # Can be omitted if not using Google Cloud KMS functionality 28 | GOOGLE_APPLICATION_CREDENTIALS="path/to/your/gcp_credentials.json" 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /test-ledger 2 | /target 3 | .env 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#e46445", 4 | "activityBar.background": "#e46445", 5 | "activityBar.foreground": "#15202b", 6 | "activityBar.inactiveForeground": "#15202b99", 7 | "activityBarBadge.background": "#58e774", 8 | "activityBarBadge.foreground": "#15202b", 9 | "commandCenter.border": "#e7e7e799", 10 | "sash.hoverBorder": "#e46445", 11 | "statusBar.background": "#d7431f", 12 | "statusBar.foreground": "#e7e7e7", 13 | "statusBarItem.hoverBackground": "#e46445", 14 | "statusBarItem.remoteBackground": "#d7431f", 15 | "statusBarItem.remoteForeground": "#e7e7e7", 16 | "titleBar.activeBackground": "#d7431f", 17 | "titleBar.activeForeground": "#e7e7e7", 18 | "titleBar.inactiveBackground": "#d7431f99", 19 | "titleBar.inactiveForeground": "#e7e7e799" 20 | }, 21 | "peacock.color": "#d7431f", 22 | "files.associations": { 23 | "runtime_output.env": "properties" 24 | } 25 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "utils", 5 | "recipes", 6 | "ingredients/*" 7 | ] 8 | 9 | [workspace.dependencies] 10 | # Solana dependencies 11 | solana-sdk = "2.1.11" 12 | solana-client = "2.1.11" 13 | solana-transaction-status-client-types = "2.1.11" 14 | solana-zk-sdk = "2.1.11" 15 | 16 | # SPL dependencies 17 | spl-token-2022 = { git = "https://github.com/kilogold/token-2022.git", branch = "cli_transaction_generation" } 18 | spl-token-client = { git = "https://github.com/kilogold/token-2022.git", branch = "cli_transaction_generation" } 19 | spl-associated-token-account = { git = "https://github.com/solana-labs/solana-program-library.git", rev = "224a96b164357a221c7f7dae5e348f9c0f7a73da" } 20 | spl-token-confidential-transfer-proof-generation = { git = "https://github.com/kilogold/token-2022.git", branch = "cli_transaction_generation" } 21 | spl-token-confidential-transfer-proof-extraction = { git = "https://github.com/kilogold/token-2022.git", branch = "cli_transaction_generation" } 22 | 23 | # Other dependencies 24 | tk-rs = { git = "https://github.com/kilogold/tk-rs.git" } 25 | tokio = { version = "1.42.0", features = ["full"] } 26 | serde = "1.0.215" 27 | serde_json = "1.0.1" 28 | bs58 = "0.5.1" 29 | dotenvy = "0.15.7" 30 | google-cloud-kms = "0.6.0" 31 | base64 = "0.22.1" 32 | jito-sdk-rust = "0.1.0" 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Confidential Balances Cookbook 5 | 6 | A collection of ingredients (tests) and recipes (test sequences) demonstrating Solana confidential transfer patterns. 7 | 8 | Examples in this repository are standalone demonstrations of confidential transfer flows, similar to those found in the [Token Program CLI](https://github.com/solana-program/token-2022/tree/969cff212c0e0add812932e50f6771933f14ff5c/clients/cli). 9 | 10 | Use this as a guide to implement confidential balances in your own applications. 11 | Although the API is still work-in-progress, the usage patterns remain stable. 12 | 13 | ## Content 14 | ### [Product Guide](docs/product_guide.md) 15 | 16 | ### [Setup](docs/setup.md) 17 | 18 | ### [Project Structure](docs/project_structure.md) 19 | 20 | ### [Recipes](docs/recipes.md) 21 | 22 | ### [FAQ](docs/FAQ.md) 23 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## Can an initialized mint retroactively add confidential transfers extension? 4 | No. However, there is an alternative: [token wrap program](https://github.com/solana-program/token-wrap). It's important to understand that pursuing token wrapping indirectly fragments liquidity since the Solana ecosystem (DeFi, dApps, etc.) recognizes the wrapped token as a separate token mint. Plan judiciously. 5 | 6 | ## How to confidentialize a Confidential Transfers mint extension without also adding the Confidential MintBurn extension? 7 | 8 | This is a creative operational solution: instead of minting on-demand, the issuer maintains a treasury account with the total supply. While the total supply remains public, individual transfer amounts stay confidential by originating all transfers from the issuer's account. The issuer pre-mints the total supply, then confidentially transfers partial amounts to recipients, creating a quasi-mint-burn mechanism. 9 | 10 | ```mermaid 11 | graph TD 12 | subgraph Token Mint 13 | TM[Token Mint] 14 | end 15 | 16 | subgraph Issuer 17 | ITA[Issuer Token Account] 18 | end 19 | 20 | subgraph Users 21 | UTA1[User Token Account 1] 22 | UTA2[User Token Account 2] 23 | UTA3[User Token Account 3] 24 | end 25 | 26 | TM -->|"Mints total supply"| ITA 27 | ITA -->|"Confidential transfer"| UTA1 28 | ITA -->|"Confidential transfer"| UTA2 29 | ITA -->|"Confidential transfer"| UTA3 30 | ``` 31 | 32 | ## What is the minimum number of CU's, accounts, and transactions needed to make a Confidential Transfer? 33 | Compute Unit (CU) usage depends on implementation details. For example, storing zk proofs in context state accounts (as primarily exemplified in this repository) versus being part of the confidential transfer instruction data. 34 | 35 | Below are mainnet examples for Confidential Balances, along with their respective CU usage. 36 | 37 | |Operation|Compute Units|Example| 38 | |---|---|---| 39 | |Realloc|6,155| [Solscan](https://solscan.io/tx/4NaK8Br354eWXXoQUoD9qQbWJeWmEkZT8uUND1sqnZixoV9bvxJ1p6E1fUkRcx64Yh7rZNmba1Tyeb9cxXdSY9gr)| 40 | |Configure|4,121| [Solscan](https://solscan.io/tx/5p82KLSCo89Gwx2CmNpGwB73S8QCaUBnRh1N3xAQSt55aJbEnWK7e86EaCzXAqFNVRRCNf1geoB5mq4JuNwFDRXQ)| 41 | |Deposit|14,526| [Solscan](https://solscan.io/tx/2Hvb1hJDt5qeYuUMWmgMGCinvW6TkVixRZgNLGyPTnfRDkyBuFK5NJLzi5KkWxh9NCK7hoBpbawnK9xaUX2AArRZ)| 42 | |Apply|16,748| [Solscan](https://solscan.io/tx/4riLZhKNkwgcypQ9gQ15go4jgnDGGX4PCuNSKNDpb8Motj9XNx9eiBYPdL5PvaYKncQbEGnctEa7NAKARMDQtGzQ)| 43 | |Withdraw|9,958| [Solscan](https://solscan.io/tx/2YpFD8Fz5QJKCt7qrcrCxwm66NWU1qPzhps1QKWkGeUnmCJzBXQ4Dpkvj8fnvG6Ag86ieShqKsk14PJ4BhkNnZ2R)| 44 | |Transfer|30,654| [Solscan](https://solscan.io/tx/2AEo6cHY7pdQJAtFZ7VgmfdKQe7LEpR7cSuxMq1xTmjxhtwYJasSbn4x9LkKrcpgzDi7jiPjB6tLS5pPzMKahirN?cluster=devnet)| 45 | 46 | ## How do I use Confidential Balances with PDAs? 47 | The initial use-cases for Confidential Balances are primarily peer-to-peer transfers. 48 | 49 | Programs using PDAs to manage confidential tokens face two key challenges: 50 | 51 | 1. **Encryption Key Management**: While PDAs could technically store encryption keys, this is insecure as account data is public and would expose private keys. Additionally, generating fresh keys requires randomness unavailable in deterministic programs. 52 | 53 | 2. **ZK Proof Generation**: Confidential transfers require zero-knowledge proofs that must be generated using private encryption keys. These operations are computationally expensive and typically performed client-side, though exact on-chain computation feasibility requires benchmarking. 54 | 55 | Practical implementation involves this split of responsibilities: 56 | 57 | ```mermaid 58 | sequenceDiagram 59 | participant Client 60 | participant Program 61 | participant Token2022 62 | 63 | Client->>Client: 1.Generate ElGamal/AES keys 64 | Client->>Client: 2.Create ZK proofs 65 | Client->>Program: 3.Submit tx with proofs 66 | Program->>Program: 4.Verify proofs 67 | Program->>Token2022: 5.PDA signs transfer 68 | Token2022->>Token2022: 6.Execute confidential transfer 69 | ``` 70 | 71 | For example, in a simple escrow contract: 72 | - User: Generates encryption keys, creates proofs for deposit/withdrawal 73 | - Escrow Program: Verifies proofs, uses PDA to authorize transfers 74 | - Token-2022: Processes the confidential transfers when authorized 75 | 76 | This requires user participation for any confidential operation. The program can verify and authorize but cannot independently generate proofs or decrypt balances. 77 | -------------------------------------------------------------------------------- /docs/auditor_rotation.md: -------------------------------------------------------------------------------- 1 | # Auditor Rotation 2 | 3 | Auditor rotation is the process of changing the global auditor of a token. 4 | Rotation capabilities depend on the chosen solution: 5 | 6 | ## On-chain with Key Derivation 7 | Steps: 8 | 1. Mint account is initialized with an auditor keypair. 9 | 1. Auditor1 sends a copy of auditor keypair to Auditor2. 10 | 1. Auditor2 derives a new keypair from the original. 11 | 1. Mint authority updates the mint account with Auditor2's pubkey. 12 | 13 | Limitations: 14 | - Cannot revoke Auditor1 access. Auditor1 keeps a copy of the keypair. 15 | - Compromised keypair irrevocably exposes associated confidential transfers. 16 | - Auditor1 knows the original seed, they can brute force Auditor2's derivation seed. 17 | - Every auditor rotation increments the total derivations to track. 18 | - This is a problem for frequently rotating auditor sets. 19 | - Requires separate tooling for associating transfers with the corresponding derivation. 20 | 21 | ```mermaid 22 | flowchart LR 23 | subgraph s1["Mint Account"] 24 | n1["Auditor Pubkey"] 25 | end 26 | n2["Auditor1 Keypair"] -. Init .-> n1 27 | n2 -- derive(seed) --> n3["Auditor2 Keypair"] 28 | n3 -- Update --> n1 29 | 30 | %%n2@{ icon: "azure:key-vaults", pos: "b"} 31 | %%n3@{ icon: "azure:key-vaults", pos: "b"} 32 | style n2 stroke:#757575 33 | style n3 stroke:#DD6D00 34 | ``` 35 | 36 | 37 | 38 | ## Off-chain MPC 39 | 1. MPC is initialized with 2-of-2 quorum: 40 | - ShareA: MPC facilitator service (neutral party). 41 | - ShareB: Auditor1. 42 | 1. Mint account is initialized with MPC public key as auditor pubkey. 43 | 1. Auditor1 invokes MPC reshare to add Auditor2 to the MPC. 44 | 1. MPC facilitator service gathers ShareA and ShareB. 45 | 1. MPC facilitator service generates ShareC for Auditor2. 46 | 1. MPC facilitator service invalidates ShareB. 47 | 48 | Benefits: 49 | - Enables auditor revocation. 50 | - Allows for multiple simultaneous auditors. 51 | - Mitigates risk of compromised auditor key. 52 | - Keeps a consistent auditor keypair regardless of the auditor set. 53 | - Cost-effective for frequent auditor rotations. 54 | 55 | Limitations: 56 | - Cannot compartmentalize auditor access to a subset of confidential transfers. 57 | - Requires third-party MPC facilitator service. 58 | - Lacks verifiability 59 | - Can't tell when a new auditor is added to the MPC. 60 | - Can't identify an individual auditor's key. 61 | - Trusted setup is required for MPC. 62 | 63 | ```mermaid 64 | flowchart LR 65 | subgraph s2["Mint Account"] 66 | n8["Auditor Pubkey"] 67 | end 68 | subgraph s4["Auditor2"] 69 | n6["ShareC"] 70 | end subgraph s3["Auditor1"] 71 | n5["ShareB"] 72 | end 73 | 74 | 75 | subgraph s1["Facilitator"] 76 | n7["ShareA"] 77 | s5["MPC"] 78 | end 79 | 80 | s3 -- [1] Rotate Auditor --> s1 81 | s1 -- [2] Create new share --> n6 82 | s1 -- [3] Invalidate old share --> n5 83 | s5 ---> n8 84 | 85 | style n6 stroke-width:4px,stroke-dasharray: 5 86 | ``` 87 | 88 | ## Hybrid MPC 89 | Assumes deployment of an "Auditor Rotation Program" (ARP) 90 | ### Setup 91 | 1. MPC is initialized with 2-of-2 quorum: 92 | - ShareA: MPC facilitator service (neutral party). 93 | - ShareB: Auditor1. 94 | 1. MPC facilitator service listens for transactions to ARP. 95 | 1. Mint account is initialized with MPC public key as auditor pubkey. 96 | ```mermaid 97 | flowchart LR 98 | subgraph s5["MPC Facilitator"] 99 | n7["ShareA"] 100 | s1["MPC"] 101 | end 102 | subgraph s2["Mint Account"] 103 | n8["Auditor Pubkey"] 104 | end 105 | subgraph s3["Auditor1"] 106 | n5["ShareB"] 107 | end 108 | s1 -- [1a] Create --> n7 109 | s1 -- [1b] Create --> n5 110 | s5 -- [2] Listen for Rotate instruction --> n9["Auditor
Rotation
Program"] 111 | n10["Mint Authority"] -- [3] Init(MPC) --> s2 112 | 113 | n10@{ shape: rect} 114 | ``` 115 | 116 | ### Rotation 117 | 1. Mint authority sends Rotate Auditor instruction to ARP. 118 | - Contains new Auditor2's MPC ID. 119 | 1. ARP generates a Pending Rotation PDA. 120 | 1. MPC facilitator reacts to ARP transaction. 121 | 1. MPC facilitator gathers ShareA and ShareB. 122 | 1. MPC facilitator generates ShareC for Auditor2. 123 | 1. MPC facilitator invalidates ShareB. 124 | 1. MPC facilitator submits a "Confirm Rotation" transaction to ARP. 125 | 1. ARP verifies confirmation is valid. 126 | 1. ARP closes the Pending Rotation PDA. 127 | 128 | ```mermaid 129 | flowchart LR 130 | subgraph s5["MPC Facilitator"] 131 | n7["ShareA"] 132 | s1["MPC"] 133 | end 134 | subgraph s2["Mint Account"] 135 | n8["Auditor Pubkey"] 136 | end 137 | subgraph s4["Auditor2"] 138 | n6["ShareC"] 139 | end 140 | subgraph s3["Auditor1"] 141 | n5["ShareB"] 142 | end 143 | subgraph s6["PDA"] 144 | n10["Pending
Rotation
Signature"] 145 | end 146 | subgraph s7["Transaction"] 147 | n12["Rotate"] 148 | end 149 | subgraph s8["Transaction"] 150 | n13["Confirm Rotation"] 151 | end 152 | s1 ---> n8 153 | n9["Auditor
Rotation
Program"] -- [2] Create pending record --> s6 154 | n11["Mint Authority"] --> s7 155 | s7 -- [1] Rotate Ixn --> n9 156 | s5 o-. Listening for Rotate instruction .-o n9 157 | s5 -- [3b] Invalidate --> n5 158 | s5 -- [3a] Create --> n6 159 | n9 -- [5] Close --> s6 160 | s5 --> s8 161 | s8 -- [4] Confirm txn --> n9 162 | 163 | n9@{ shape: rect} 164 | n11@{ shape: rect} 165 | style n6 stroke-width:4px,stroke-dasharray: 5 166 | ``` 167 | 168 | ## Custodial 169 | This is a solution with an opaque custodian. 170 | The custodian is trusted to: 171 | - Update mint account with new auditor keypair. 172 | - Facilitate providing correct decrpytion keypair for each auditor era. 173 | - Provide authentication mechanism for each auditor. 174 | - Determine how to custody the underlying keypair. 175 | 176 | There are many ways to implement this solution. 177 | It's up to the custodian to determine the best approach. -------------------------------------------------------------------------------- /docs/block_explorers.md: -------------------------------------------------------------------------------- 1 | # Block Explorer Integration Guide 2 | 3 | ## Overview 4 | 5 | Block explorers play a critical role in the Solana ecosystem by providing transparency and visibility into on-chain activity. When supporting Confidential Balances, explorers face unique challenges related to encrypted data and specialized instruction handling. 6 | 7 | This guide outlines best practices for integrating Confidential Balances support into block explorers. 8 | 9 | ## Understanding Confidential Transactions 10 | 11 | Solana has no inherent "confidential transaction" primitive. Instead, confidentiality exists at the instruction level through the Token2022 program. When working with confidential transfers: 12 | 13 | 1. Token amounts are encrypted using homomorphic encryption (ElGamal) 14 | 2. Zero-knowledge proofs validate transaction integrity without revealing amounts 15 | 3. Special ciphertexts may be included for auditor access 16 | 17 | Block explorers must detect and properly display these specialized instructions to provide accurate transaction information. 18 | 19 | ## Fundamental Constraints 20 | 21 | Block explorers operating securely lack access to user encryption keys, preventing automatic decryption of confidential balances. Instead, explorers must: 22 | 23 | - Identify and clearly display confidential transfer instructions 24 | - Implement wallet connection interfaces to request decryption when needed 25 | - Support specialized UI components for encrypted balances 26 | - Maintain clear distinction between encrypted and plaintext data 27 | 28 | ## Detecting Confidential Instructions 29 | 30 | To properly identify confidential operations, block explorers need to: 31 | 32 | 1. Identify Token2022 program invocations (program ID) 33 | 2. Parse instruction data to detect confidential transfer operations (instruction ID 0x27) 34 | 3. Examine inner instruction types for specific confidential operations 35 | 4. Handle the specialized serialization format of encrypted data 36 | 37 | ```typescript 38 | // Example detection logic 39 | function isConfidentialTransferInstruction(instruction) { 40 | return instruction.programId.equals(TOKEN_2022_PROGRAM_ID) && 41 | instruction.data[0] === 0x27; // ConfidentialTransferExtension 42 | } 43 | ``` 44 | 45 | ## Context-State Account Handling 46 | 47 | Context-state accounts are temporary accounts created during confidential transfer operations to store zero-knowledge proofs. These accounts are essential for validating the integrity of confidential transfers without revealing the actual amounts. 48 | 49 | For operations like withdrawals, two types of proofs are typically required: 50 | 1. **Equality proofs** - Verify that the amount being withdrawn matches the encrypted balance 51 | 2. **Range proofs** - Ensure the withdrawal amount is within valid bounds (non-negative) 52 | 53 | Each proof requires its own context-state account, which stores the cryptographic proof data needed for on-chain verification. These accounts are typically created just before the operation and closed immediately after to reclaim rent, making them transient in nature. 54 | 55 | When using context-state accounts for confidential transfers: 56 | 57 | 1. Sender/receiver ciphertexts are stored in separate context-state accounts 58 | 2. These accounts are typically closed after transfer completion 59 | 3. Block explorers must: 60 | - Identify the context account creation instruction in transaction history 61 | - Extract ciphertext from the instruction payload 62 | - Maintain references even after account closure 63 | 64 | ```mermaid 65 | sequenceDiagram 66 | participant TX as Transaction 67 | participant CT as Confidential Transfer 68 | participant CS as Context-State Account 69 | 70 | TX->>CS: Create Context Account 71 | Note right of CS: Ciphertext stored here 72 | CT->>CS: Reference during transfer 73 | CS->>CS: Account typically closed 74 | Note right of TX: Block explorer must reference
creation instruction for ciphertext 75 | ``` 76 | 77 | ## Displaying Confidential Transfers 78 | 79 | Block explorers should implement specialized UI elements for confidential transactions, clearly indicating when amounts are encrypted. There are two primary approaches to decryption: 80 | 81 | ### Auditor Decryption 82 | 83 | Auditors have a more straightforward path to decrypt confidential transfers thanks to dedicated ciphertext included in the transfer instructions. 84 | 85 | ```mermaid 86 | sequenceDiagram 87 | participant Explorer as Block Explorer 88 | participant RPC as Solana RPC Node 89 | participant Auditor as User Wallet with Auditor key 90 | 91 | Explorer->>RPC: Fetch transaction 92 | RPC-->>Explorer: Return serialized transaction 93 | 94 | Explorer->>Explorer: Deserialize transaction 95 | 96 | Explorer->>Explorer: Identify Confidential Transfer instruction 97 | Note over Explorer: Check for Token2022 program with
confidential transfer instruction type 98 | 99 | Explorer->>Explorer: Extract auditor ciphertext 100 | Note over Explorer: Auditor ciphertext is directly
included in instruction data 101 | 102 | Explorer->>Auditor: Pass auditor ciphertext 103 | Note over Auditor: Decrypt using auditor
ElGamal private key 104 | 105 | Auditor-->>Explorer: Return decrypted amount 106 | 107 | Explorer->>Explorer: Display amount with
"Auditor-Verified" indicator 108 | ``` 109 | 110 | ### Sender/Receiver Decryption 111 | 112 | For users to decrypt their own transaction amounts, the process is more complex and requires wallet integration: 113 | 114 | ```mermaid 115 | sequenceDiagram 116 | participant Explorer as Block Explorer 117 | participant RPC as Solana RPC Node 118 | participant Wallet as User Wallet 119 | 120 | Explorer->>RPC: Fetch transaction 121 | RPC-->>Explorer: Return serialized transaction 122 | 123 | Note over Explorer: 1. Deserialize transaction 124 | 125 | Explorer->>Explorer: Inspect instructions 126 | Note over Explorer: 2. Identify Token2022 program calls 127 | 128 | Explorer->>Explorer: Check instruction ID (0x27) 129 | Note over Explorer: 3. Detect Confidential Transfer Extension 130 | 131 | Explorer->>Explorer: Extract context-state account pubkey 132 | Note over Explorer: 4. Parse instruction payload 133 | 134 | Explorer->>RPC: Fetch context-state creation transactions 135 | RPC-->>Explorer: Return historical transactions 136 | 137 | Explorer->>Explorer: Scan for initialization instruction 138 | Note over Explorer: 5. Find first ctx-state init instruction 139 | 140 | Explorer->>Explorer: Extract transfer amount ciphertext 141 | Note over Explorer: 6. Get encrypted data from instruction 142 | 143 | Explorer->>Wallet: Request decryption of amount 144 | Note over Wallet: 7. Decrypt using ElGamal private key 145 | Wallet-->>Explorer: Return decrypted amount 146 | 147 | Explorer->>Explorer: Display decrypted amount in UI 148 | ``` 149 | 150 | ## UI/UX Best Practices 151 | 152 | Block explorers should implement these UI/UX best practices for confidential transactions: 153 | 154 | - Clearly distinguish encrypted balances from plaintext ones 155 | - Provide informative tooltips explaining confidentiality mechanics 156 | - Display encryption status indicators (locked/unlocked icons) 157 | - Support optional balance revelation with appropriate warnings 158 | - Maintain visual consistency with conventional transaction displays 159 | 160 | ## External Integration Recommendations 161 | 162 | Block explorers without native wallet integration capabilities should: 163 | 164 | 1. Clearly identify confidential transactions/instructions 165 | 2. Provide links to specialized dApps like @microsite_main 166 | 3. Implement deep linking to pass transaction context 167 | 4. Support QR code generation for mobile workflows 168 | 169 | ## Implementation Checklist 170 | 171 | - [ ] Detect and parse Token2022 confidential instructions 172 | - [ ] Implement wallet connection for decryption requests 173 | - [ ] Add UI components for encrypted balance display 174 | - [ ] Support context-state account tracing 175 | - [ ] Add integration with specialized confidential dApps 176 | - [ ] Provide educational content about confidential balances 177 | 178 | ## Additional Resources 179 | 180 | - [Confidential Balances Product Guide](product_guide.md) 181 | - [Wallet Integration Guide](wallet_guide.md) 182 | - [Protocol Deep Dive](https://www.solana-program.com/docs/confidential-balances/overview) 183 | 184 | -------------------------------------------------------------------------------- /docs/product_guide.md: -------------------------------------------------------------------------------- 1 | # Confidential Balances Product Guide 2 | 3 | ## Introduction 4 | Confidential Balances is a set of Token2022 extensions that enable privacy on Solana asset transfers. The set of extensions is comprised of: 5 | - [Confidential Transfer](https://github.com/solana-program/token-2022/tree/main/program/src/extension/confidential_transfer) 6 | - [Confidential Transfer Fee](https://github.com/solana-program/token-2022/tree/main/program/src/extension/confidential_transfer_fee) 7 | - [Confidential MintBurn](https://github.com/solana-program/token-2022/tree/main/program/src/extension/confidential_mint_burn) 8 | - [Motivation](https://github.com/solana-labs/solana-program-library/issues/6879) 9 | 10 | Instead of an all-or-nothing approach, these token extensions allow varying degrees of configurable privacy. You can transfer tokens with increasing levels of confidentiality: 11 | 1. Disabled confidentiality. 12 | 1. Whitelisted confidentiality. 13 | 1. Opt-in confidentiality. 14 | 1. Confidentiality by default with opt-out options. 15 | 1. Required confidentiality. 16 | 17 | Confidentiality is achieved with use of serveral encryption techniques on token balances: 18 | - Homomorphic encryption to enable arithmetic operations on encrypted data. 19 | - Advanced encryption standards (AES) to encrypt the balance and transfer amounts. 20 | - Zero-knowledge proofs (ZKPs) to ensure integrity of encrypted balance changes without revealing the amount. 21 | 22 | Confidential Balances have compliance baked in. Token issuers can optionally assign global auditors for regulated jurisdictions. Auditors have the ability to decrypt account balances and transfer amounts for KYC, AML, and other compliance purposes. 23 | 24 | To develop a technical intuition for Confidential Balances, see the [Protocol Overview](https://spl.solana.com/confidential-token/deep-dive/overview). 25 | 26 | 27 | ## Getting Started 28 | Confidential Balances require several Solana primitives illustrated below: 29 | ```mermaid 30 | flowchart TB 31 | A[Token 2022
Program] 32 | F[ZK ElGamal Proof
Program] 33 | 34 | subgraph Token Mint 35 | B[Conf. Transfer Extension] 36 | C[Conf. MintBurn Extension] 37 | end 38 | 39 | subgraph Sender Token Account 40 | D[Conf. Transfer
Account Extension] 41 | end 42 | 43 | subgraph Recipient Token Account 44 | E[Conf. Transfer
Account Extension] 45 | end 46 | ``` 47 | - The token mint must be created with Token2022 to support extensions. 48 | - Token accounts must also initialize a confidential transfer extension. 49 | - ElGamal Proof program is used to generate & verify ZKP's. 50 | 51 | Developers interfacing with Confidential Balances need to set up a typical [Solana development environment](https://solana.com/docs/intro/installation#install-dependencies), or at the bare minimum, the latest version of the [Solana CLI](https://solana.com/docs/intro/installation#install-the-solana-cli). 52 | 53 | 54 | ## Try It Out 55 | There are two ways to try your first confidential transfer: 56 | - Beginner: [Token CLI Quickstart Guide](https://spl.solana.com/confidential-token/quickstart) 57 | - Minimum requirements: 58 | - solana-cli 2.1.13 (src:67412607; feat:1725507508, client:Agave) 59 | - spl-token-cli 5.1.0 60 | - Provides all basic functionality to operate a confidential token. 61 | - Fixed set of operations from the CLI menu. 62 | - Use [this provided shell script](https://github.com/solana-program/token-2022/blob/main/clients/cli/examples/confidential-transfer.sh) to execute an end-to-end confidential transfer. 63 | 64 | - Intermediate: [Cookbook Basic Transfer Example Recipe](recipes.md#L12) 65 | - Requires the Solana development environment installation. 66 | - Includes examples of signing transactions & encryptions with [Turnkey](https://www.turnkey.com/) and [Google KMS](https://cloud.google.com/security/products/security-key-management?hl=en). 67 | - Extended set of operations from the CLI menu with easy tweaking and experimentation. 68 | 69 | Regardless of method you will find yourself conducting the same key operations: 70 | ```mermaid 71 | sequenceDiagram 72 | 73 | participant A as "Sender" 74 | participant AA as "Sender Token Account" 75 | participant BB as "Receiver Token Account" 76 | participant B as "Receiver" 77 | 78 | A->>AA: Deposit 79 | A->>AA: Apply 80 | 81 | AA->>BB: Transfer 82 | B->>BB: Apply 83 | 84 | B->>BB: Withdraw 85 | 86 | ``` 87 | - Deposit: 88 | - Confidentializes a token account's balance by storing the amount in an encrypted form. 89 | - Deposit amount is set to a pending state. 90 | - This operation is disabled if using MintBurn extension. 91 | - Withdraw: 92 | - Removes amount from available confidential balance. 93 | - Withdraw amount is set to a pending state. 94 | - This operation is disabled if using MintBurn extension. 95 | - Transfer: 96 | - Deducts the sender's available confidential balance. 97 | - Credits the recipient's pending balance. 98 | - Apply: 99 | - Reconciles all pending amounts to credit/debit public token balance & available confidential balance. 100 | 101 | Read more about [pending & available balances](https://www.solana-program.com/docs/confidential-balances/encryption#account-state). 102 | 103 | ## Specialized Guides 104 | - [Block Explorers](block_explorers.md) 105 | - [Wallets](wallet_guide.md) 106 | -------------------------------------------------------------------------------- /docs/project_structure.md: -------------------------------------------------------------------------------- 1 | ## Project Structure 2 | 3 | ``` 4 | .(root) 5 | ├── ingredients/ # Individual testable modules 6 | ├── recipes/ # Complex flows combining ingredients 7 | └── utils/ # Shared utilities 8 | ``` 9 | 10 | Each ingredient demonstrates isolated functionality. 11 | Recipes combine these ingredients in specific sequences to demonstrate more complex flows. 12 | 13 | To experiment with an individual ingredient, append the following to the respective ingredient's `src/lib.rs` file: 14 | ```rs 15 | #[cfg(test)] 16 | mod tests { 17 | use super::*; 18 | 19 | #[tokio::test] 20 | async fn my_test() -> Result<(), Box> { 21 | // [Your experimental logic here.] 22 | Ok(()) 23 | } 24 | } 25 | ``` 26 | ### Dependency Organization 27 | All dependencies live in the workspace [`Cargo.toml`](../Cargo.toml). 28 | Each ingredient/recipe references dependencies in the workspace. 29 | 30 | -------------------------------------------------------------------------------- /docs/recipes.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | ## Table of Contents 4 | - [Basic Transfer](#basic-transfer) 5 | - [Sample transactions on Devnet](#sample-transactions-on-devnet) 6 | - [Confidential MintBurn Transfer](#confidential-mintburn-transfer) 7 | 8 | 9 | ## [Basic Transfer](../recipes/src/lib.rs#L43) 10 | ### Command: 11 | ```bash 12 | cargo test --package recipes --lib -- --nocapture recipe::basic_transfer_recipe --exact --show-output 13 | ``` 14 | ### Scenario: 15 | - Public mint account (without confidential mint/burn extension). 16 | - Alice makes an offchain request to the Token Issuer (mint authority) for confidentially redeeming cUSD stablecoins. 17 | - Token Issuer delivers funds to Alice's token account as confidential transfer. 18 | 19 | ### Notes: 20 | - The mint account mints/burns publicly, and requires Deposit & Apply instructions prior to Confidential Transfer. 21 | - The Token Issuer must have their own token account to receive the minted tokens. 22 | - Due to public mint/burn, the transfer is only partially confidential. 23 | - Ideally, the tokens are minted directly into the recipient's token account, but we're doing the extra step to demonstrate the confidential transfer process. 24 | ```mermaid 25 | sequenceDiagram 26 | participant Issuer as Token Issuer's Wallet 27 | participant Validator as System Program 28 | participant Token22 as Token22 Program 29 | participant Mint as cUSD Mint Account 30 | participant ElGamal as ElGamal Proof Program 31 | participant IssuerATA as Token Issuer's Token Account 32 | participant AliceATA as Alice's Token Account 33 | participant Alice as Alice's Wallet 34 | 35 | Note over Issuer,Alice: Step 1: Create Wallet Keypairs 36 | Issuer->>Validator: Create wallet keypair & allocate account 37 | Alice->>Validator: Create wallet keypair & allocate account 38 | 39 | Note over Issuer,Mint: Step 2: Create Mint Account 40 | Issuer->>Validator: Create mint account (Mint Authority: Token Issuer) 41 | Validator-->>Mint: Create 42 | Issuer->>Token22: Initialize Mint 43 | Issuer->>Token22: Enable confidential transfer extension on Mint 44 | 45 | Note over Issuer,IssuerATA: Step 3: Create Sender Token Account 46 | Issuer->>Issuer: Generate ElGamal keypair 47 | Issuer->>Issuer: Generate AES key 48 | Issuer->>IssuerATA: Create Associated Token Account (ATA) 49 | Issuer->>Token22: Configure confidential extension 50 | Token22-->>IssuerATA: Configure 51 | 52 | Note over Issuer,IssuerATA: Step 4: Mint Tokens 53 | Issuer->>Token22: Mint tokens 54 | Token22-->>IssuerATA: Credit minted amount 55 | 56 | Note over Issuer,IssuerATA: Step 5: Deposit Tokens 57 | Issuer->>Token22: Deposit to pending confidential balance 58 | Token22-->>IssuerATA: Decrease (non-confidential) public token balance 59 | Token22-->>IssuerATA: Increase pending balance 60 | 61 | Note over Issuer,IssuerATA: Step 6: Apply Sender's Pending Balance 62 | Issuer->>Token22: Apply pending to available balance 63 | Token22-->>IssuerATA: Decrease pending balance. 64 | Token22-->>IssuerATA: Increase available/confidential balance. 65 | 66 | Note over Alice,AliceATA: Step 7: Create Recipient Token Account 67 | Alice->>Alice: Generate ElGamal keypair 68 | Alice->>Alice: Generate AES key 69 | Alice->>AliceATA: Create Associated Token Account (ATA) 70 | Alice->>Token22: Configure confidential extension 71 | Token22-->>AliceATA: Configure 72 | 73 | Note over Issuer,AliceATA: Step 8: Transfer with Proofs 74 | Issuer->>Issuer: Generate proof data for transfer 75 | Issuer->>+Issuer: Generate proof accounts 76 | Issuer->>Validator: Create equality proof 77 | Issuer->>Validator: Create ciphertext validity proof 78 | Issuer->>-Validator: Create range proof 79 | 80 | Issuer->>+Issuer: Execute confidential transfer 81 | Issuer->>ElGamal: Verify equality proof 82 | Issuer->>ElGamal: Verify ciphertext proof 83 | Issuer->>ElGamal: Verify range proof 84 | Issuer->>-Token22: Execute confidential transfer 85 | 86 | Token22-->>IssuerATA: Debit encrypted amount 87 | Token22-->>AliceATA: Credit encrypted amount 88 | 89 | Issuer->>+Issuer: Close proof accounts 90 | Issuer->>ElGamal: Close equality proof 91 | Issuer->>ElGamal: Close ciphertext validity proof 92 | Issuer->>-ElGamal: Close range proof 93 | 94 | Note over Alice,AliceATA: Step 9: Apply Recipient's Pending Balance 95 | Alice->>Token22: Apply pending to available balance 96 | Token22-->>AliceATA: Decrease pending balance 97 | Token22-->>AliceATA: Increase available/confidential balance 98 | 99 | Note over Alice,AliceATA: Step 10: Withdraw Tokens (Optional) 100 | Alice->>Alice: Generate proof data for withdraw 101 | Alice->>+Alice: Generate proof accounts 102 | Alice->>Validator: Create range proof account 103 | Alice->>-Validator: Create equality proof acount 104 | 105 | Alice->>+Alice: Execute withdrawal 106 | Alice->>ElGamal: Verify range 107 | Alice->>ElGamal: Verify equality 108 | Alice->>-Token22: Withdraw tokens from confidential balance 109 | Token22-->>AliceATA: Decrease available/confidential balance 110 | Token22-->>AliceATA: Increase (non-confidential) public token balance 111 | Alice->>+Alice: Close proof accounts 112 | Alice->>ElGamal: Close range proof account 113 | Alice->>-ElGamal: Close equality proof account 114 | ``` 115 | 116 | ### Sample transactions on Devnet: 117 | Accounts: 118 | - Sender account: `Gx6c2Nbbs5QpNMESyVGCarV4mRaaj2HMdmPWpqnJmiXq` 119 | - Sender token account: `F9tQww2FjPNkeRsEYiRgRZL4fuZ8gSMkTvABWY95uGji` 120 | - Recipient account: `5Co1gFfHZCvQuh4Axn7HGKQGzRAr8ADRUnFtJyvf84Fr` 121 | - Recipient token account: `FfqTATdbRN3e1PegbbDQVj34gPyYcqaV1Jh2xpNYyJGa` 122 | - Fee payer account: `HM7txdn2hDBeGDvuC2xfDdKL9XmUNuNvw6BbyXcZKtQD` 123 | - Mint authority account: `GMh1zRFSJudft5uaQiEespzUNePYFL73PCgK5LQacRbK` 124 | - Mint account: `GyRRg4bEhVN75JtoANe4vyEw2zpWYduuCGDxCEZu7KTe` 125 | 126 | Transaction links per step (devnet): 127 | 1. Create Wallet Keypairs 128 | - [Fund Sender Account](https://explorer.solana.com/tx/chw4YusLfm3Kq778VJnRPWeGczbTA9HXeizbtPrwtJfSmd4qcGFXKntVTDHxkCkX8tkbHpLZffD24jy4XVuhgMB?cluster=devnet) 129 | 130 | - [Fund Recipient Account](https://explorer.solana.com/tx/5JDFa2SBKfUGrMreQDvvcKPFHyQepNKWW9dUMB2mYmvipKUJTo2K91YweQ5P9RAbSGghwZyoNiPJY2rDkfNQbAT2?cluster=devnet) 131 | 132 | 1. [Create Mint Account](https://explorer.solana.com/tx/4qAXna48cMC677F6ZjkKbsHETnNpDzX1FF8mFN1zi6LMBjszUjcUR19E8Ng1Uw68m7GLgHsnYVyJRZFPF4sFGXaW?cluster=devnet) 133 | 134 | 1. [Create Token Account](https://explorer.solana.com/tx/2wpnFHCLcFeNsEHcjF2P8Gj3dVsbct9RLupWLCZkN6TTXHgJQUdiYqu9hSzTcX1mtTK4AAPwaN4W1ZgcA6dX7aQT?cluster=devnet) 135 | 136 | 1. [Mint Tokens](https://explorer.solana.com/tx/4MJu5e2cFrws8ehYBU93PcqBhRVwr4o4pxFbNZcZg9XjLy86GyWX7g1C8kRVvPNtxYSwcrmuh6GTdU6BVoSq7jqj?cluster=devnet) 137 | 138 | 1. [Deposit Tokens](https://explorer.solana.com/tx/wJw7HhX1p737XNvVwJLEwE7oCDuSxyJYZPD7xJqLWL4ao3osJ7bdmUoy8R5pTtfL2EqPysr8v2wgJRNTMM9VHsM?cluster=devnet) 139 | 140 | 1. [Apply Pending Balance](https://explorer.solana.com/tx/6y1aNHz7NzVzbEXxf4Rw5xV1EZ8CWFx1zamL9N49YkdJ3JKRpMSLVqdSGfBobSbiAj5zuxfyibwTC1NXgKdjWco?cluster=devnet) 141 | 142 | 1. [Create Recipient Token Account](https://explorer.solana.com/tx/2VfuPxmm9JsZJB8DKpTCfDzAsigFaNdEuWPtNqyWSKAqbMJJqcy8jguHzPjLiSTJAJzX9sEGFeXN2fjMCqpRRkka?cluster=devnet) 143 | 144 | 1. Transfer 145 | 146 | - Create Range Proof Context State (too large for a single transaction) 147 | - [Allocate Context State Account](https://explorer.solana.com/tx/3i7Wmspx4MUyiNAjHuXxhiP773dLaTghnnCGZzwmih5zLMhRoUqwPHhhAK33w3TAEtgsLUCTrCvw9KXmohJkyUvg?cluster=devnet) 148 | - [Encode the proof ](https://explorer.solana.com/tx/3oQivHMnWAWFjFZcyn2gk8UhkQfzuDPb8hEg1h3xxLwPbyLMc1QNhBvfk6ozd4myPk2vxj1WTKXnHzgt47h2FxPL?cluster=devnet) 149 | 150 | - [Create Equality Proof Context State](https://explorer.solana.com/tx/aPjBD7yuEPrRiyj8Ycu1jPFKrYgt3YBC5nk1JQxWenGydLHKcpwCvDzsqzMSMeoP5Y5YPkPebsPhUPCzi2SDHkv?cluster=devnet) 151 | 152 | - [Create Ciphertext Validity Proof Context State](https://explorer.solana.com/tx/5K3Du5EfX1pzcMBzdRNtcLXbQsikr2KDbAKVYgvrUwSh8WMkVsn4Jp4MCgFu7vW2SPqeKyLh3CXX8Jdx6pxoyt3d?cluster=devnet) 153 | 154 | - [Transfer with Split Proofs](https://explorer.solana.com/tx/2rhcbfkr64koHWjoHCJKjbxxS6TonbRH1KVQUvZSFJwM7vnz181eb4eqSkgo3aEFmbnZT5K4z124jW2rRXGuAYU2?cluster=devnet) 155 | 156 | - [Close Proof Accounts](https://explorer.solana.com/tx/5VuxBSS2e9uXqVSME3e9Ktg8ZsLH57y441obxF1s2SaFo8hs1tTgQxofWyw9VsGPPq8PGcMs8JFD3XhBg7p3Zph?cluster=devnet) 157 | 158 | 1. [Apply Pending Balance](https://explorer.solana.com/tx/86nZEeHGtc2jqLSFn6L8i5P3UyGdQCyt4V3WBF35HCEEbmCehpxErFA76X5ktjqk9h1aYsbCVASBX42fJZ3wkKR?cluster=devnet) 159 | 160 | 1. Withdraw 161 | - [Equality Proof Context State Account](https://explorer.solana.com/tx/3iZt3wwbjink4Xqaih5w1naUmbx2EjZ2M5KQJaygnCUNoPbtgzZVXXCggpEJaLiBRRegdSsyxK2drvCDpfM3QSFg?cluster=devnet) 162 | 163 | - [Range Proof Context State Account](https://explorer.solana.com/tx/3ewh5q4vJWJggjAvA1Sug6yaPMVAe4ePy3nBndYTVf7wchfbiMEeJxNQ6dvTxx1D1p9AZfxX4GuqwT9xGKRDr7DL?cluster=devnet) 164 | 165 | - [Withdraw Transaction](https://explorer.solana.com/tx/UDKGNqXuyWQGwvDPgzrEEwPZmGbyWcDiqxhcb97NHDXwMeZipUsCxFKzBWLZXvnvPDtAA9ZtAjNmYohkvVv2S4T?cluster=devnet) 166 | 167 | - [Close Equality Proof Context State Account](https://explorer.solana.com/tx/5uXMwYM7ykH9SM8EmVmv6h3ZDzvGgVwihYzUmnvV5BxCeoxA9QbWdQbK7nM7gfW2cRQ3iqJzZftniRshd5PL9JVZ?cluster=devnet) 168 | 169 | - [Close Range Proof Context State Account](https://explorer.solana.com/tx/128Yq7WSjejDg5vpGNL6VzJgKqG2o3trPFHiCjTfLEzRJ1j3jbWVQkv7msXDtBsDKHeTEpJF6zoiQMjzU3NwKPaZ?cluster=devnet) 170 | 171 | ## [Basic Atomic Transfer](../recipes/src/lib.rs#L90) 172 | Scenario: 173 | - Transfers use Jito bundles to ensure an all-or-nothing operation. 174 | ### Command: 175 | ```bash 176 | cargo test --package recipes --lib -- --nocapture recipe::basic_transfer_recipe_atomic --exact --show-output 177 | ``` 178 | ## [Confidential MintBurn Transfer](../recipes/src/lib.rs#L18) 179 | Scenario: 180 | - Confidential mint account (using confidential mint/burn extension). 181 | ```mermaid 182 | sequenceDiagram 183 | Note over WIP: Work in progess 184 | ``` 185 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Version Requirements 4 | - `solana-cli` v2.1.7 5 | 6 | ## Environment Setup 7 | Use the [.env.example](../.env.example) file to create a `.env` file. 8 | This is the bare minimum setup to run the recipes. 9 | 10 | ## Environment File Behavior 11 | This project uses two environment files: 12 | 13 | 1. **`.env`**: Contains initial configuration values that remain unchanged during runtime. 14 | 15 | 2. **`runtime_output.env`**: Generated during execution to store all runtime values and execution results. 16 | 17 | This approach provides several advantages: 18 | 19 | 1. **Clean Separation**: The original `.env` configuration remains untouched during recipe execution. 20 | 21 | 2. **Runtime Value Storage**: All dynamically generated values (keypairs, transaction results, etc.) are serialized to `runtime_output.env`. 22 | 23 | 3. **Prioritized Loading**: When a recipe or ingredient runs: 24 | - Values are first searched for in `runtime_output.env` (from prior executions) 25 | - If not found, the system falls back to the original `.env` file 26 | - If not found in either, a new value is generated and stored in `runtime_output.env` 27 | 28 | 4. **Reusability**: This approach enables: 29 | - Running individual ingredients in isolation using data from prior recipe runs 30 | - Collecting all resulting private keys from a single recipe execution 31 | 32 | 5. **Reset Capability**: To reset to a clean state, simply delete `runtime_output.env` 33 | 34 | This behavior is implemented using the [dotenvy](https://github.com/allan2/dotenvy) crate. 35 | 36 | ## Test Commands 37 | 38 | ### Running Individual Ingredients 39 | 40 | ```bash 41 | # Run all tests in an ingredient 42 | cargo test -p setup_participants 43 | 44 | # Run a specific test from an ingredient 45 | cargo test -p setup_participants setup_basic_participant 46 | ``` 47 | 48 | ### Running Recipes (Test Sequences) 49 | 50 | ```bash 51 | # Run all recipes 52 | cargo test -p test-runner 53 | 54 | # Run a specific recipe 55 | cargo test -p test-runner recipe::basic_transfer_recipe 56 | ``` 57 | 58 | ### Test Output Options 59 | 60 | ```bash 61 | # Show log output mid-test 62 | cargo test -- --nocapture 63 | 64 | # Show test execution time 65 | cargo test -- --show-output 66 | 67 | ``` -------------------------------------------------------------------------------- /docs/wallet_guide.md: -------------------------------------------------------------------------------- 1 | # Wallet Integration Guide 2 | 3 | ## [Transfers](wallet_guide_transfers.md) 4 | 5 | ## [Checking Balances](wallet_guide_balances.md) 6 | 7 | ## Encryption Scheme 8 | 9 | The confidential transfer system uses a dual-key encryption scheme based on ElGamal and AES encryption. The keys are derived from wallet signatures using specific seed messages. 10 | 11 | ### Key Derivation Process 12 | 13 | 1. **ElGamal Key Derivation** 14 | - Seed Message: `"ElGamalSecretKey"` 15 | - Purpose: Used for ElGamal encryption of confidential balances 16 | - Source: [Agave zk-sdk](https://github.com/anza-xyz/agave/blob/d58415068289a0e030d91d2bbb5680f752947ff6/zk-sdk/src/encryption/elgamal.rs#L516) 17 | 18 | 2. **AES Key Derivation** 19 | - Seed Message: `"AeKey"` 20 | - Purpose: Used for AES encryption of confidential data 21 | - Source: [Agave zk-sdk](https://github.com/anza-xyz/agave/blob/d58415068289a0e030d91d2bbb5680f752947ff6/zk-sdk/src/encryption/auth_encryption.rs#L136) 22 | 23 | ### Signature Generation 24 | 25 | The key derivation process follows these steps: 26 | 27 | 1. The wallet signs a specific seed message concatenated with an empty public seed 28 | 2. The signature is used to derive the encryption keys 29 | 3. The empty public seed ensures compatibility with the SPL Token CLI implementation 30 | 31 | ```typescript 32 | // Example of signature generation 33 | const messageToSign = Buffer.concat([ 34 | seedMessage, // Either "ElGamalSecretKey" or "AeKey" 35 | emptyPublicSeed // Empty byte array for CLI compatibility 36 | ]); 37 | ``` 38 | 39 | ```mermaid 40 | sequenceDiagram 41 | participant Wallet 42 | participant SolanaSDK 43 | participant ZkSDK as Zk SDK 44 | 45 | Note over Wallet: Initialize confidential account 46 | 47 | Note over Wallet: ElGamal Key Setup 48 | Wallet->>Wallet: Create "ElGamalSecretKey" buffer 49 | Wallet->>Wallet: Create seed buffer 50 | Wallet->>Wallet: Concatenate buffers 51 | Wallet->>SolanaSDK: signMessage(concatenatedBuffer) 52 | SolanaSDK-->>Wallet: Return signature 53 | Wallet->>ZkSDK: deriveElGamalKey(signature) 54 | ZkSDK-->>Wallet: Return ElGamal encryption key 55 | 56 | Note over Wallet: AES Key Setup 57 | Wallet->>Wallet: Create "AeKey" buffer 58 | Wallet->>Wallet: Create seed buffer 59 | Wallet->>Wallet: Concatenate buffers 60 | Wallet->>SolanaSDK: signMessage(concatenatedBuffer) 61 | SolanaSDK-->>Wallet: Return signature 62 | Wallet->>ZkSDK: deriveAESKey(signature) 63 | ZkSDK-->>Wallet: Return AES encryption key 64 | 65 | Note over Wallet: Account Ready 66 | Note over Wallet: Keys available for confidential operations 67 | ``` 68 | 69 | ### Security Considerations 70 | 71 | 1. **Hard-coded Seeds** 72 | - The seed messages are hard-coded to ensure consistent key derivation across all implementations 73 | - This matches the implementation in the Solana Token-2022 program 74 | - Reference: [Token-2022 CLI Implementation](https://github.com/solana-program/token-2022/blob/9730044abe4f2ac62afeb010dc0a5ffc8a9fbadc/clients/cli/src/command.rs#L4695) 75 | 76 | 2. **Compatibility Requirements** 77 | - While custom key derivation (using alternative seeds or custom seed buffers) is technically feasible, the current implementation strictly adheres to the hardcoded conventions of `ElGamalSecretKey` and `AeKey` as seeds. This ensures compatibility with the Token-2022 program and SPL Token CLI. Future versions may introduce support for customizable key derivation schemes. 78 | - This ensures that keys derived in the frontend match those used in backend operations 79 | 80 | 3. **Implementation Notes** 81 | - The wallet must support message signing 82 | - The signature process is deterministic, ensuring consistent key derivation 83 | - The derived keys must always either be securely stored or derived on-the-fly and never transmitted. They are only used for encryption/decryption operations 84 | 85 | ### Usage in Transactions 86 | 87 | The encryption scheme is used in various operations: 88 | 89 | 1. **Confidential Transfers** 90 | - ElGamal encryption for balance encryption 91 | - AES encryption for additional data protection 92 | 93 | 2. **Balance Decryption** 94 | - Uses AES key for decrypting confidential balances 95 | - Requires wallet signature for key derivation 96 | 97 | 3. **Account Initialization** 98 | - Both ElGamal and AES keys are derived during account setup 99 | - For securely cached encryption keys, this should be the only event/instance where encryption keys are generated. 100 | - Ensures proper encryption of initial confidential data 101 | -------------------------------------------------------------------------------- /docs/wallet_guide_balances.md: -------------------------------------------------------------------------------- 1 | # Wallet Guide: Handling Confidential Balances 2 | 3 | This guide covers how wallets can display user balances for Solana's Confidential Balances (SPL Token-2022 extension), focusing on key management and security models. 4 | 5 | ## Overview 6 | 7 | Confidential Balances enhance privacy by encrypting token amounts on-chain using: 8 | 9 | * **ElGamal encryption:** Used for core transfer logic and securing balance amounts. Decryption requires the owner's ElGamal private key. 10 | * **AES encryption:** Often used to encrypt the *available balance* specifically for efficient viewing by the owner. Decryption requires a specific AES key. 11 | 12 | The main challenge for wallets is securely managing the necessary ElGamal and AES keys to perform confidential operations and display the available balance. 13 | 14 | ## Encryption Key Management 15 | 16 | Wallets need access to the correct ElGamal keypair and AES key associated with the confidential token account. There are primarily two approaches for managing these keys: 17 | 18 | 1. **Derived Keys:** Both the ElGamal keypair and the AES key can be deterministically derived from the user's main wallet signature (e.g., signing predefined messages). This is convenient as it avoids managing separate keys; the user's main wallet key implicitly controls the derived encryption keys. 19 | 2. **Separate Keys:** Unique ElGamal and/or AES keys could be generated and managed independently of the main wallet key. This requires separate mechanisms for storing, backing up, and retrieving these keys. 20 | 21 | For standard non-custodial wallets, **deriving the encryption keys from the user's signature is the recommended approach** for simplicity and security. 22 | 23 | ## Secure Decryption: Client-Side (Non-Custodial Standard) 24 | 25 | This section focuses specifically on decrypting the *available balance* (typically AES-encrypted) for display in a non-custodial wallet. 26 | 27 | **Process:** 28 | 29 | 1. Fetch the token account data (containing the encrypted available balance). 30 | 2. Prompt the user to sign the predefined message (if using derived keys to get the AES key). 31 | 3. **Locally derive/retrieve** the AES decryption key. 32 | 4. **Locally decrypt** the available balance using the key. 33 | 5. Format and display the result. 34 | 35 | **Security:** Sensitive material (signature, derived/retrieved keys) **never leaves the client-side wallet environment.** 36 | 37 | **Visualization (Client-Side Balance Decryption Flow):** 38 | 39 | ```mermaid 40 | sequenceDiagram 41 | participant User 42 | participant WalletFrontend as Wallet Frontend (Client-Side Logic) 43 | participant WalletSigner as Wallet Signing Interface 44 | participant SolanaRPC as Solana RPC Node 45 | 46 | User->>WalletFrontend: Request Balance View 47 | WalletFrontend->>WalletSigner: Request Signature (if deriving AES key) 48 | WalletSigner-->>User: Prompt for Signature 49 | User-->>WalletSigner: Approve Signature 50 | WalletSigner-->>WalletFrontend: Return Signature 51 | WalletFrontend->>WalletFrontend: Derive/Retrieve AES Key (Local Crypto) 52 | WalletFrontend->>SolanaRPC: Fetch Account Data (with encrypted_balance) 53 | SolanaRPC-->>WalletFrontend: Return Account Data 54 | WalletFrontend->>WalletFrontend: Decrypt balance using AES Key (Local Crypto) 55 | WalletFrontend-->>User: Display Decrypted Balance 56 | ``` 57 | 58 | ## Alternative: Trusted Backend Decryption 59 | 60 | In scenarios where the user inherently trusts a backend service (e.g., custodial wallets, wallet-as-a-service platforms), decryption of the available balance can be delegated to the backend. This model assumes the backend securely manages the necessary keys (ElGamal and AES), whether derived or stored separately. 61 | 62 | **Process:** 63 | 64 | 1. The frontend fetches account data. 65 | 2. The frontend requests the **trusted backend API** to decrypt the balance, potentially providing identifiers or authentication. 66 | 3. The backend retrieves the necessary keys and performs decryption. 67 | 4. The backend returns the decrypted balance to the frontend. 68 | 69 | **Considerations:** 70 | 71 | * **Trust Assumption:** Relies entirely on the security and trustworthiness of the backend provider for key management and operations. 72 | * **Use Cases:** Suitable for custodial setups. 73 | * **ZK Proof Limitation:** This backend pattern might also appear temporarily in non-custodial contexts if client-side libraries for generating the complex Zero-Knowledge proofs required for *full confidential transfers* (using the ElGamal keys) are unavailable. However, even then, performing *balance decryption* (using the AES key) client-side is still preferred for non-custodial wallets if feasible. 74 | 75 | **Visualization (Trusted Backend Balance Decryption Flow):** 76 | 77 | ```mermaid 78 | sequenceDiagram 79 | participant User 80 | participant WalletFrontend as Wallet Frontend 81 | participant BackendAPI as Trusted Backend API Service 82 | participant SolanaRPC as Solana RPC Node 83 | 84 | User->>WalletFrontend: Request Balance View 85 | WalletFrontend->>SolanaRPC: Fetch Account Data 86 | SolanaRPC-->>WalletFrontend: Return Account Data 87 | WalletFrontend->>BackendAPI: Request Balance Decryption (send account_data, auth info) 88 | BackendAPI->>BackendAPI: Retrieve Keys & Decrypt balance 89 | BackendAPI-->>WalletFrontend: Return Decrypted Balance 90 | WalletFrontend-->>User: Display Balance 91 | ``` 92 | 93 | ## Conclusion 94 | 95 | Handling Confidential Balances requires secure management of ElGamal and AES keys. Displaying the available balance involves decrypting it using the appropriate AES key. For non-custodial wallets, the standard secure method is **strictly client-side cryptography**, ideally deriving *all* necessary encryption keys from the user's signature locally. Backend decryption is viable only in **trusted environments** like custodial services. Robust client-side libraries for all cryptographic operations are essential for secure non-custodial Confidential Balance support. 96 | -------------------------------------------------------------------------------- /docs/wallet_guide_transfers.md: -------------------------------------------------------------------------------- 1 | # Transfers 2 | 3 | ## Product Flows 4 | These flows assume that Bob and Alice's token accounts are already set up to support confidential transfers. For instructions on initializing token accounts, refer to the [wallet setup guide](/docs/wallet_guide_setup.md). 5 | 6 | ```mermaid 7 | sequenceDiagram 8 | participant A as "Alice" 9 | participant AW as "Alice Wallet App" 10 | participant BW as "Bob Wallet App" 11 | participant B as "Bob" 12 | 13 | A->>AW: Send Bob 10 CTKN 14 | 15 | alt Manual 16 | BW->>B: Notify of pending balance (10 CTKN) 17 | B->>BW: Apply pending amounts 18 | else Automated 19 | BW->>BW: Detect and Apply pending balance 20 | end 21 | ``` 22 | 23 | - Alice sends Bob 10 CTKN with a single transaction signing prompt. 24 | - The frontend determines the source of funds: 25 | - Public balance (requires a [Deposit](#deposit) operation) 26 | - Confidential balance 27 | - Available balance 28 | - Pending balance (requires an [Apply](#apply) operation) 29 | - Bob must apply pending amounts independently. 30 | - The wallet can notify Bob of pending amounts. 31 | - Custodial wallets can automate the [Apply](#apply) operation for Bob. 32 | - Only the token account authority can sign an [Apply](#apply) operation. 33 | 34 | ## Main Interactions (without Confidential MintBurn) 35 | This section outlines a conceptual flow for an end-to-end confidential transfer from a technical operations perspective. 36 | 37 | ```mermaid 38 | sequenceDiagram 39 | 40 | participant A as "Sender" 41 | participant AA as "Sender Token Account" 42 | participant BB as "Receiver Token Account" 43 | participant B as "Receiver" 44 | 45 | A->>AA: Deposit 46 | A->>AA: Apply 47 | 48 | AA->>BB: Transfer 49 | B->>BB: Apply 50 | 51 | B->>BB: Withdraw 52 | ``` 53 | 54 | These are high-level operations that can manifest as: 55 | - Multiple instructions in a single transaction. 56 | - A sequential series of transactions. 57 | - A combination of both. 58 | 59 | The following sections provide specifics for each operation, along with UX considerations. 60 | 61 | ### Apply 62 | ```mermaid 63 | sequenceDiagram 64 | 65 | participant A as "Sender" 66 | participant AA as "Sender Token Account" 67 | participant CC as "Sender Token Account" 68 | participant BB as "Receiver Token Account" 69 | participant B as "Receiver" 70 | note over A,AA: Deposit Operation 71 | note over B,CC: Transfer Operation 72 | 73 | A->>AA: Deposit 74 | A->>AA: Apply 75 | 76 | CC->>BB: Transfer 77 | B->>BB: Apply 78 | ``` 79 | 80 | The [Apply](#apply) operation is a straightforward [instruction](../ingredients/apply_pending_balance/src/lib.rs#L85) that can be invoked at any time to convert pending balance into available (confidential) balance. For a responsive user experience, the [Apply](#apply) operation should immediately follow the [Deposit](#deposit) and [Transfer](#transfer) operations, allowing users to promptly see the effects of their actions. 81 | 82 | ### Deposit 83 | ```mermaid 84 | sequenceDiagram 85 | 86 | participant A as "Sender" 87 | participant AA as "Sender Token Account" 88 | 89 | A->>AA: Deposit 90 | A->>AA: Apply 91 | ``` 92 | 93 | The [Deposit](#deposit) operation is a straightforward [instruction](../ingredients/deposit_tokens/src/lib.rs#L23) that is compact enough to be included in a transaction with most other instructions. Users typically deposit in anticipation of a transfer. From the end-user's perspective, the [Apply](#apply) operation is typically the next step and should be implicitly automated: 94 | 95 | Automated by instructions: 96 | - Include both instructions in the same transaction. 97 | 98 | Automated by wallet: 99 | - Prompt the end-user only once to sign two chronologically-ordered transactions. 100 | - Present both transaction signatures in a single UI element. 101 | 102 | ### Transfer 103 | ```mermaid 104 | sequenceDiagram 105 | 106 | participant AA as "Sender Token Account" 107 | participant BB as "Receiver Token Account" 108 | participant B as "Receiver" 109 | 110 | AA->>BB: Transfer 111 | B->>BB: Apply 112 | ``` 113 | 114 | The [Transfer](#transfer) operation is complex, involving two user operations. The first operation consists of multiple instructions: 115 | 116 | #### Transfer operation 117 | 1. Create a `range` zk proof account. 118 | 2. Create an `equality` zk proof account. 119 | 3. Create a `ciphertext validity` zk proof account. 120 | 4. Transfer (referencing the above proofs). 121 | 5. Close the `range` zk proof account. 122 | 6. Close the `equality` zk proof account. 123 | 7. Close the `ciphertext validity` zk proof account. 124 | 125 | This operation spans multiple transactions due to size limits but should be presented to the user as a single operation. Instruction order is crucial. See [Failing complex operations](#failing-complex-operations) & [Zk Proof location](#zk-proof-location-state-account-vs-instruction) for more details. 126 | 127 | #### Apply operation 128 | Unlike the [Deposit](#deposit) operation, where only the sender is involved, the [Apply](#apply) operation requires action from the receiver and cannot be automated. 129 | 130 | When a token account's pending balance is modified, it increases the pending balance queue. This queue can be monitored for changes. If an account has a `Pending Balance Credit Counter` greater than 0, the wallet UI can handle the notification in distinct ways: 131 | - Non-custodial wallets: 132 | - Prompt the end user with a predefined [Apply](#apply) transaction for signing. 133 | - Custodial wallets (multiple options): 134 | - Same as above. 135 | - Automatically apply the pending balance on behalf of the user. 136 | 137 | Here's a CLI example output for a receiver's token account immediately after the [Transfer](#transfer) operation. 138 | 139 | ``` 140 | $ spl-token display 8tKwnVasPLvVG7a9rdLV7rNs7B8zqwKgQGQb2qvYk9R1 141 | 142 | SPL Token Account 143 | Address: 8tKwnVasPLvVG7a9rdLV7rNs7B8zqwKgQGQb2qvYk9R1 144 | Program: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 145 | Balance: 0 146 | Decimals: 2 147 | Mint: J1vRTq4TAXZsTniXFC9Kx2XgzaxrDMQRdnrB1QzjmAVk 148 | Owner: AY4RA4mKQvB7jcUHauo2cyp2pgv5ZPHc9aMsFEs9rBg3 149 | State: Initialized 150 | Delegation: (not set) 151 | Close authority: (not set) 152 | Extensions: 153 | Immutable owner 154 | Confidential transfer: 155 | Approved: true 156 | Encryption key: FMLF1R4/cT1jMcrB9v3E6W33rW5J3JtfBwKU361T2y8= 157 | Pending Balance Low: DD50L2TA9Bf8jd+jlpYaux6NuTk/GGMapsHUwyAM13KkGN7rJF+tvq8oebjPkDWkfJPVCWHC4IpbVGrJFEg3EA== 158 | Pending Balance High: UOcgeT1vkxWkhm8znA86hJSNzdRAGkJOtJ9l0Xm8cjIOFFmPY0VsmI7im526rNjOnfSXfZwdY07SNILj0/XwHQ== 159 | Available Balance: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== 160 | Decryptable Available Balance: zCjiI7gzoRGepXrzpvw02k3tapH4jy2wMza4TJkwsFWexRbn 161 | Confidential Credits: Enabled 162 | Non-Confidential Credits: Enabled 163 | Pending Balance Credit Counter: 1 164 | Maximum Pending Balance Credit Counter: 65536 165 | Expected Pending Balance Credit Counter: 0 166 | Actual Pending Balance Credit Counter: 0 167 | ``` 168 | 169 | ### Withdraw 170 | ```mermaid 171 | sequenceDiagram 172 | 173 | participant BB as "Receiver Token Account" 174 | participant B as "Receiver" 175 | 176 | B->>BB: Withdraw 177 | ``` 178 | 179 | The [Withdraw](#withdraw) operation is complex, requiring multiple instructions: 180 | 181 | 1. Create a `range` zk proof account. 182 | 2. Create an `equality` zk proof account. 183 | 3. Withdraw (referencing the above proofs). 184 | 4. Close the `range` zk proof account. 185 | 5. Close the `equality` zk proof account. 186 | 187 | This operation shares the same complexities as the [Transfer](#transfer) operation but does not require an [Apply](#apply) operation. Once the withdraw instruction (#3) is confirmed, the token account balance is reflected in typical SPL fashion. 188 | 189 | ## Main Interactions (with Confidential MintBurn) 190 | The flow simplifies as [Deposit](#deposit) and [Withdraw](#withdraw) become disabled. More details will be provided soon. 191 | 192 | ## Trade-offs 193 | While the frontend presentation for confidential balances can be consistent across Solana's ecosystem, there's no "right way" to execute operations. When deciding on an approach compatible with your wallet project, here are some trade-offs to consider: 194 | 195 | ### Failing complex operations 196 | For a technical illustration of operation complexity, refer to the [Basic Transfer Recipe](/docs/recipes.md#L24) diagram. 197 | 198 | The execution of [Transfer](#transfer) and [Withdraw](#withdraw) transactions can fail for several reasons: 199 | - Transaction becomes stale while awaiting confirmation. 200 | - Some transactions (like Range Proofs) are near transaction size limits. There's no room for priority fee instructions. 201 | - A subset of transactions are malformed. 202 | - Confidential Balances have several dependencies, some with independent versioning cadence. Even minor dependency deviations may cause serialization or cryptographic errors. 203 | Critical dependencies include: 204 | - Token2022 Program (on-chain Token Extension logic) 205 | - ElGamal Zk Program 206 | - [Client libraries](/Cargo.toml) 207 | - Developer error. 208 | - Incorrect instruction order. 209 | - Incorrect zk proof account initialization. 210 | 211 | Since these operations progressively create new state, we need a way to reconcile the state in case of failure. There are two general strategies: 212 | 213 | #### Atomic 214 | Use an all-or-nothing approach with Jito Bundles. When the operation is at most five transactions, they can be bundled. Bundles guarantee transaction order and atomicity. This approach has its own trade-offs: 215 | - Landing the bundle on-chain is not guaranteed. 216 | - Failing one transaction fails the entire bundle. 217 | - Retrying a bundle requires re-signing all bundled transactions, preventing blockhash expiration. 218 | - Jito Bundles are only supported by a subset of Solana validators, potentially delaying bundle confirmation. 219 | 220 | See an example of [atomic transfer in the recipes](/recipes/src/lib.rs#L90). 221 | 222 | #### Non-Atomic 223 | Foregoing atomicity allows for more flexible transaction handling but requires additional logic to handle failures. The general approach is to track operation progress within the wallet, always knowing which transactions have been executed so far. Upon transaction failure, there are two recourses: 224 | 225 | *Abort (roll-back):* 226 | - The sender must issue new transactions to sign for state account closures. 227 | - Requires keeping hold of allocated state account keypairs (typically ephemeral as not needed after closing). 228 | 229 | *Recover (roll-forward):* 230 | - Retry the operation from the point of failure. 231 | - Only succeeds if the transaction is indeed structurally sound (no execution errors). 232 | - Blockhashes may expire during retries, requiring transaction re-signing (same as atomic approach). 233 | 234 | Leaving the operation stale leaves SOL on the table, as closing accounts recovers an account's existential deposit (rent). 235 | 236 | ### Zk Proof location: State Account vs Instruction 237 | Context state accounts are the most reliable approach for Confidential Balances because they can accommodate more zk proof data (max 10MB). However, small-enough proofs can be included within the transaction instruction itself, eliminating the need for a state account. 238 | 239 | If you're looking to reduce the total number of transactions, consider including proofs within the transaction instruction. See an [example when extending the sender/receiver token accounts for confidential transfers](/ingredients/setup_token_account/src/lib.rs#L72-L77). 240 | 241 | -------------------------------------------------------------------------------- /ingredients/apply_pending_balance/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "apply_pending_balance" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | utils = { path = "../../utils" } 8 | solana-sdk = { workspace = true } 9 | spl-token-2022 = { workspace = true } 10 | spl-token-client = { workspace = true } 11 | spl-associated-token-account = { workspace = true } 12 | -------------------------------------------------------------------------------- /ingredients/apply_pending_balance/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, sync::Arc}; 2 | 3 | use utils::{ 4 | get_non_blocking_rpc_client, get_or_create_keypair, get_rpc_client, load_value, print_transaction_url, 5 | }; 6 | use solana_sdk::{signer::Signer, transaction::Transaction}; 7 | use spl_associated_token_account::get_associated_token_address_with_program_id; 8 | use spl_token_2022::{ 9 | error::TokenError, 10 | extension::{ 11 | confidential_transfer::{ 12 | account_info::ApplyPendingBalanceAccountInfo, instruction, ConfidentialTransferAccount, 13 | }, 14 | BaseStateWithExtensions, 15 | }, 16 | solana_zk_sdk::encryption::{auth_encryption::AeKey, elgamal::ElGamalKeypair}, 17 | }; 18 | use spl_token_client::{ 19 | client::{ProgramRpcClient, ProgramRpcClientSendTransaction}, 20 | token::Token, 21 | }; 22 | 23 | pub async fn apply_pending_balance( 24 | token_account_authority: &dyn Signer 25 | ) -> Result<(), Box> { 26 | let fee_payer_keypair = Arc::new(get_or_create_keypair("fee_payer_keypair")?); 27 | let client = get_rpc_client()?; 28 | let mint = get_or_create_keypair("mint")?; 29 | let decimals = load_value("mint_decimals")?; 30 | 31 | let token_account_pubkey = get_associated_token_address_with_program_id( 32 | &token_account_authority.pubkey(), 33 | &mint.pubkey(), 34 | &spl_token_2022::id(), 35 | ); 36 | 37 | let sender_elgamal_keypair = 38 | ElGamalKeypair::new_from_signer(&token_account_authority, &token_account_pubkey.to_bytes()) 39 | .unwrap(); 40 | let sender_aes_key = 41 | AeKey::new_from_signer(&token_account_authority, &token_account_pubkey.to_bytes()).unwrap(); 42 | 43 | // The "pending" balance must be applied to "available" balance before it can be transferred 44 | 45 | // A "non-blocking" RPC client (for async calls) 46 | let token = { 47 | let rpc_client = get_non_blocking_rpc_client()?; 48 | 49 | let program_client = 50 | ProgramRpcClient::new(Arc::new(rpc_client), ProgramRpcClientSendTransaction); 51 | 52 | // Create a "token" client, to use various helper functions for Token Extensions 53 | Token::new( 54 | Arc::new(program_client), 55 | &spl_token_2022::id(), 56 | &mint.pubkey(), 57 | Some(decimals), 58 | fee_payer_keypair.clone(), 59 | ) 60 | }; 61 | 62 | // Get sender token account data 63 | let token_account_info = token 64 | .get_account_info(&token_account_pubkey) 65 | .await?; 66 | 67 | // Unpack the ConfidentialTransferAccount extension portion of the token account data 68 | let confidential_transfer_account = 69 | token_account_info.get_extension::()?; 70 | 71 | // ConfidentialTransferAccount extension information needed to construct an `ApplyPendingBalance` instruction. 72 | let apply_pending_balance_account_info = 73 | ApplyPendingBalanceAccountInfo::new(confidential_transfer_account); 74 | 75 | // Return the number of times the pending balance has been credited 76 | let expected_pending_balance_credit_counter = 77 | apply_pending_balance_account_info.pending_balance_credit_counter(); 78 | 79 | // Update the decryptable available balance (add pending balance to available balance) 80 | let new_decryptable_available_balance = apply_pending_balance_account_info 81 | .new_decryptable_available_balance(&sender_elgamal_keypair.secret(), &sender_aes_key) 82 | .map_err(|_| TokenError::AccountDecryption)?; 83 | 84 | // Create a `ApplyPendingBalance` instruction 85 | let apply_pending_balance_instruction = instruction::apply_pending_balance( 86 | &spl_token_2022::id(), 87 | &token_account_pubkey, // Token account 88 | expected_pending_balance_credit_counter, // Expected number of times the pending balance has been credited 89 | &new_decryptable_available_balance.into(), // Cipher text of the new decryptable available balance 90 | &token_account_authority.pubkey(), // Token account owner 91 | &[&token_account_authority.pubkey()], // Additional signers 92 | )?; 93 | 94 | let recent_blockhash = client.get_latest_blockhash()?; 95 | let transaction = Transaction::new_signed_with_payer( 96 | &[apply_pending_balance_instruction], 97 | Some(&fee_payer_keypair.pubkey()), 98 | &[&token_account_authority, &fee_payer_keypair as &dyn Signer], 99 | recent_blockhash, 100 | ); 101 | 102 | let transaction_signature = client.send_and_confirm_transaction(&transaction)?; 103 | 104 | print_transaction_url("Apply Pending Balance", &transaction_signature.to_string()); 105 | Ok(()) 106 | } -------------------------------------------------------------------------------- /ingredients/deposit_tokens/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deposit_tokens" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | utils = { path = "../../utils" } 8 | solana-sdk = { workspace = true } 9 | spl-token-2022 = { workspace = true } 10 | spl-associated-token-account = { workspace = true } 11 | -------------------------------------------------------------------------------- /ingredients/deposit_tokens/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use utils::{get_or_create_keypair, get_rpc_client, load_value, print_transaction_url}; 4 | use solana_sdk::{signer::Signer, transaction::Transaction}; 5 | use spl_associated_token_account::get_associated_token_address_with_program_id; 6 | use spl_token_2022::extension::confidential_transfer::instruction::deposit; 7 | 8 | pub async fn deposit_tokens(deposit_amount: u64, depositor_signer: &dyn Signer) -> Result<(), Box> { 9 | let client = get_rpc_client()?; 10 | let mint = get_or_create_keypair("mint")?; 11 | let decimals = load_value("mint_decimals")?; 12 | 13 | // Confidential balance has separate "pending" and "available" balances 14 | // Must first deposit tokens from non-confidential balance to "pending" confidential balance 15 | 16 | let depositor_token_account = get_associated_token_address_with_program_id( 17 | &depositor_signer.pubkey(), // Token account owner 18 | &mint.pubkey(), // Mint 19 | &spl_token_2022::id(), 20 | ); 21 | 22 | // Instruction to deposit from non-confidential balance to "pending" balance 23 | let deposit_instruction = deposit( 24 | &spl_token_2022::id(), 25 | &depositor_token_account, // Token account 26 | &mint.pubkey(), // Mint 27 | deposit_amount, // Amount to deposit 28 | decimals, // Mint decimals 29 | &depositor_signer.pubkey(), // Token account owner 30 | &[&depositor_signer.pubkey()], // Signers 31 | )?; 32 | 33 | let recent_blockhash = client.get_latest_blockhash()?; 34 | let transaction = Transaction::new_signed_with_payer( 35 | &[deposit_instruction], 36 | Some(&depositor_signer.pubkey()), 37 | &[&depositor_signer], 38 | recent_blockhash, 39 | ); 40 | 41 | let transaction_signature = client.send_and_confirm_transaction(&transaction)?; 42 | 43 | print_transaction_url("Deposit Tokens", &transaction_signature.to_string()); 44 | Ok(()) 45 | } -------------------------------------------------------------------------------- /ingredients/global_auditor_assert/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "global_auditor_assert" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-sdk = { workspace = true } 8 | solana-client = { workspace = true } 9 | solana-transaction-status-client-types = { workspace = true } 10 | spl-token-2022 = { workspace = true } 11 | spl-token-confidential-transfer-proof-generation = { workspace = true } 12 | 13 | utils = { path = "../../utils" } 14 | bs58 = { workspace = true } 15 | -------------------------------------------------------------------------------- /ingredients/global_auditor_assert/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, str::FromStr}; 2 | 3 | use utils::{get_rpc_client, load_value}; 4 | use bs58; 5 | use solana_client::rpc_config::RpcTransactionConfig; 6 | use solana_transaction_status_client_types::{ 7 | EncodedTransaction, UiMessage, UiTransactionEncoding, 8 | }; 9 | use spl_token_2022::{ 10 | extension::confidential_transfer::instruction::TransferInstructionData, instruction::decode_instruction_data, solana_zk_sdk::encryption::elgamal::{ElGamalCiphertext, ElGamalKeypair} 11 | }; 12 | 13 | use solana_sdk::{ 14 | commitment_config::CommitmentConfig, 15 | signature::Signature, 16 | }; 17 | 18 | use spl_token_confidential_transfer_proof_generation::{try_combine_lo_hi_ciphertexts, TRANSFER_AMOUNT_LO_BITS}; 19 | 20 | pub async fn last_transfer_amount( 21 | asserting_amount: u64, 22 | auditor_keypair: &ElGamalKeypair, 23 | ) -> Result<(), Box> { 24 | // Load the last confidential transfer signature from storage 25 | let loaded_signature: String = load_value("last_confidential_transfer_signature")?; 26 | 27 | // Convert the loaded signature string into a Signature object 28 | let signature = Signature::from_str(loaded_signature.as_str())?; 29 | 30 | // Get the RPC client to interact with the blockchain 31 | let client = get_rpc_client()?; 32 | 33 | // Configure the transaction request with specific encoding and commitment settings 34 | let config = RpcTransactionConfig { 35 | encoding: Some(UiTransactionEncoding::Json), 36 | commitment: Some(CommitmentConfig::confirmed()), 37 | max_supported_transaction_version: Some(0), 38 | }; 39 | 40 | // Fetch the transaction details using the signature and configuration 41 | let tx = client.get_transaction_with_config(&signature, config)?; 42 | 43 | // Extract the transaction's message to process it 44 | match tx.transaction.transaction { 45 | EncodedTransaction::Json(ui_transaction) => { 46 | if let UiMessage::Raw(raw_message) = ui_transaction.message { 47 | 48 | // Decode the base58 encoded instruction data 49 | let input = bs58::decode(raw_message.instructions[0].data.clone()) 50 | .into_vec() 51 | .map_err(|e| format!("Base58 decode error: {:?}", e))?; 52 | 53 | // Trim the token instruction type from the input 54 | let input = &input[1..]; 55 | 56 | // Decode the instruction data into a TransferInstructionData object 57 | let decoded_instruction: TransferInstructionData = *decode_instruction_data(&input)?; 58 | 59 | // Extract and convert the low and high ciphertext parts 60 | let ct_pod_lo = decoded_instruction.transfer_amount_auditor_ciphertext_lo; 61 | let ct_lo = ElGamalCiphertext::try_from(ct_pod_lo)?; 62 | let ct_pod_hi = decoded_instruction.transfer_amount_auditor_ciphertext_hi; 63 | let ct_hi = ElGamalCiphertext::try_from(ct_pod_hi)?; 64 | 65 | // Combine the low and high ciphertexts to get the full transfer amount ciphertext 66 | let transfer_amount_auditor_ciphertext = try_combine_lo_hi_ciphertexts( 67 | &ct_lo, 68 | &ct_hi, 69 | TRANSFER_AMOUNT_LO_BITS, 70 | ).ok_or(format!("Failed to combine ciphertexts"))?; 71 | 72 | // Decrypt the transfer amount using the auditor's secret key 73 | let decrypted_amount = auditor_keypair.secret().decrypt(&transfer_amount_auditor_ciphertext); 74 | 75 | // Decode the decrypted amount and assert it matches the expected asserting amount 76 | let decrypted_decoded_amount = decrypted_amount.decode_u32().ok_or(format!("Failed to decode u32"))?; 77 | assert_eq!(decrypted_decoded_amount, asserting_amount); 78 | } 79 | } 80 | // Handle unexpected transaction encoding 81 | _ => println!("Unexpected transaction encoding"), 82 | } 83 | 84 | Ok(()) 85 | } -------------------------------------------------------------------------------- /ingredients/mint_tokens/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mint_tokens" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | utils = { path = "../../utils" } 8 | solana-sdk = { workspace = true } 9 | spl-token-2022 = { workspace = true } 10 | spl-associated-token-account = { workspace = true } 11 | 12 | # For Confidential MintBurn 13 | spl-token-confidential-transfer-proof-extraction = { workspace = true } 14 | solana-zk-sdk = { workspace = true } 15 | -------------------------------------------------------------------------------- /ingredients/mint_tokens/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use utils::{get_or_create_keypair, get_rpc_client, print_transaction_url}; 4 | use solana_sdk::{ 5 | instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, 6 | transaction::Transaction, 7 | }; 8 | //use solana_zk_sdk::encryption::pod::elgamal::PodElGamalCiphertext; 9 | use spl_associated_token_account::get_associated_token_address_with_program_id; 10 | use spl_token_2022::{ 11 | //extension::confidential_mint_burn, 12 | instruction::mint_to, 13 | solana_zk_sdk::encryption::{ 14 | //auth_encryption::AeKey, 15 | elgamal::ElGamalKeypair 16 | } 17 | }; 18 | // use spl_token_confidential_transfer_proof_extraction::instruction::ProofLocation; 19 | 20 | pub async fn go_with_confidential_mintburn( 21 | _mint_authority: &Keypair, 22 | _token_account_owner: &Pubkey, 23 | _mint_amount: u64, 24 | _supply_elgamal_pubkey: &ElGamalKeypair 25 | ) -> Result<(), Box> { 26 | Err("Not yet implemented".into()) 27 | 28 | // let client = get_rpc_client()?; 29 | // let mint = get_or_create_keypair("mint")?; 30 | // let fee_payer_keypair = get_or_create_keypair("fee_payer_keypair")?; 31 | 32 | // let receiving_token_account = get_associated_token_address_with_program_id( 33 | // &token_account_owner, 34 | // &mint.pubkey(), 35 | // &spl_token_2022::id(), 36 | // ); 37 | 38 | // // Instruction to mint tokens 39 | // let context_state_dummy = Pubkey::new_unique(); 40 | 41 | // let confidential_mint_instructions = 42 | // confidential_mint_burn::instruction::confidential_mint_with_split_proofs( 43 | // &spl_token_2022::id(), 44 | // &receiving_token_account, 45 | // &mint.pubkey(), 46 | // Some(supply_elgamal_pubkey.pubkey_owned()), 47 | // &PodElGamalCiphertext::default(), 48 | // &PodElGamalCiphertext::default(), 49 | // &mint_authority.pubkey(), 50 | // &[&mint_authority.pubkey()], 51 | // ProofLocation::ContextStateAccount(&context_state_dummy), 52 | // ProofLocation::ContextStateAccount(&context_state_dummy), 53 | // ProofLocation::ContextStateAccount(&context_state_dummy), 54 | // AeKey::new_rand().encrypt(mint_amount).into() 55 | // )?; 56 | 57 | // let transaction = Transaction::new_signed_with_payer( 58 | // confidential_mint_instructions.as_slice(), 59 | // Some(&fee_payer_keypair.pubkey()), 60 | // &[&fee_payer_keypair, &mint_authority], 61 | // client.get_latest_blockhash()?, 62 | // ); 63 | 64 | // let transaction_signature = client.send_and_confirm_transaction(&transaction)?; 65 | 66 | // println!( 67 | // "\nMint Tokens: https://explorer.solana.com/tx/{}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899", 68 | // transaction_signature 69 | // ); 70 | // Ok(()) 71 | } 72 | 73 | pub async fn go( 74 | mint_authority: &Keypair, 75 | token_account_owner: &Pubkey, 76 | mint_amount: u64 77 | ) -> Result<(), Box> { 78 | let client = get_rpc_client()?; 79 | let mint = get_or_create_keypair("mint")?; 80 | let fee_payer_keypair = get_or_create_keypair("fee_payer_keypair")?; 81 | 82 | let receiving_token_account = get_associated_token_address_with_program_id( 83 | &token_account_owner, // Token account owner 84 | &mint.pubkey(), // Mint 85 | &spl_token_2022::id(), 86 | ); 87 | 88 | // Instruction to mint tokens 89 | let mint_to_instruction: Instruction = mint_to( 90 | &spl_token_2022::id(), 91 | &mint.pubkey(), // Mint 92 | &receiving_token_account, // Token account to mint to 93 | &mint_authority.pubkey(), // Token account owner 94 | &[&mint_authority.pubkey()], // Additional signers (mint authority) 95 | mint_amount, // Amount to mint 96 | )?; 97 | 98 | let transaction = Transaction::new_signed_with_payer( 99 | &[mint_to_instruction], 100 | Some(&fee_payer_keypair.pubkey()), 101 | &[&fee_payer_keypair, &mint_authority], 102 | client.get_latest_blockhash()?, 103 | ); 104 | 105 | let transaction_signature = client.send_and_confirm_transaction(&transaction)?; 106 | 107 | print_transaction_url("Mint Tokens", &transaction_signature.to_string()); 108 | Ok(()) 109 | } -------------------------------------------------------------------------------- /ingredients/setup_mint/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "setup_mint" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | utils = { path = "../../utils" } 8 | solana-sdk = { workspace = true } 9 | spl-token-2022 = { workspace = true } 10 | spl-token-client = { workspace = true } 11 | -------------------------------------------------------------------------------- /ingredients/setup_mint/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | utils::{get_or_create_keypair, get_rpc_client, print_transaction_url, record_value}, solana_sdk::{ 3 | signature::Keypair, signer::Signer, system_instruction::create_account, transaction::Transaction 4 | }, spl_token_2022::{extension::ExtensionType, instruction::initialize_mint, solana_zk_sdk::encryption::elgamal::ElGamalKeypair, state::Mint}, spl_token_client::token::ExtensionInitializationParams, std::{error::Error, sync::Arc} 5 | }; 6 | 7 | pub async fn create_mint(absolute_authority: &Keypair, auditor_elgamal_keypair: &ElGamalKeypair) -> Result<(), Box> { 8 | let fee_payer_keypair = Arc::new(get_or_create_keypair("fee_payer_keypair")?); 9 | let client = get_rpc_client()?; 10 | let mint = get_or_create_keypair("mint")?; 11 | let mint_authority = absolute_authority; 12 | let freeze_authority = absolute_authority; 13 | let decimals = record_value("mint_decimals", 2)?; 14 | 15 | // Confidential Transfer Extension authority 16 | // Authority to modify the `ConfidentialTransferMint` configuration and to approve new accounts (if `auto_approve_new_accounts` is false?) 17 | let authority = absolute_authority; 18 | 19 | // ConfidentialTransferMint extension parameters 20 | let confidential_transfer_mint_extension = 21 | ExtensionInitializationParams::ConfidentialTransferMint { 22 | authority: Some(authority.pubkey()), 23 | auto_approve_new_accounts: true, // If `true`, no approval is required and new accounts may be used immediately 24 | auditor_elgamal_pubkey: Some((*auditor_elgamal_keypair.pubkey()).into()), 25 | }; 26 | 27 | // Calculate the space required for the mint account with the extension 28 | let space = ExtensionType::try_calculate_account_len::(&[ 29 | ExtensionType::ConfidentialTransferMint, 30 | ])?; 31 | 32 | // Calculate the lamports required for the mint account 33 | let rent = client.get_minimum_balance_for_rent_exemption(space)?; 34 | 35 | // Instructions to create the mint account 36 | let create_account_instruction = create_account( 37 | &fee_payer_keypair.pubkey(), 38 | &mint.pubkey(), 39 | rent, 40 | space as u64, 41 | &spl_token_2022::id(), 42 | ); 43 | 44 | // ConfidentialTransferMint extension instruction 45 | let extension_instruction = 46 | confidential_transfer_mint_extension.instruction(&spl_token_2022::id(), &mint.pubkey())?; 47 | 48 | let instructions = vec![ 49 | create_account_instruction, 50 | extension_instruction, 51 | ]; 52 | 53 | let recent_blockhash = client.get_latest_blockhash()?; 54 | let transaction = Transaction::new_signed_with_payer( 55 | &instructions, 56 | Some(&fee_payer_keypair.pubkey()), 57 | &[&fee_payer_keypair, &mint as &dyn Signer], 58 | recent_blockhash, 59 | ); 60 | 61 | { // Add `initialize_mint_instruction` to the signed transaction 62 | 63 | let mut transaction = transaction; 64 | 65 | // Initialize the mint account 66 | //TODO: Use program-2022/src/extension/confidential_transfer/instruction/initialize_mint() 67 | let initialize_mint_instruction = initialize_mint( 68 | &spl_token_2022::id(), 69 | &mint.pubkey(), 70 | &mint_authority.pubkey(), 71 | Some(&freeze_authority.pubkey()), 72 | decimals, 73 | )?; 74 | 75 | { 76 | let mut unique_pubkeys: std::collections::HashSet<_> = transaction.message.account_keys.iter().cloned().collect(); 77 | transaction.message.account_keys.extend( 78 | initialize_mint_instruction 79 | .accounts 80 | .iter() 81 | .map(|account| account.pubkey) 82 | .filter(|pubkey| unique_pubkeys.insert(*pubkey)) 83 | ); 84 | } 85 | 86 | let compiled_initialize_mint_instruction = 87 | transaction.message.compile_instruction(&initialize_mint_instruction); 88 | 89 | transaction.message.instructions.push(compiled_initialize_mint_instruction); 90 | 91 | transaction.sign( 92 | &[&fee_payer_keypair, &mint as &dyn Signer], 93 | recent_blockhash 94 | ); 95 | 96 | let transaction_signature = client.send_and_confirm_transaction(&transaction)?; 97 | print_transaction_url("Create Mint Account", &transaction_signature.to_string()); 98 | } 99 | 100 | Ok(()) 101 | } -------------------------------------------------------------------------------- /ingredients/setup_mint_confidential/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "setup_mint_confidential" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | utils = { path = "../../utils" } 8 | solana-sdk = { workspace = true } 9 | spl-token-2022 = { workspace = true } 10 | spl-token-client = { workspace = true } 11 | -------------------------------------------------------------------------------- /ingredients/setup_mint_confidential/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | utils::{get_or_create_keypair, get_rpc_client, record_value}, 3 | solana_sdk::{ 4 | signature::Keypair, signer::Signer, system_instruction::create_account, 5 | transaction::Transaction, 6 | }, 7 | spl_token_2022::{ 8 | extension::{confidential_mint_burn, ExtensionType}, instruction::initialize_mint, 9 | solana_zk_sdk::encryption::{auth_encryption::AeKey, elgamal::ElGamalKeypair, pod::elgamal::PodElGamalPubkey}, state::Mint, 10 | }, 11 | spl_token_client::token::ExtensionInitializationParams, 12 | std::{error::Error, sync::Arc}, 13 | }; 14 | 15 | pub async fn create_mint( 16 | absolute_authority: &Keypair, 17 | auditor_elgamal_keypair: &ElGamalKeypair, 18 | ) -> Result<(), Box> { 19 | 20 | let fee_payer_keypair = Arc::new(get_or_create_keypair("fee_payer_keypair")?); 21 | let client = get_rpc_client()?; 22 | let mint = get_or_create_keypair("mint")?; 23 | let mint_authority = absolute_authority; 24 | let freeze_authority = absolute_authority; 25 | let decimals = record_value("mint_decimals", 2)?; 26 | 27 | // Confidential Transfer Extension authority 28 | // Authority to modify the `ConfidentialTransferMint` configuration and to approve new accounts (if `auto_approve_new_accounts` is false?) 29 | let authority = absolute_authority; 30 | 31 | // Calculate the space required for the mint account with the extension 32 | let space = ExtensionType::try_calculate_account_len::(&[ 33 | ExtensionType::ConfidentialTransferMint, 34 | ExtensionType::ConfidentialMintBurn, 35 | ])?; 36 | 37 | // Calculate the lamports required for the mint account 38 | let rent = client.get_minimum_balance_for_rent_exemption(space)?; 39 | 40 | // Instructions to create the mint account 41 | let create_account_instruction = create_account( 42 | &fee_payer_keypair.pubkey(), 43 | &mint.pubkey(), 44 | rent, 45 | space as u64, 46 | &spl_token_2022::id(), 47 | ); 48 | 49 | // ConfidentialTransferMint extension instruction 50 | let extension_confidential_transfer_init_instruction = 51 | ExtensionInitializationParams::ConfidentialTransferMint { 52 | authority: Some(authority.pubkey()), 53 | auto_approve_new_accounts: true, // If `true`, no approval is required and new accounts may be used immediately 54 | auditor_elgamal_pubkey: Some((*auditor_elgamal_keypair.pubkey()).into()), 55 | } 56 | .instruction(&spl_token_2022::id(), &mint.pubkey())?; 57 | 58 | let pod_auditor_elgamal_keypair: PodElGamalPubkey = auditor_elgamal_keypair.pubkey_owned().into(); 59 | let extension_mintburn_init_instruction= confidential_mint_burn::instruction::initialize_mint( 60 | &spl_token_2022::id(), 61 | &mint.pubkey(), 62 | &pod_auditor_elgamal_keypair, 63 | &AeKey::new_rand().encrypt(0).into(), 64 | )?; 65 | 66 | // Initialize the mint account 67 | //TODO: Use program-2022/src/extension/confidential_transfer/instruction/initialize_mint() 68 | let initialize_mint_instruction = initialize_mint( 69 | &spl_token_2022::id(), 70 | &mint.pubkey(), 71 | &mint_authority.pubkey(), 72 | Some(&freeze_authority.pubkey()), 73 | decimals, 74 | )?; 75 | 76 | let instructions = vec![ 77 | create_account_instruction, 78 | extension_confidential_transfer_init_instruction, 79 | extension_mintburn_init_instruction, 80 | initialize_mint_instruction, 81 | ]; 82 | 83 | let recent_blockhash = client.get_latest_blockhash()?; 84 | let transaction = Transaction::new_signed_with_payer( 85 | &instructions, 86 | Some(&fee_payer_keypair.pubkey()), 87 | &[&fee_payer_keypair, &mint as &dyn Signer], 88 | recent_blockhash, 89 | ); 90 | let transaction_signature = client.send_and_confirm_transaction(&transaction)?; 91 | 92 | println!( 93 | "\nCreate Mint Account: https://explorer.solana.com/tx/{}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899", 94 | transaction_signature 95 | ); 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /ingredients/setup_participants/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "setup_participants" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "setup_participants" 8 | path = "src/lib.rs" 9 | 10 | [dependencies] 11 | utils = { path = "../../utils" } 12 | tokio = { workspace = true } 13 | solana-sdk = { workspace = true } 14 | -------------------------------------------------------------------------------- /ingredients/setup_participants/src/lib.rs: -------------------------------------------------------------------------------- 1 | use utils::get_rpc_client; 2 | use { 3 | solana_sdk::{ 4 | pubkey::Pubkey, signature::Keypair 5 | }, 6 | std::error::Error, 7 | }; 8 | 9 | pub async fn setup_basic_participant(participant_pubkey: &Pubkey, fee_payer_keypair: Option<&Keypair>, initial_lamports: u64) -> Result<(), Box> { 10 | 11 | let client = get_rpc_client()?; 12 | 13 | match fee_payer_keypair { 14 | Some(keypair) => { 15 | let recent_blockhash = client.get_latest_blockhash()?; 16 | let tx = solana_sdk::system_transaction::transfer( 17 | keypair, 18 | participant_pubkey, 19 | initial_lamports, 20 | recent_blockhash, 21 | ); 22 | client.send_and_confirm_transaction(&tx)?; 23 | } 24 | None => { 25 | if client.request_airdrop(&participant_pubkey, initial_lamports).is_err() { 26 | let current_balance = client.get_balance(&participant_pubkey)?; 27 | println!("Failed to request airdrop. Ensure the fee payer account has sufficient SOL."); 28 | println!("Current participant balance: {}", current_balance); 29 | } 30 | } 31 | } 32 | 33 | //Hack: To await airdrop settlement. Refactor to use async/await with appropriate commitment. 34 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 35 | 36 | Ok(()) 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | use utils::get_or_create_keypair; 43 | use solana_sdk::signer::Signer; 44 | use solana_sdk::native_token::LAMPORTS_PER_SOL; 45 | 46 | #[tokio::test(flavor = "multi_thread", worker_threads = 8)] 47 | async fn test_setup_basic_participant() -> Result<(), Box> { 48 | let participant_keypair = get_or_create_keypair("SOLO_TEST_participant_keypair")?; 49 | 50 | setup_basic_participant(&participant_keypair.pubkey(), None, 2 * LAMPORTS_PER_SOL).await?; 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ingredients/setup_token_account/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "setup_token_account" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tokio = { workspace = true } 8 | utils = { path = "../../utils" } 9 | spl-token-2022 = { workspace = true } 10 | spl-associated-token-account = { workspace = true } 11 | solana-sdk = { workspace = true } 12 | spl-token-confidential-transfer-proof-extraction = { workspace = true } 13 | -------------------------------------------------------------------------------- /ingredients/setup_token_account/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use utils::{get_or_create_keypair, get_rpc_client, print_transaction_url}; 4 | use solana_sdk::{signer::Signer, transaction::Transaction}; 5 | use spl_associated_token_account::{ 6 | get_associated_token_address_with_program_id, instruction::create_associated_token_account, 7 | }; 8 | use spl_token_2022::{ 9 | error::TokenError, 10 | extension::{ 11 | confidential_transfer::instruction::{configure_account, PubkeyValidityProofData}, 12 | ExtensionType, 13 | }, 14 | instruction::reallocate, 15 | solana_zk_sdk::encryption::{auth_encryption::AeKey, elgamal::ElGamalKeypair}, 16 | }; 17 | use spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation}; 18 | 19 | pub async fn setup_token_account( 20 | token_account_authority: &dyn Signer 21 | ) -> Result<(), Box> { 22 | 23 | let client = get_rpc_client()?; 24 | let mint = get_or_create_keypair("mint")?; 25 | let fee_payer_keypair = get_or_create_keypair("fee_payer_keypair")?; 26 | 27 | // Associated token address of the sender 28 | let token_account_pubkey = get_associated_token_address_with_program_id( 29 | &token_account_authority.pubkey(), // Token account owner 30 | &mint.pubkey(), // Mint 31 | &spl_token_2022::id(), 32 | ); 33 | 34 | // Instruction to create associated token account 35 | let create_associated_token_account_instruction = create_associated_token_account( 36 | &fee_payer_keypair.pubkey(), // Funding account 37 | &token_account_authority.pubkey(), // Token account owner 38 | &mint.pubkey(), // Mint 39 | &spl_token_2022::id(), 40 | ); 41 | 42 | // Instruction to reallocate the token account to include the `ConfidentialTransferAccount` extension 43 | let reallocate_instruction = reallocate( 44 | &spl_token_2022::id(), 45 | &token_account_pubkey, // Token account 46 | &fee_payer_keypair.pubkey(), // Payer 47 | &token_account_authority.pubkey(), // Token account owner 48 | &[&token_account_authority.pubkey()], // Signers 49 | &[ExtensionType::ConfidentialTransferAccount], // Extension to reallocate space for 50 | )?; 51 | 52 | // Create the ElGamal keypair and AES key for the sender token account 53 | let token_account_authority_elgamal_keypair = 54 | ElGamalKeypair::new_from_signer(&token_account_authority, &token_account_pubkey.to_bytes()) 55 | .unwrap(); 56 | let token_account_authority_aes_key = 57 | AeKey::new_from_signer(&token_account_authority, &token_account_pubkey.to_bytes()).unwrap(); 58 | 59 | // The maximum number of `Deposit` and `Transfer` instructions that can 60 | // credit `pending_balance` before the `ApplyPendingBalance` instruction is executed 61 | let maximum_pending_balance_credit_counter = 65536; 62 | 63 | // Initial token balance is 0 64 | let decryptable_balance = token_account_authority_aes_key.encrypt(0); 65 | 66 | // The instruction data that is needed for the `ProofInstruction::VerifyPubkeyValidity` instruction. 67 | // It includes the cryptographic proof as well as the context data information needed to verify the proof. 68 | // Generating the proof data client-side (instead of using a separate proof account) 69 | let proof_data = PubkeyValidityProofData::new(&token_account_authority_elgamal_keypair) 70 | .map_err(|_| TokenError::ProofGeneration)?; 71 | 72 | // `InstructionOffset` indicates that proof is included in the same transaction 73 | // This means that the proof instruction offset must be always be 1. 74 | let proof_location = ProofLocation::InstructionOffset( 75 | 1.try_into().unwrap(), 76 | ProofData::InstructionData(&proof_data), 77 | ); 78 | 79 | // Instructions to configure the token account, including the proof instruction 80 | // Appends the `VerifyPubkeyValidityProof` instruction right after the `ConfigureAccount` instruction. 81 | let configure_account_instruction = configure_account( 82 | &spl_token_2022::id(), // Program ID 83 | &token_account_pubkey, // Token account 84 | &mint.pubkey(), // Mint 85 | &decryptable_balance.into(), // Initial balance 86 | maximum_pending_balance_credit_counter, // Maximum pending balance credit counter 87 | &token_account_authority.pubkey(), // Token Account Owner 88 | &[], // Additional signers 89 | proof_location, // Proof location 90 | ) 91 | .unwrap(); 92 | 93 | // Instructions to configure account must come after `initialize_account` instruction 94 | let mut instructions = vec![ 95 | create_associated_token_account_instruction, 96 | reallocate_instruction, 97 | ]; 98 | instructions.extend(configure_account_instruction); 99 | 100 | let recent_blockhash = client.get_latest_blockhash()?; 101 | let transaction = Transaction::new_signed_with_payer( 102 | &instructions, 103 | Some(&fee_payer_keypair.pubkey()), 104 | &[&token_account_authority, &fee_payer_keypair as &dyn Signer], 105 | recent_blockhash, 106 | ); 107 | 108 | let transaction_signature = client.send_and_confirm_transaction(&transaction)?; 109 | 110 | print_transaction_url("Create Token Account", &transaction_signature.to_string()); 111 | 112 | Ok(()) 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | use utils::get_or_create_keypair; 119 | #[tokio::test] 120 | async fn test_setup_token_account() -> Result<(), Box> { 121 | let sender_keypair = get_or_create_keypair("sender_keypair")?; 122 | 123 | setup_token_account(&sender_keypair).await?; 124 | Ok(()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /ingredients/transfer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "transfer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-sdk = { workspace = true } 8 | spl-token-client = { workspace = true } 9 | spl-token-2022 = { workspace = true } 10 | spl-associated-token-account = { workspace = true } 11 | spl-token-confidential-transfer-proof-generation = { workspace = true } 12 | spl-token-confidential-transfer-proof-extraction = { workspace = true } 13 | solana-zk-sdk = { workspace = true } 14 | 15 | bs58 = { workspace = true } 16 | jito-sdk-rust = { workspace = true } 17 | utils = { path = "../../utils" } 18 | bytemuck = "1.20.0" 19 | bincode = "1.3.3" 20 | serde_json = "1.0" 21 | -------------------------------------------------------------------------------- /ingredients/transfer/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | utils::{ 3 | get_non_blocking_rpc_client, get_or_create_keypair, get_rpc_client, jito, load_value, print_transaction_url, record_value 4 | }, 5 | serde_json::json, 6 | solana_sdk::{ 7 | pubkey::Pubkey, signature::{Keypair, Signer}, system_instruction, transaction::Transaction 8 | }, 9 | spl_associated_token_account::get_associated_token_address_with_program_id, 10 | spl_token_2022::{ 11 | extension::{ 12 | confidential_transfer::{ 13 | account_info::TransferAccountInfo, 14 | ConfidentialTransferAccount, ConfidentialTransferMint, 15 | }, 16 | BaseStateWithExtensions, StateWithExtensionsOwned, 17 | }, 18 | solana_zk_sdk::{ 19 | encryption::{ 20 | auth_encryption::AeKey, 21 | elgamal::{self, ElGamalKeypair}, 22 | pod::elgamal::PodElGamalPubkey, 23 | }, 24 | zk_elgamal_proof_program::{self, instruction::{close_context_state, ContextStateInfo}}, 25 | }, 26 | state::{Account, Mint}, 27 | }, 28 | spl_token_client::{ 29 | client::{ProgramRpcClient, ProgramRpcClientSendTransaction}, 30 | token::{ProofAccount, ProofAccountWithCiphertext, Token}, 31 | }, 32 | spl_token_confidential_transfer_proof_generation::transfer::TransferProofData, 33 | std::{error::Error, sync::Arc} 34 | }; 35 | 36 | pub async fn with_split_proofs(sender_keypair: Arc, recipient_keypair: Arc, confidential_transfer_amount: u64) -> Result<(), Box> { 37 | 38 | let client = get_rpc_client()?; 39 | let transactions = prepare_transactions(sender_keypair.clone(), recipient_keypair, confidential_transfer_amount).await?; 40 | assert!(transactions.len() == 5); 41 | 42 | print_transaction_url("Transfer [Allocate Proof Accounts]", &client.send_and_confirm_transaction(&transactions[0])?.to_string()); 43 | print_transaction_url("Transfer [Encode Range Proof]", &client.send_and_confirm_transaction(&transactions[1])?.to_string()); 44 | print_transaction_url("Transfer [Encode Remaining Proofs]", &client.send_and_confirm_transaction(&transactions[2])?.to_string()); 45 | let transfer_signature = client.send_and_confirm_transaction(&transactions[3])?; 46 | print_transaction_url("Transfer [Execute Transfer]", &transfer_signature.to_string()); 47 | print_transaction_url("Transfer [Close Proof Accounts]", &client.send_and_confirm_transaction(&transactions[4])?.to_string()); 48 | 49 | record_value("last_confidential_transfer_signature", &transfer_signature.to_string())?; 50 | 51 | Ok(()) 52 | 53 | } 54 | 55 | async fn prepare_transactions(sender_keypair: Arc, recipient_keypair: Arc, confidential_transfer_amount: u64) -> Result, Box> { 56 | let client = get_rpc_client()?; 57 | 58 | let mint = get_or_create_keypair("mint")?; 59 | let sender_associated_token_address: Pubkey = get_associated_token_address_with_program_id( 60 | &sender_keypair.pubkey(), 61 | &mint.pubkey(), 62 | &spl_token_2022::id(), 63 | ); 64 | let decimals = load_value("mint_decimals")?; 65 | 66 | let token = { 67 | let rpc_client = get_non_blocking_rpc_client()?; 68 | 69 | let program_client: ProgramRpcClient = 70 | ProgramRpcClient::new(Arc::new(rpc_client), ProgramRpcClientSendTransaction); 71 | 72 | // Create a "token" client, to use various helper functions for Token Extensions 73 | Token::new( 74 | Arc::new(program_client), 75 | &spl_token_2022::id(), 76 | &mint.pubkey(), 77 | Some(decimals), 78 | sender_keypair.clone(), 79 | /* 80 | Can't use the intended separate fee_payer_keypair because I get the following error: 81 | Client(Error { 82 | request: Some(SendTransaction), 83 | kind: RpcError(RpcResponseError { 84 | code: -32602, 85 | message: "base64 encoded solana_sdk::transaction::versioned::VersionedTransaction too large: 1652 bytes (max: encoded/raw 1644/1232)", 86 | data: Empty 87 | }) 88 | }) 89 | 90 | Makes me wonder if transaction has one too many signers for range proof. 91 | */ 92 | ) 93 | }; 94 | let recipient_associated_token_address = get_associated_token_address_with_program_id( 95 | &recipient_keypair.pubkey(), 96 | &mint.pubkey(), 97 | &spl_token_2022::id(), 98 | ); 99 | 100 | // Must first create 3 accounts to store proofs before transferring tokens 101 | // This must be done in a separate transactions because the proofs are too large for single transaction 102 | 103 | // Equality Proof - prove that two ciphertexts encrypt the same value 104 | // Ciphertext Validity Proof - prove that ciphertexts are properly generated 105 | // Range Proof - prove that ciphertexts encrypt a value in a specified range (0, u64::MAX) 106 | 107 | // "Authority" for the proof accounts (to close the accounts after the transfer) 108 | let context_state_authority = &sender_keypair; 109 | 110 | // Generate address for equality proof account 111 | let equality_proof_context_state_account = Keypair::new(); 112 | let equality_proof_pubkey = equality_proof_context_state_account.pubkey(); 113 | 114 | // Generate address for ciphertext validity proof account 115 | let ciphertext_validity_proof_context_state_account = Keypair::new(); 116 | let ciphertext_validity_proof_pubkey = ciphertext_validity_proof_context_state_account.pubkey(); 117 | 118 | // Generate address for range proof account 119 | let range_proof_context_state_account = Keypair::new(); 120 | let range_proof_pubkey = range_proof_context_state_account.pubkey(); 121 | 122 | // Get sender token account data 123 | let sender_token_account_info = token 124 | .get_account_info(&sender_associated_token_address) 125 | .await?; 126 | 127 | let sender_account_extension_data = 128 | sender_token_account_info.get_extension::()?; 129 | 130 | // ConfidentialTransferAccount extension information needed to create proof data 131 | let sender_transfer_account_info = TransferAccountInfo::new(sender_account_extension_data); 132 | 133 | let sender_elgamal_keypair = 134 | ElGamalKeypair::new_from_signer(&sender_keypair, &sender_associated_token_address.to_bytes())?; 135 | let sender_aes_key = 136 | AeKey::new_from_signer(&sender_keypair, &sender_associated_token_address.to_bytes())?; 137 | 138 | // Get recipient token account data 139 | let recipient_account = token 140 | .get_account(recipient_associated_token_address) 141 | .await?; 142 | 143 | // Get recipient ElGamal pubkey from the recipient token account data and convert to elgamal::ElGamalPubkey 144 | let recipient_elgamal_pubkey: elgamal::ElGamalPubkey = 145 | StateWithExtensionsOwned::::unpack(recipient_account.data)? 146 | .get_extension::()? 147 | .elgamal_pubkey 148 | .try_into()?; 149 | 150 | // Get mint account data 151 | let mint_account = token.get_account(mint.pubkey()).await?; 152 | 153 | // Get auditor ElGamal pubkey from the mint account data 154 | let auditor_elgamal_pubkey_option = Option::::from( 155 | StateWithExtensionsOwned::::unpack(mint_account.data)? 156 | .get_extension::()? 157 | .auditor_elgamal_pubkey, 158 | ); 159 | 160 | // Convert auditor ElGamal pubkey to elgamal::ElGamalPubkey type 161 | let auditor_elgamal_pubkey: elgamal::ElGamalPubkey = auditor_elgamal_pubkey_option 162 | .ok_or("No Auditor ElGamal pubkey")? 163 | .try_into()?; 164 | 165 | // Generate proof data 166 | let TransferProofData { 167 | equality_proof_data, 168 | ciphertext_validity_proof_data_with_ciphertext, 169 | range_proof_data, 170 | } = sender_transfer_account_info.generate_split_transfer_proof_data( 171 | confidential_transfer_amount, 172 | &sender_elgamal_keypair, 173 | &sender_aes_key, 174 | &recipient_elgamal_pubkey, 175 | Some(&auditor_elgamal_pubkey), 176 | )?; 177 | 178 | // Create 3 proofs ------------------------------------------------------ 179 | 180 | // Range Proof Instructions------------------------------------------------------------------------------ 181 | let (range_create_ix, range_verify_ix) = get_zk_proof_context_state_account_creation_instructions( 182 | &sender_keypair.pubkey(), 183 | &range_proof_context_state_account.pubkey(), 184 | &context_state_authority.pubkey(), 185 | &range_proof_data, 186 | )?; 187 | 188 | // Equality Proof Instructions--------------------------------------------------------------------------- 189 | let (equality_create_ix, equality_verify_ix) = get_zk_proof_context_state_account_creation_instructions( 190 | &sender_keypair.pubkey(), 191 | &equality_proof_context_state_account.pubkey(), 192 | &context_state_authority.pubkey(), 193 | &equality_proof_data, 194 | )?; 195 | 196 | // Ciphertext Validity Proof Instructions ---------------------------------------------------------------- 197 | let (cv_create_ix, cv_verify_ix) = get_zk_proof_context_state_account_creation_instructions( 198 | &sender_keypair.pubkey(), 199 | &ciphertext_validity_proof_context_state_account.pubkey(), 200 | &context_state_authority.pubkey(), 201 | &ciphertext_validity_proof_data_with_ciphertext.proof_data, 202 | )?; 203 | 204 | 205 | // Transact Proofs ------------------------------------------------------------------------------------ 206 | 207 | // Transaction 1: Allocate all proof accounts at once. 208 | let tx1 = Transaction::new_signed_with_payer( 209 | &[range_create_ix.clone(), equality_create_ix.clone(), cv_create_ix.clone()], 210 | Some(&sender_keypair.pubkey()), 211 | &[ 212 | &sender_keypair, 213 | &range_proof_context_state_account as &dyn Signer, 214 | &equality_proof_context_state_account as &dyn Signer, 215 | &ciphertext_validity_proof_context_state_account as &dyn Signer 216 | ], 217 | client.get_latest_blockhash()?, 218 | ); 219 | 220 | // Transaction 2: Encode Range Proof on its own (because it's the largest). 221 | let tx2 = Transaction::new_signed_with_payer( 222 | &[range_verify_ix], 223 | Some(&sender_keypair.pubkey()), 224 | &[&sender_keypair], 225 | client.get_latest_blockhash()?, 226 | ); 227 | 228 | // Transaction 3: Encode all remaining proofs. 229 | let tx3 = Transaction::new_signed_with_payer( 230 | &[equality_verify_ix, cv_verify_ix], 231 | Some(&sender_keypair.pubkey()), 232 | &[&sender_keypair], 233 | client.get_latest_blockhash()?, 234 | ); 235 | 236 | // Transaction 4: Execute transfer (below) 237 | // Transfer with Split Proofs ------------------------------------------- 238 | 239 | let equality_proof_context_proof_account = ProofAccount::ContextAccount(equality_proof_pubkey); 240 | let ciphertext_validity_proof_context_proof_account = 241 | ProofAccount::ContextAccount(ciphertext_validity_proof_pubkey); 242 | let range_proof_context_proof_account = ProofAccount::ContextAccount(range_proof_pubkey); 243 | 244 | let ciphertext_validity_proof_account_with_ciphertext = ProofAccountWithCiphertext { 245 | proof_account: ciphertext_validity_proof_context_proof_account, 246 | ciphertext_lo: ciphertext_validity_proof_data_with_ciphertext.ciphertext_lo, 247 | ciphertext_hi: ciphertext_validity_proof_data_with_ciphertext.ciphertext_hi, 248 | }; 249 | 250 | let tx4 = token.confidential_transfer_transfer_tx( 251 | &sender_associated_token_address, 252 | &recipient_associated_token_address, 253 | &sender_keypair.pubkey(), 254 | Some(&equality_proof_context_proof_account), 255 | Some(&ciphertext_validity_proof_account_with_ciphertext), 256 | Some(&range_proof_context_proof_account), 257 | confidential_transfer_amount, 258 | Some(sender_transfer_account_info), 259 | &sender_elgamal_keypair, 260 | &sender_aes_key, 261 | &recipient_elgamal_pubkey, 262 | Some(&auditor_elgamal_pubkey), 263 | &[&sender_keypair], 264 | ).await?; 265 | 266 | // Transaction 5: (below) 267 | // Close Proof Accounts -------------------------------------------------- 268 | 269 | // Authority to close the proof accounts 270 | let context_state_authority_pubkey = context_state_authority.pubkey(); 271 | // Lamports from the closed proof accounts will be sent to this account 272 | let destination_account = &sender_keypair.pubkey(); 273 | 274 | // Close the equality proof account 275 | let close_equality_proof_instruction = close_context_state( 276 | ContextStateInfo { 277 | context_state_account: &equality_proof_pubkey, 278 | context_state_authority: &context_state_authority_pubkey, 279 | }, 280 | &destination_account, 281 | ); 282 | 283 | // Close the ciphertext validity proof account 284 | let close_ciphertext_validity_proof_instruction = close_context_state( 285 | ContextStateInfo { 286 | context_state_account: &ciphertext_validity_proof_pubkey, 287 | context_state_authority: &context_state_authority_pubkey, 288 | }, 289 | &destination_account, 290 | ); 291 | 292 | // Close the range proof account 293 | let close_range_proof_instruction = close_context_state( 294 | ContextStateInfo { 295 | context_state_account: &range_proof_pubkey, 296 | context_state_authority: &context_state_authority_pubkey, 297 | }, 298 | &destination_account, 299 | ); 300 | 301 | let recent_blockhash = client.get_latest_blockhash()?; 302 | let tx5 = Transaction::new_signed_with_payer( 303 | &[ 304 | close_equality_proof_instruction, 305 | close_ciphertext_validity_proof_instruction, 306 | close_range_proof_instruction, 307 | ], 308 | Some(&sender_keypair.pubkey()), 309 | &[&sender_keypair], // Signers 310 | recent_blockhash, 311 | ); 312 | 313 | Ok(vec![tx1, tx2, tx3, tx4, tx5]) 314 | } 315 | 316 | pub async fn with_split_proofs_atomic(sender_keypair: Arc, recipient_keypair: Arc, confidential_transfer_amount: u64) -> Result<(), Box> { 317 | 318 | // When using Jito bundles there are many reasons why a bundle might not land: 319 | // - Not enough priority fee prolongs transaction inclusion, risking rejection. 320 | // - Unfortunately, many transactions in transfer are saturated, lacking room to insert a priority fee instruction. 321 | // - This is the most likely reason why bundles fail. 322 | // - We never know if the leading validator is running the Jito engine. 323 | 324 | // We'll do a best attempt at retrying the bundle. 325 | utils::run_with_retry(5, || async { 326 | 327 | let mut transactions = prepare_transactions(sender_keypair.clone(), recipient_keypair.clone(), confidential_transfer_amount).await?; 328 | 329 | // Reconstruct the one transaction to add the jito tip instruction. 330 | { 331 | // Not-so-early-out check for testnet or mainnet. 332 | let client = get_rpc_client()?; 333 | assert!(client.url().contains("testnet") || client.url().contains("mainnet"), "This Jito demo only works on testnet or mainnet (adjust code for custom endpoints)"); 334 | 335 | let jito_tip_ix = jito::create_jito_tip_instruction(sender_keypair.pubkey()).await?; 336 | 337 | // Any transaction can be used. This one is the simplest to edit (and fits within size limits). 338 | let tx3 = &mut transactions[2]; 339 | 340 | // Include instruction's accounts into the transaction (without duplicates). 341 | { 342 | let mut unique_pubkeys: std::collections::HashSet<_> = tx3.message.account_keys.iter().cloned().collect(); 343 | tx3.message.account_keys.extend( 344 | jito_tip_ix 345 | .accounts 346 | .iter() 347 | .map(|account| account.pubkey) 348 | .filter(|pubkey| unique_pubkeys.insert(*pubkey)) 349 | ); 350 | 351 | tx3.message.account_keys.push(solana_sdk::system_program::id()); 352 | } 353 | 354 | // Include instruction into the transaction. 355 | let compiled_jito_tip_ix = tx3.message.compile_instruction(&jito_tip_ix); 356 | tx3.message.instructions.push(compiled_jito_tip_ix); 357 | 358 | // Re-sign the transaction for integrity. 359 | tx3.sign(&[&sender_keypair], client.get_latest_blockhash()?); 360 | 361 | } 362 | 363 | let serialized_tx1 = bs58::encode(bincode::serialize(&transactions[0])?).into_string(); 364 | let serialized_tx2 = bs58::encode(bincode::serialize(&transactions[1])?).into_string(); 365 | let serialized_tx3 = bs58::encode(bincode::serialize(&transactions[2])?).into_string(); 366 | let serialized_tx4 = bs58::encode(bincode::serialize(&transactions[3])?).into_string(); 367 | let serialized_tx5 = bs58::encode(bincode::serialize(&transactions[4])?).into_string(); 368 | 369 | let tx_bundle = json!([ 370 | serialized_tx1, 371 | serialized_tx2, 372 | serialized_tx3, 373 | serialized_tx4, 374 | serialized_tx5 375 | ]); 376 | 377 | let bundled_signatures = jito::submit_and_confirm_bundle(tx_bundle).await?; 378 | print_transaction_url("Transfer [Allocate Proof Accounts]", &bundled_signatures[0]); 379 | print_transaction_url("Transfer [Encode Range Proof]", &bundled_signatures[1]); 380 | print_transaction_url("Transfer [Encode Remaining Proofs]", &bundled_signatures[2]); 381 | print_transaction_url("Transfer [Execute Transfer]", &bundled_signatures[3]); 382 | print_transaction_url("Transfer [Close Proof Accounts]", &bundled_signatures[4]); 383 | 384 | record_value("last_confidential_transfer_signature", &bundled_signatures[3])?; 385 | 386 | Ok(()) 387 | }).await 388 | } 389 | 390 | /// Refactored version of spl_token_client::token::Token::confidential_transfer_create_context_state_account(). 391 | /// Instead of sending transactions internally, this function now returns the instructions to be used externally. 392 | fn get_zk_proof_context_state_account_creation_instructions< 393 | ZK: bytemuck::Pod + zk_elgamal_proof_program::proof_data::ZkProofData, 394 | U: bytemuck::Pod, 395 | >( 396 | fee_payer_pubkey: &Pubkey, 397 | context_state_account_pubkey: &Pubkey, 398 | context_state_authority_pubkey: &Pubkey, 399 | proof_data: &ZK, 400 | ) -> Result<(solana_sdk::instruction::Instruction, solana_sdk::instruction::Instruction), Box> { 401 | use std::mem::size_of; 402 | use spl_token_confidential_transfer_proof_extraction::instruction::zk_proof_type_to_instruction; 403 | 404 | let client = get_rpc_client()?; 405 | let space = size_of::>(); 406 | let rent = client.get_minimum_balance_for_rent_exemption(space)?; 407 | 408 | let context_state_info = ContextStateInfo { 409 | context_state_account: context_state_account_pubkey, 410 | context_state_authority: context_state_authority_pubkey, 411 | }; 412 | 413 | let instruction_type = zk_proof_type_to_instruction(ZK::PROOF_TYPE)?; 414 | 415 | let create_account_ix = system_instruction::create_account( 416 | fee_payer_pubkey, 417 | context_state_account_pubkey, 418 | rent, 419 | space as u64, 420 | &zk_elgamal_proof_program::id(), 421 | ); 422 | 423 | let verify_proof_ix = 424 | instruction_type.encode_verify_proof(Some(context_state_info), proof_data); 425 | 426 | // Return a tuple containing the create account instruction and verify proof instruction. 427 | Ok((create_account_ix, verify_proof_ix)) 428 | } -------------------------------------------------------------------------------- /ingredients/withdraw_tokens/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "withdraw_tokens" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-sdk = { workspace = true } 8 | spl-token-client = { workspace = true } 9 | spl-token-2022 = { workspace = true } 10 | spl-associated-token-account = { workspace = true } 11 | spl-token-confidential-transfer-proof-generation = { workspace = true } 12 | 13 | utils = { path = "../../utils" } 14 | -------------------------------------------------------------------------------- /ingredients/withdraw_tokens/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | utils::{get_non_blocking_rpc_client, get_or_create_keypair, load_value, print_transaction_url}, 3 | solana_sdk:: 4 | signature::{Keypair, Signer} 5 | , 6 | spl_associated_token_account:: 7 | get_associated_token_address_with_program_id 8 | , 9 | spl_token_2022::{ 10 | extension::{ 11 | confidential_transfer::{ 12 | account_info:: 13 | WithdrawAccountInfo 14 | , 15 | ConfidentialTransferAccount, 16 | }, 17 | BaseStateWithExtensions, 18 | }, 19 | solana_zk_sdk:: 20 | encryption::{ 21 | auth_encryption::AeKey, 22 | elgamal::ElGamalKeypair, 23 | } 24 | , 25 | }, 26 | spl_token_client::{ 27 | client::{ProgramRpcClient, ProgramRpcClientSendTransaction, RpcClientResponse}, 28 | token::{ProofAccount, Token}, 29 | }, 30 | spl_token_confidential_transfer_proof_generation:: 31 | withdraw::WithdrawProofData 32 | , 33 | std::{error::Error, sync::Arc}, 34 | }; 35 | 36 | pub async fn withdraw_tokens(withdraw_amount: u64, recipient_signer: Arc) -> Result<(), Box> { 37 | 38 | let mint = get_or_create_keypair("mint")?; 39 | let decimals = load_value("mint_decimals")?; 40 | let recipient_associated_token_address = get_associated_token_address_with_program_id( 41 | &recipient_signer.pubkey(), 42 | &mint.pubkey(), 43 | &spl_token_2022::id(), 44 | ); 45 | 46 | // A "non-blocking" RPC client (for async calls) 47 | let token = { 48 | let rpc_client = get_non_blocking_rpc_client()?; 49 | 50 | let program_client = 51 | ProgramRpcClient::new(Arc::new(rpc_client), ProgramRpcClientSendTransaction); 52 | 53 | // Create a "token" client, to use various helper functions for Token Extensions 54 | Token::new( 55 | Arc::new(program_client), 56 | &spl_token_2022::id(), 57 | &mint.pubkey(), 58 | Some(decimals), 59 | recipient_signer.clone(), 60 | // ^^^ HACK: Unsafe clone of keypair due to Rust lifetime issues. 61 | ) 62 | }; 63 | 64 | let receiver_elgamal_keypair = 65 | ElGamalKeypair::new_from_signer(&recipient_signer, &recipient_associated_token_address.to_bytes()) 66 | .unwrap(); 67 | let receiver_aes_key = 68 | AeKey::new_from_signer(&recipient_signer, &recipient_associated_token_address.to_bytes()).unwrap(); 69 | 70 | // Get recipient token account data 71 | let token_account = token 72 | .get_account_info(&recipient_associated_token_address) 73 | .await?; 74 | 75 | // Unpack the ConfidentialTransferAccount extension portion of the token account data 76 | let extension_data = token_account.get_extension::()?; 77 | 78 | // Confidential Transfer extension information needed to construct a `Withdraw` instruction. 79 | let withdraw_account_info = WithdrawAccountInfo::new(extension_data); 80 | 81 | // Authority for the withdraw proof account (to close the account) 82 | let context_state_authority = &recipient_signer; 83 | 84 | let equality_proof_context_state_keypair = Keypair::new(); 85 | let equality_proof_context_state_pubkey = equality_proof_context_state_keypair.pubkey(); 86 | let range_proof_context_state_keypair = Keypair::new(); 87 | let range_proof_context_state_pubkey = range_proof_context_state_keypair.pubkey(); 88 | 89 | // Create a withdraw proof data 90 | let WithdrawProofData { 91 | equality_proof_data, 92 | range_proof_data, 93 | } = withdraw_account_info.generate_proof_data( 94 | withdraw_amount, 95 | &receiver_elgamal_keypair, 96 | &receiver_aes_key, 97 | )?; 98 | 99 | // Generate withdrawal proof accounts 100 | let context_state_authority_pubkey = context_state_authority.pubkey(); 101 | let create_equality_proof_signer = &[&equality_proof_context_state_keypair]; 102 | let create_range_proof_signer = &[&range_proof_context_state_keypair]; 103 | 104 | match token 105 | .confidential_transfer_create_context_state_account( 106 | &equality_proof_context_state_pubkey, 107 | &context_state_authority_pubkey, 108 | &equality_proof_data, 109 | false, 110 | create_equality_proof_signer, 111 | ) 112 | .await 113 | { 114 | Ok(RpcClientResponse::Signature(signature)) => { 115 | print_transaction_url("Equality Proof Context State Account", &signature.to_string()); 116 | } 117 | _ => { 118 | panic!("Unexpected result from create equality proof context state account"); 119 | } 120 | } 121 | match token 122 | .confidential_transfer_create_context_state_account( 123 | &range_proof_context_state_pubkey, 124 | &context_state_authority_pubkey, 125 | &range_proof_data, 126 | true, 127 | create_range_proof_signer, 128 | ) 129 | .await 130 | { 131 | Ok(RpcClientResponse::Signature(signature)) => { 132 | print_transaction_url("Range Proof Context State Account", &signature.to_string()); 133 | } 134 | _ => { 135 | panic!("Unexpected result from create range proof context state account"); 136 | } 137 | } 138 | 139 | // do the withdrawal 140 | match token 141 | .confidential_transfer_withdraw( 142 | &recipient_associated_token_address, 143 | &recipient_signer.pubkey(), 144 | Some(&ProofAccount::ContextAccount( 145 | equality_proof_context_state_pubkey, 146 | )), 147 | Some(&ProofAccount::ContextAccount( 148 | range_proof_context_state_pubkey, 149 | )), 150 | withdraw_amount, 151 | decimals, 152 | Some(withdraw_account_info), 153 | &receiver_elgamal_keypair, 154 | &receiver_aes_key, 155 | &[&recipient_signer], 156 | ) 157 | .await 158 | { 159 | Ok(RpcClientResponse::Signature(signature)) => { 160 | print_transaction_url("Withdraw Transaction", &signature.to_string()); 161 | } 162 | Ok(RpcClientResponse::Transaction(_)) => { 163 | panic!("Unexpected result from withdraw: transaction"); 164 | } 165 | Ok(RpcClientResponse::Simulation(_)) => { 166 | panic!("Unexpected result from withdraw: simulation"); 167 | } 168 | Err(e) => { 169 | panic!("Unexpected result from withdraw: {:?}", e); 170 | } 171 | } 172 | 173 | // close context state account 174 | let close_context_state_signer = &[&context_state_authority]; 175 | 176 | match token 177 | .confidential_transfer_close_context_state_account( 178 | &equality_proof_context_state_pubkey, 179 | &recipient_associated_token_address, 180 | &context_state_authority_pubkey, 181 | close_context_state_signer, 182 | ) 183 | .await 184 | { 185 | Ok(RpcClientResponse::Signature(signature)) => { 186 | print_transaction_url("Close Equality Proof Context State Account", &signature.to_string()); 187 | } 188 | _ => { 189 | panic!("Unexpected result from close equality proof context state account"); 190 | } 191 | } 192 | match token 193 | .confidential_transfer_close_context_state_account( 194 | &range_proof_context_state_pubkey, 195 | &recipient_associated_token_address, 196 | &context_state_authority_pubkey, 197 | close_context_state_signer, 198 | ) 199 | .await 200 | { 201 | Ok(RpcClientResponse::Signature(signature)) => { 202 | print_transaction_url("Close Range Proof Context State Account", &signature.to_string()); 203 | } 204 | _ => { 205 | panic!("Unexpected result from close range proof context state account"); 206 | } 207 | } 208 | 209 | Ok(()) 210 | } -------------------------------------------------------------------------------- /recipes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "recipes" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-sdk = { workspace = true } 8 | tokio = { workspace = true } 9 | solana-zk-sdk = { workspace = true } 10 | setup_participants = { path = "../ingredients/setup_participants" } 11 | setup_mint = { path = "../ingredients/setup_mint" } 12 | setup_token_account = { path = "../ingredients/setup_token_account" } 13 | mint_tokens = { path = "../ingredients/mint_tokens" } 14 | deposit_tokens = { path = "../ingredients/deposit_tokens" } 15 | apply_pending_balance = { path = "../ingredients/apply_pending_balance" } 16 | transfer = { path = "../ingredients/transfer" } 17 | utils = { path = "../utils" } 18 | withdraw_tokens = { path = "../ingredients/withdraw_tokens" } 19 | global_auditor_assert = { path = "../ingredients/global_auditor_assert" } 20 | setup_mint_confidential = { path = "../ingredients/setup_mint_confidential" } 21 | -------------------------------------------------------------------------------- /recipes/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod recipe { 3 | use std::error::Error; 4 | use std::sync::Arc; 5 | 6 | use apply_pending_balance; 7 | use deposit_tokens; 8 | use utils::{get_or_create_keypair, get_or_create_keypair_elgamal}; 9 | use mint_tokens; 10 | use setup_mint; 11 | use setup_mint_confidential; 12 | use setup_participants; 13 | use setup_token_account; 14 | use solana_sdk::{native_token::LAMPORTS_PER_SOL, signer::Signer}; 15 | use transfer; 16 | use withdraw_tokens; 17 | 18 | #[tokio::test(flavor = "multi_thread", worker_threads = 8)] 19 | async fn confidential_mintburn_transfer_recipe() -> Result<(), Box> { 20 | let sender_keypair = get_or_create_keypair("sender_keypair")?; 21 | let recipient_keypair = get_or_create_keypair("recipient_keypair")?; 22 | let fee_payer_keypair = get_or_create_keypair("fee_payer_keypair")?; 23 | let auditor_elgamal_keypair = get_or_create_keypair_elgamal("auditor_elgamal")?; 24 | let absolute_mint_authority = get_or_create_keypair("absolute_mint_authority")?; 25 | 26 | // Step 1. Setup participants 27 | setup_participants::setup_basic_participant(&fee_payer_keypair.pubkey(), None, 2 * LAMPORTS_PER_SOL).await?; 28 | setup_participants::setup_basic_participant(&sender_keypair.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL).await?; 29 | setup_participants::setup_basic_participant(&recipient_keypair.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/5).await?; 30 | 31 | // Step 2. Create mint 32 | setup_mint_confidential::create_mint(&absolute_mint_authority, &auditor_elgamal_keypair).await?; 33 | 34 | // Step 3. Setup token account for sender 35 | setup_token_account::setup_token_account(&sender_keypair).await?; 36 | 37 | // Step 4. Confidentially mint tokens 38 | mint_tokens::go_with_confidential_mintburn(&absolute_mint_authority, &sender_keypair.pubkey(), 100_00, &auditor_elgamal_keypair).await?; 39 | 40 | Ok(()) 41 | } 42 | 43 | #[tokio::test(flavor = "multi_thread", worker_threads = 8)] 44 | async fn basic_transfer_recipe() -> Result<(), Box> { 45 | let sender_keypair = Arc::new(get_or_create_keypair("sender_keypair")?); 46 | let recipient_keypair = Arc::new(get_or_create_keypair("recipient_keypair")?); 47 | let fee_payer_keypair = get_or_create_keypair("fee_payer_keypair")?; 48 | let auditor_elgamal_keypair = get_or_create_keypair_elgamal("auditor_elgamal")?; 49 | let absolute_mint_authority = get_or_create_keypair("absolute_mint_authority")?; 50 | 51 | // Step 1. Setup participants 52 | setup_participants::setup_basic_participant(&fee_payer_keypair.pubkey(), None, 2 * LAMPORTS_PER_SOL).await?; 53 | setup_participants::setup_basic_participant(&sender_keypair.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/2).await?; 54 | setup_participants::setup_basic_participant(&recipient_keypair.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/5).await?; 55 | 56 | // Step 2. Create mint 57 | setup_mint::create_mint(&absolute_mint_authority, &auditor_elgamal_keypair).await?; 58 | 59 | // Step 3. Setup token account for sender 60 | setup_token_account::setup_token_account(&sender_keypair).await?; 61 | 62 | // Step 4. Mint tokens 63 | mint_tokens::go(&absolute_mint_authority, &sender_keypair.pubkey(), 100_00).await?; 64 | 65 | // Step 5. Deposit tokens 66 | deposit_tokens::deposit_tokens(50_00, &sender_keypair).await?; 67 | 68 | // Step 6. Apply pending balance 69 | apply_pending_balance::apply_pending_balance(&sender_keypair).await?; 70 | 71 | // Step 7. Create recipient token account 72 | setup_token_account::setup_token_account(&recipient_keypair).await?; 73 | 74 | // Step 8. Transfer tokens with split proofs 75 | transfer::with_split_proofs(sender_keypair.clone(), recipient_keypair.clone(), 50_00).await?; 76 | 77 | // Step 9. Apply recipient's pending balance 78 | apply_pending_balance::apply_pending_balance(&recipient_keypair).await?; 79 | 80 | // Step 10. Withdraw tokens 81 | withdraw_tokens::withdraw_tokens(20_00, recipient_keypair.clone()).await?; 82 | 83 | // Step 11. Auditor asserts last transfer amount 84 | global_auditor_assert::last_transfer_amount(50_00, &auditor_elgamal_keypair).await?; 85 | 86 | Ok(()) 87 | } 88 | 89 | #[tokio::test(flavor = "multi_thread", worker_threads = 8)] 90 | async fn basic_transfer_recipe_atomic() -> Result<(), Box> { 91 | let sender_keypair = Arc::new(get_or_create_keypair("sender_keypair")?); 92 | let recipient_keypair = Arc::new(get_or_create_keypair("recipient_keypair")?); 93 | let fee_payer_keypair = get_or_create_keypair("fee_payer_keypair")?; 94 | let auditor_elgamal_keypair = get_or_create_keypair_elgamal("auditor_elgamal")?; 95 | let absolute_mint_authority = get_or_create_keypair("absolute_mint_authority")?; 96 | 97 | // Step 1. Setup participants 98 | setup_participants::setup_basic_participant(&fee_payer_keypair.pubkey(), None, 2 * LAMPORTS_PER_SOL).await?; 99 | setup_participants::setup_basic_participant(&sender_keypair.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/2).await?; 100 | setup_participants::setup_basic_participant(&recipient_keypair.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/5).await?; 101 | 102 | // Step 2. Create mint 103 | setup_mint::create_mint(&absolute_mint_authority, &auditor_elgamal_keypair).await?; 104 | 105 | // Step 3. Setup token account for sender 106 | setup_token_account::setup_token_account(&sender_keypair).await?; 107 | 108 | // Step 4. Mint tokens 109 | mint_tokens::go(&absolute_mint_authority, &sender_keypair.pubkey(), 100_00).await?; 110 | 111 | // Step 5. Deposit tokens 112 | deposit_tokens::deposit_tokens(50_00, &sender_keypair).await?; 113 | 114 | // Step 6. Apply pending balance 115 | apply_pending_balance::apply_pending_balance(&sender_keypair).await?; 116 | 117 | // Step 7. Create recipient token account 118 | setup_token_account::setup_token_account(&recipient_keypair).await?; 119 | 120 | // Step 8. Transfer tokens with split proofs 121 | transfer::with_split_proofs_atomic(sender_keypair.clone(), recipient_keypair.clone(), 50_00).await?; 122 | 123 | // Step 9. Apply recipient's pending balance 124 | apply_pending_balance::apply_pending_balance(&recipient_keypair).await?; 125 | 126 | // Step 10. Withdraw tokens 127 | withdraw_tokens::withdraw_tokens(20_00, recipient_keypair.clone()).await?; 128 | 129 | // Step 11. Auditor asserts last transfer amount 130 | global_auditor_assert::last_transfer_amount(50_00, &auditor_elgamal_keypair).await?; 131 | 132 | Ok(()) 133 | } 134 | 135 | #[tokio::test(flavor = "multi_thread", worker_threads = 8)] 136 | async fn basic_transfer_recipe_turnkey() -> Result<(), Box> { 137 | 138 | let sender_signer = utils::get_turnkey_signers_from_env( 139 | "TURNKEY_SENDER_PRIVATE_KEY_ID", 140 | "TURNKEY_SENDER_PUBLIC_KEY" 141 | )?; 142 | 143 | let recipient_signer = utils::get_turnkey_signers_from_env( 144 | "TURNKEY_RECEIVER_PRIVATE_KEY_ID", 145 | "TURNKEY_RECEIVER_PUBLIC_KEY" 146 | )?; 147 | 148 | let recipient_signer = Arc::new(recipient_signer); 149 | let sender_signer = Arc::new(sender_signer); 150 | 151 | let fee_payer_keypair = get_or_create_keypair("fee_payer_keypair")?; 152 | let auditor_elgamal_keypair = get_or_create_keypair_elgamal("auditor_elgamal")?; 153 | let absolute_mint_authority = get_or_create_keypair("absolute_mint_authority")?; 154 | 155 | // Step 1. Setup participants 156 | setup_participants::setup_basic_participant(&fee_payer_keypair.pubkey(), None, 2 * LAMPORTS_PER_SOL).await?; 157 | setup_participants::setup_basic_participant(&sender_signer.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/2).await?; 158 | setup_participants::setup_basic_participant(&recipient_signer.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/5).await?; 159 | 160 | // Step 2. Create mint 161 | setup_mint::create_mint(&absolute_mint_authority, &auditor_elgamal_keypair).await?; 162 | 163 | // Step 3. Setup token account for sender 164 | setup_token_account::setup_token_account(&sender_signer).await?; 165 | 166 | // Step 4. Mint tokens 167 | mint_tokens::go(&absolute_mint_authority, &sender_signer.pubkey(), 100_00).await?; 168 | 169 | // Step 5. Deposit tokens 170 | deposit_tokens::deposit_tokens(50_00, &sender_signer).await?; 171 | 172 | // Step 6. Apply pending balance 173 | apply_pending_balance::apply_pending_balance(&sender_signer).await?; 174 | 175 | // Step 7. Create recipient token account 176 | setup_token_account::setup_token_account(&recipient_signer).await?; 177 | 178 | // Step 8. Transfer tokens with split proofs 179 | transfer::with_split_proofs(sender_signer.clone(), recipient_signer.clone(), 50_00).await?; 180 | 181 | // Step 9. Apply recipient's pending balance 182 | apply_pending_balance::apply_pending_balance(&recipient_signer).await?; 183 | 184 | // Step 10. Withdraw tokens 185 | withdraw_tokens::withdraw_tokens(20_00, recipient_signer.clone()).await?; 186 | 187 | // Step 11. Auditor asserts last transfer amount 188 | global_auditor_assert::last_transfer_amount(50_00, &auditor_elgamal_keypair).await?; 189 | 190 | Ok(()) 191 | } 192 | 193 | 194 | #[tokio::test(flavor = "multi_thread", worker_threads = 8)] 195 | async fn basic_transfer_recipe_gcp() -> Result<(), Box> { 196 | let sender_signer = utils::get_gcp_signer_from_env("projects/cookbook-448105/locations/us-west1/keyRings/test/cryptoKeys/first_key/cryptoKeyVersions/1").await?; 197 | let recipient_signer = utils::get_gcp_signer_from_env("projects/cookbook-448105/locations/us-west1/keyRings/test/cryptoKeys/second_key/cryptoKeyVersions/1").await?; 198 | 199 | let recipient_signer = Arc::new(recipient_signer); 200 | let sender_signer = Arc::new(sender_signer); 201 | 202 | let fee_payer_keypair = get_or_create_keypair("fee_payer_keypair")?; 203 | let auditor_elgamal_keypair = get_or_create_keypair_elgamal("auditor_elgamal")?; 204 | let absolute_mint_authority = get_or_create_keypair("absolute_mint_authority")?; 205 | 206 | // Step 1. Setup participants 207 | setup_participants::setup_basic_participant(&fee_payer_keypair.pubkey(), None, 2 * LAMPORTS_PER_SOL).await?; 208 | setup_participants::setup_basic_participant(&sender_signer.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/2).await?; 209 | setup_participants::setup_basic_participant(&recipient_signer.pubkey(), Some(&fee_payer_keypair), LAMPORTS_PER_SOL/5).await?; 210 | 211 | // Step 2. Create mint 212 | setup_mint::create_mint(&absolute_mint_authority, &auditor_elgamal_keypair).await?; 213 | 214 | // Step 3. Setup token account for sender 215 | setup_token_account::setup_token_account(&sender_signer).await?; 216 | 217 | // Step 4. Mint tokens 218 | mint_tokens::go(&absolute_mint_authority, &sender_signer.pubkey(), 100_00).await?; 219 | 220 | // Step 5. Deposit tokens 221 | deposit_tokens::deposit_tokens(50_00, &sender_signer).await?; 222 | 223 | // Step 6. Apply pending balance 224 | apply_pending_balance::apply_pending_balance(&sender_signer).await?; 225 | 226 | // Step 7. Create recipient token account 227 | setup_token_account::setup_token_account(&recipient_signer).await?; 228 | 229 | // Step 8. Transfer tokens with split proofs 230 | transfer::with_split_proofs(sender_signer.clone(), recipient_signer.clone(), 50_00).await?; 231 | 232 | // Step 9. Apply recipient's pending balance 233 | apply_pending_balance::apply_pending_balance(&recipient_signer).await?; 234 | 235 | // Step 10. Withdraw tokens 236 | withdraw_tokens::withdraw_tokens(20_00, recipient_signer.clone()).await?; 237 | 238 | // Step 11. Auditor asserts last transfer amount 239 | global_auditor_assert::last_transfer_amount(50_00, &auditor_elgamal_keypair).await?; 240 | 241 | Ok(()) 242 | } 243 | 244 | } -------------------------------------------------------------------------------- /runtime_output.env: -------------------------------------------------------------------------------- 1 | auditor_elgamal=[15,107,85,239,151,144,236,45,235,229,109,137,17,83,15,247,89,80,158,107,200,145,145,98,11,54,84,213,37,62,40,6] 2 | absolute_mint_authority=[127,122,187,244,125,181,198,193,89,227,118,161,22,151,166,56,213,150,106,146,250,128,227,119,134,58,217,123,91,197,5,190,171,195,17,180,115,168,246,79,156,131,187,216,79,99,86,118,239,28,149,75,155,107,158,2,115,246,172,129,5,84,88,105] 3 | mint=[181,128,23,163,3,152,57,48,33,246,4,229,169,104,15,127,121,199,66,121,54,187,179,234,251,236,244,239,214,173,249,165,77,121,108,44,103,65,113,144,106,99,7,163,165,152,8,160,11,108,237,116,245,203,43,94,22,251,199,235,135,73,47,162] 4 | mint_decimals=2 5 | last_confidential_transfer_signature="2frVemvpf4ENLxk3qejB31qESHwH2jXnEcn5mztfPisE1A1LpvhTxnhUEYq9roYrB4Ms73TUKWMw1or6C23FBrSJ" -------------------------------------------------------------------------------- /utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "utils" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-sdk = { workspace = true } 8 | serde = { workspace = true } 9 | serde_json = { workspace = true } 10 | solana-zk-sdk = { workspace = true } 11 | solana-client = { workspace = true } 12 | dotenvy = { workspace = true } 13 | tk-rs = { workspace = true } 14 | tokio = { workspace = true } 15 | google-cloud-kms = { workspace = true } 16 | base64 = { workspace = true } 17 | bs58 = { workspace = true } 18 | jito-sdk-rust = { workspace = true } 19 | reqwest = { version = "0.12.11", features = ["json"] } -------------------------------------------------------------------------------- /utils/src/gcp.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::error::Error; 3 | 4 | use base64::Engine; 5 | 6 | use google_cloud_kms::{ 7 | client::{Client, ClientConfig}, 8 | grpc::kms::v1::{ 9 | AsymmetricSignRequest, GetPublicKeyRequest, 10 | }, 11 | }; 12 | use solana_sdk::pubkey::Pubkey; 13 | 14 | pub struct GcpSigner { 15 | client: Client, 16 | 17 | // Example: "projects/*/locations/*/keyRings/*/cryptoKeys/*/cryptoKeyVersions/*" 18 | resource_name: String, 19 | } 20 | 21 | impl GcpSigner { 22 | pub async fn new(resource_name: String) -> Result> { 23 | let config = ClientConfig::default().with_auth().await?; 24 | let client = Client::new(config).await?; 25 | Ok(Self { 26 | client, 27 | resource_name: resource_name, 28 | }) 29 | } 30 | } 31 | 32 | fn decode_pem(pem: &str) -> Result> { 33 | // Step 1: Strip PEM headers 34 | let pem_body = pem 35 | .lines() 36 | .filter(|line| !line.starts_with("-----")) 37 | .collect::>() 38 | .join(""); 39 | 40 | // Step 2: Decode the base64 PEM body 41 | let der_bytes = base64::engine::general_purpose::STANDARD.decode(&pem_body)?; 42 | 43 | // Step 3: Extract the raw public key 44 | // For Ed25519, the raw key is the last 32 bytes of the DER structure 45 | let raw_key = &der_bytes[der_bytes.len() - 32..]; 46 | 47 | // Step 4: Convert the raw key to a Pubkey 48 | Pubkey::try_from(raw_key).map_err(|e| Box::new(e) as Box) 49 | } 50 | 51 | impl solana_sdk::signer::Signer for GcpSigner { 52 | fn try_pubkey(&self) -> Result { 53 | // START Blocking thread... 54 | tokio::task::block_in_place(|| { 55 | let handle = tokio::runtime::Handle::current(); 56 | handle.block_on(async { 57 | // START logic 58 | let resp = self 59 | .client 60 | .get_public_key( 61 | GetPublicKeyRequest { 62 | name: self.resource_name.clone(), 63 | }, 64 | None, 65 | ) 66 | .await 67 | .map_err(|e| solana_sdk::signer::SignerError::Custom(e.to_string())); 68 | 69 | decode_pem(&resp.unwrap().pem) 70 | .map_err(|e| solana_sdk::signer::SignerError::Custom(e.to_string())) 71 | // END logic 72 | }) 73 | }) 74 | // END Blocking thread... 75 | } 76 | 77 | fn try_sign_message(&self, message: &[u8]) -> Result { 78 | // START Blocking thread... 79 | tokio::task::block_in_place(|| { 80 | let handle = tokio::runtime::Handle::current(); 81 | handle.block_on(async { 82 | 83 | // START logic 84 | let resp = self 85 | .client 86 | .asymmetric_sign( 87 | AsymmetricSignRequest { 88 | name: self.resource_name.clone(), 89 | digest: None, 90 | digest_crc32c: None, 91 | data: message.to_vec(), 92 | data_crc32c: None, 93 | }, 94 | None, 95 | ) 96 | .await 97 | .map_err(|e| solana_sdk::signer::SignerError::Custom(e.to_string()))?; 98 | 99 | let signature_bytes: [u8; 64] = resp.signature.as_slice().try_into().map_err(|_| solana_sdk::signer::SignerError::Custom("Invalid signature length".to_string()))?; 100 | let signature = solana_sdk::signature::Signature::from(signature_bytes); 101 | Ok(signature) 102 | // END logic 103 | }) 104 | }) 105 | // END Blocking thread... 106 | } 107 | 108 | fn is_interactive(&self) -> bool { 109 | false 110 | } 111 | } 112 | 113 | mod gcp_test { 114 | #[cfg(test)] 115 | use super::*; 116 | #[cfg(test)] 117 | use dotenvy; 118 | #[cfg(test)] 119 | use solana_sdk::signer::Signer; 120 | #[cfg(test)] 121 | use google_cloud_kms::grpc::kms::v1::ListKeyRingsRequest; 122 | 123 | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] 124 | async fn test_signer() -> Result<(), Box> { 125 | dotenvy::from_filename_override(crate::ENV_FILE_PATH).ok(); 126 | 127 | let signer = GcpSigner::new("projects/cookbook-448105/locations/us-west1/keyRings/test/cryptoKeys/first_key/cryptoKeyVersions/1".to_string()).await?; 128 | let pubkey = signer.try_pubkey()?; 129 | println!("Pubkey: {:?}", pubkey); 130 | 131 | let signature = signer.try_sign_message(b"HelloWorld!")?; 132 | println!("Signature: {:?}", tk_rs::bytes_to_hex(signature.as_ref().into())); 133 | Ok(()) 134 | } 135 | 136 | #[tokio::test] 137 | async fn test_gcp() -> Result<(), Box> { 138 | dotenvy::from_filename_override(crate::ENV_FILE_PATH).ok(); 139 | 140 | let config = ClientConfig::default().with_auth().await?; 141 | let client = Client::new(config).await?; 142 | 143 | // list 144 | match client 145 | .list_key_rings( 146 | ListKeyRingsRequest { 147 | parent: "projects/cookbook-448105/locations/us-west1".to_string(), 148 | page_size: 5, 149 | page_token: "".to_string(), 150 | filter: "".to_string(), 151 | order_by: "".to_string(), 152 | }, 153 | None, 154 | ) 155 | .await 156 | { 157 | Ok(response) => { 158 | println!("List key rings"); 159 | for r in response.key_rings { 160 | println!("- {:?}", r); 161 | } 162 | } 163 | Err(err) => panic!("err: {:?}", err), 164 | }; 165 | 166 | // get 167 | match client 168 | .get_public_key( 169 | GetPublicKeyRequest { 170 | name: "projects/cookbook-448105/locations/us-west1/keyRings/test/cryptoKeys/first_key/cryptoKeyVersions/1" 171 | .to_string(), 172 | }, 173 | None, 174 | ) 175 | .await 176 | { 177 | Ok(response) => { 178 | println!("Get keyring: {:?}", response); 179 | } 180 | Err(err) => panic!("err: {:?}", err), 181 | } 182 | 183 | let resp =client.asymmetric_sign( 184 | AsymmetricSignRequest { 185 | name: "projects/cookbook-448105/locations/us-west1/keyRings/test/cryptoKeys/first_key/cryptoKeyVersions/1" 186 | .to_string(), 187 | digest: None, 188 | digest_crc32c: None, 189 | data: b"HelloWorld!".to_vec(), 190 | data_crc32c: None, 191 | }, 192 | None, 193 | ).await?; 194 | 195 | println!("Signature: {:?}", tk_rs::bytes_to_hex(&resp.signature)); 196 | 197 | Ok(()) 198 | } 199 | 200 | } -------------------------------------------------------------------------------- /utils/src/jito.rs: -------------------------------------------------------------------------------- 1 | use { 2 | std::time::Duration, 3 | solana_sdk::{instruction::Instruction, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, system_instruction}, 4 | jito_sdk_rust::JitoJsonRpcSDK, 5 | std::str::FromStr, 6 | reqwest, 7 | serde_json::Value 8 | }; 9 | 10 | 11 | #[derive(Debug)] 12 | pub struct BundleStatus { 13 | confirmation_status: Option, 14 | err: Option, 15 | transactions: Option>, 16 | } 17 | 18 | pub const MAX_RETRIES: u32 = 40; 19 | pub const RETRY_DELAY: Duration = Duration::from_secs(3); 20 | pub const JITO_ENGINE_URL: &str = "https://dallas.testnet.block-engine.jito.wtf/api/v1"; 21 | 22 | pub async fn create_jito_tip_instruction(sender_pubkey: Pubkey) -> Result> { 23 | let jito_sdk = JitoJsonRpcSDK::new(JITO_ENGINE_URL, None); 24 | 25 | let random_tip_account = jito_sdk.get_random_tip_account().await?; 26 | let jito_tip_account = Pubkey::from_str(&random_tip_account)?; 27 | let jito_tip_amount:u64 = get_max_tip_amount().await?; 28 | println!("Jito tip lamports: {}", jito_tip_amount); 29 | 30 | Ok(system_instruction::transfer( 31 | &sender_pubkey, 32 | &jito_tip_account, 33 | jito_tip_amount, 34 | )) 35 | } 36 | pub async fn submit_and_confirm_bundle(bundle: serde_json::Value) -> Result, Box> { 37 | 38 | let jito_sdk = JitoJsonRpcSDK::new(JITO_ENGINE_URL, None); 39 | 40 | // UUID for the bundle 41 | let uuid = None; 42 | 43 | // Send bundle using Jito SDK 44 | println!("Sending bundle with {} transactions...", bundle.as_array().unwrap().len()); 45 | let response = jito_sdk.send_bundle(Some(bundle), uuid).await?; 46 | 47 | // Extract bundle UUID from response 48 | let bundle_uuid = response["result"] 49 | .as_str() 50 | .ok_or("Failed to get bundle UUID from response")?; 51 | println!("Bundle sent with UUID: {}", bundle_uuid); 52 | 53 | confirm_bundle_status(&jito_sdk, &bundle_uuid).await 54 | 55 | } 56 | pub async fn confirm_bundle_status(jito_sdk: &JitoJsonRpcSDK, bundle_uuid: &str) -> Result, Box> { 57 | 58 | for attempt in 1..=MAX_RETRIES { 59 | println!("Checking bundle status (attempt {}/{})", attempt, MAX_RETRIES); 60 | 61 | let status_response = jito_sdk.get_in_flight_bundle_statuses(vec![bundle_uuid.to_string()]).await?; 62 | 63 | if let Some(result) = status_response.get("result") { 64 | if let Some(value) = result.get("value") { 65 | if let Some(statuses) = value.as_array() { 66 | if let Some(bundle_status) = statuses.get(0) { 67 | if let Some(status) = bundle_status.get("status") { 68 | match status.as_str() { 69 | Some("Landed") => { 70 | println!("Bundle landed on-chain. Checking final status..."); 71 | return Ok(check_final_bundle_status(&jito_sdk, bundle_uuid).await?); 72 | }, 73 | Some("Pending") => { 74 | println!("Bundle is pending. Waiting..."); 75 | }, 76 | Some(status) => { 77 | if status == "Failed" { 78 | return Err(format!("Bundle failed to land on-chain").into()); 79 | } 80 | println!("Unexpected bundle status: {}. Waiting...", status); 81 | }, 82 | None => { 83 | println!("Unable to parse bundle status. Waiting..."); 84 | } 85 | } 86 | } else { 87 | println!("Status field not found in bundle status. Waiting..."); 88 | } 89 | } else { 90 | println!("Bundle status not found. Waiting..."); 91 | } 92 | } else { 93 | println!("Unexpected value format. Waiting..."); 94 | } 95 | } else { 96 | println!("Value field not found in result. Waiting..."); 97 | 98 | } 99 | } else if let Some(error) = status_response.get("error") { 100 | println!("Error checking bundle status: {:?}", error); 101 | } else { 102 | println!("Unexpected response format. Waiting..."); 103 | } 104 | 105 | if attempt < MAX_RETRIES { 106 | std::thread::sleep(RETRY_DELAY); 107 | } 108 | } 109 | 110 | Err(format!("Failed to confirm bundle status after {} attempts", MAX_RETRIES).into()) 111 | } 112 | 113 | pub async fn get_max_tip_amount() -> Result> { 114 | // Query the API 115 | let response = reqwest::get("https://bundles.jito.wtf/api/v1/bundles/tip_floor").await?; 116 | let data: Value = response.json().await?; 117 | 118 | // Parse the JSON to get the 99th percentile tip 119 | let landed_tips_99th_percentile = data[0]["landed_tips_99th_percentile"] 120 | .as_f64() 121 | .ok_or("Failed to parse landed_tips_99th_percentile")?; 122 | 123 | println!("Jito landed_tips_99th_percentile: {}", landed_tips_99th_percentile); 124 | 125 | // Convert SOL to Lamports 126 | let jito_tip_amount = (landed_tips_99th_percentile * LAMPORTS_PER_SOL as f64) as u64; 127 | 128 | Ok(jito_tip_amount) 129 | } 130 | 131 | async fn check_final_bundle_status(jito_sdk: &JitoJsonRpcSDK, bundle_uuid: &str) -> Result, Box> { 132 | 133 | for attempt in 1..=MAX_RETRIES { 134 | println!("Checking final bundle status (attempt {}/{})", attempt, MAX_RETRIES); 135 | 136 | let status_response = jito_sdk.get_bundle_statuses(vec![bundle_uuid.to_string()]).await?; 137 | let bundle_status = get_bundle_status(&status_response)?; 138 | 139 | match bundle_status.confirmation_status.as_deref() { 140 | Some("confirmed") => { 141 | println!("Bundle confirmed on-chain. Waiting for finalization..."); 142 | check_transaction_error(&bundle_status)?; 143 | return match bundle_status.transactions { 144 | Some(transactions) => Ok(transactions), 145 | None => Err("Error retrieving transactions from finalized bundle status".into()), 146 | }; 147 | }, 148 | Some("finalized") => { 149 | println!("Bundle finalized on-chain successfully!"); 150 | check_transaction_error(&bundle_status)?; 151 | return match bundle_status.transactions { 152 | Some(transactions) => Ok(transactions), 153 | None => Err("Error retrieving transactions from finalized bundle status".into()), 154 | }; 155 | }, 156 | Some(status) => { 157 | println!("Unexpected final bundle status: {}. Continuing to poll...", status); 158 | }, 159 | None => { 160 | println!("Unable to parse final bundle status. Continuing to poll..."); 161 | } 162 | } 163 | 164 | if attempt < MAX_RETRIES { 165 | std::thread::sleep(RETRY_DELAY); 166 | } 167 | } 168 | 169 | Err(format!("Failed to get finalized status after {} attempts", MAX_RETRIES).into()) 170 | } 171 | 172 | fn get_bundle_status(status_response: &serde_json::Value) -> Result> { 173 | status_response 174 | .get("result") 175 | .and_then(|result| result.get("value")) 176 | .and_then(|value| value.as_array()) 177 | .and_then(|statuses| statuses.get(0)) 178 | .ok_or_else(|| format!("Failed to parse bundle status").into()) 179 | .map(|bundle_status| BundleStatus { 180 | confirmation_status: bundle_status.get("confirmation_status").and_then(|s| s.as_str()).map(String::from), 181 | err: bundle_status.get("err").cloned(), 182 | transactions: bundle_status.get("transactions").and_then(|t| t.as_array()).map(|arr| { 183 | arr.iter().filter_map(|v| v.as_str().map(String::from)).collect() 184 | }), 185 | }) 186 | } 187 | 188 | fn check_transaction_error(bundle_status: &BundleStatus) -> Result<(), Box> { 189 | if let Some(err) = &bundle_status.err { 190 | if err["Ok"].is_null() { 191 | println!("Transaction executed without errors."); 192 | Ok(()) 193 | } else { 194 | println!("Transaction encountered an error: {:?}", err); 195 | Err(format!("Transaction encountered an error").into()) 196 | } 197 | } else { 198 | Ok(()) 199 | } 200 | } -------------------------------------------------------------------------------- /utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gcp::GcpSigner; 2 | use solana_client::nonblocking::rpc_client::RpcClient as NonBlockingRpcClient; 3 | use solana_client::rpc_client::RpcClient; 4 | use solana_sdk::commitment_config::CommitmentConfig; 5 | use solana_sdk::signer::keypair::Keypair; 6 | use solana_sdk::signer::Signer; 7 | use solana_zk_sdk::encryption::auth_encryption::AeKey; 8 | use solana_zk_sdk::encryption::elgamal::{ElGamalKeypair, ElGamalSecretKey}; 9 | use std::env; 10 | use std::error::Error; 11 | use std::fs::OpenOptions; 12 | use std::io::Write; 13 | use dotenvy; 14 | 15 | pub mod gcp; 16 | pub mod jito; 17 | 18 | pub const ENV_FILE_PATH: &str = "../.env"; 19 | pub const RUNTIME_ENV_FILE_PATH: &str = "../runtime_output.env"; 20 | 21 | // Get or create a keypair from an .env file 22 | pub fn get_or_create_keypair(variable_name: &str) -> Result> { 23 | // First check runtime_output.env if it exists 24 | if std::path::Path::new(RUNTIME_ENV_FILE_PATH).exists() { 25 | dotenvy::from_filename_override(RUNTIME_ENV_FILE_PATH).ok(); 26 | if let Ok(secret_key_string) = env::var(variable_name) { 27 | // Try to parse from runtime_output.env 28 | let decoded_secret_key: Vec = serde_json::from_str(&secret_key_string)?; 29 | return Ok(Keypair::from_bytes(&decoded_secret_key)?); 30 | } 31 | } 32 | 33 | // Then check original .env 34 | dotenvy::from_filename_override(ENV_FILE_PATH).ok(); 35 | 36 | match env::var(variable_name) { 37 | Ok(secret_key_string) => { 38 | // Parse from .env 39 | let decoded_secret_key: Vec = serde_json::from_str(&secret_key_string)?; 40 | Ok(Keypair::from_bytes(&decoded_secret_key)?) 41 | } 42 | Err(_) => { 43 | // Create a new keypair if the environment variable is not found in either file 44 | let keypair = Keypair::new(); 45 | 46 | // Convert secret key to Vec and then to JSON, append to runtime_output.env file 47 | let secret_key_bytes = Vec::from(keypair.to_bytes()); 48 | let json_secret_key = serde_json::to_string(&secret_key_bytes)?; 49 | 50 | // Create runtime_output.env if it doesn't exist 51 | if !std::path::Path::new(RUNTIME_ENV_FILE_PATH).exists() { 52 | std::fs::File::create(RUNTIME_ENV_FILE_PATH)?; 53 | } 54 | 55 | // Open runtime_output.env file, create it if it does not exist 56 | let mut file = OpenOptions::new().append(true).create(true).open(RUNTIME_ENV_FILE_PATH)?; 57 | 58 | writeln!(file, "{}={}", variable_name, json_secret_key)?; 59 | 60 | Ok(keypair) 61 | } 62 | } 63 | } 64 | pub fn get_turnkey_signer(private_key_id_env: &str, public_key_env: &str) -> Result, Box> { 65 | let signer = tk_rs::TurnkeySigner::new( 66 | dotenvy::var("TURNKEY_API_PUBLIC_KEY").unwrap(), 67 | dotenvy::var("TURNKEY_API_PRIVATE_KEY").unwrap(), 68 | dotenvy::var("TURNKEY_ORGANIZATION_ID").unwrap(), 69 | dotenvy::var(private_key_id_env).unwrap(), 70 | dotenvy::var(public_key_env).unwrap(), 71 | )?; 72 | Ok(Box::new(signer)) 73 | } 74 | 75 | 76 | pub fn get_or_create_keypair_elgamal(variable_name: &str) -> Result> { 77 | // First check runtime_output.env if it exists 78 | if std::path::Path::new(RUNTIME_ENV_FILE_PATH).exists() { 79 | dotenvy::from_filename_override(RUNTIME_ENV_FILE_PATH).ok(); 80 | if let Ok(secret_key_string) = env::var(variable_name) { 81 | // Try to parse from runtime_output.env 82 | let decoded_secret_key: Vec = serde_json::from_str(&secret_key_string)?; 83 | return Ok(ElGamalKeypair::new(ElGamalSecretKey::from_seed(&decoded_secret_key)?)); 84 | } 85 | } 86 | 87 | // Then check original .env 88 | dotenvy::from_filename_override(ENV_FILE_PATH).ok(); 89 | 90 | match env::var(variable_name) { 91 | Ok(secret_key_string) => { 92 | let decoded_secret_key: Vec = serde_json::from_str(&secret_key_string)?; 93 | Ok(ElGamalKeypair::new(ElGamalSecretKey::from_seed(&decoded_secret_key)?)) 94 | } 95 | Err(_) => { 96 | let keypair = ElGamalKeypair::new_rand(); 97 | 98 | // Convert secret key to Vec and then to JSON, append to runtime_output.env file 99 | let secret_key_bytes = Vec::from(keypair.secret().as_bytes()); 100 | let json_secret_key = serde_json::to_string(&secret_key_bytes)?; 101 | 102 | // Create runtime_output.env if it doesn't exist 103 | if !std::path::Path::new(RUNTIME_ENV_FILE_PATH).exists() { 104 | std::fs::File::create(RUNTIME_ENV_FILE_PATH)?; 105 | } 106 | 107 | // Open runtime_output.env file, create it if it does not exist 108 | let mut file = OpenOptions::new().append(true).create(true).open(RUNTIME_ENV_FILE_PATH)?; 109 | 110 | writeln!(file, "{}={}", variable_name, json_secret_key)?; 111 | 112 | Ok(keypair) 113 | }, 114 | } 115 | } 116 | 117 | pub fn record_value<'a, T: serde::Serialize>(variable_name: &str, value: T) -> Result> { 118 | // Serialize the value to a JSON string 119 | let json_value = serde_json::to_string(&value)?; 120 | 121 | // Create runtime_output.env if it doesn't exist 122 | if !std::path::Path::new(RUNTIME_ENV_FILE_PATH).exists() { 123 | std::fs::File::create(RUNTIME_ENV_FILE_PATH)?; 124 | } 125 | 126 | // Read the existing runtime_output.env file content 127 | let mut content = std::fs::read_to_string(RUNTIME_ENV_FILE_PATH).unwrap_or_default(); 128 | 129 | // Remove any existing line with the same variable name 130 | content = content 131 | .lines() 132 | .filter(|line| !line.starts_with(&format!("{}=", variable_name))) 133 | .collect::>() 134 | .join("\n"); 135 | 136 | // Append the new variable value 137 | content.push_str(&format!("\n{}={}", variable_name, json_value)); 138 | 139 | // Write the updated content back to the runtime_output.env file 140 | std::fs::write(RUNTIME_ENV_FILE_PATH, content)?; 141 | 142 | Ok(value) 143 | } 144 | 145 | pub fn load_value(variable_name: &str) -> Result> { 146 | // First try to load from runtime_output.env 147 | if std::path::Path::new(RUNTIME_ENV_FILE_PATH).exists() { 148 | dotenvy::from_filename_override(RUNTIME_ENV_FILE_PATH).ok(); 149 | if let Ok(env_value) = env::var(variable_name) { 150 | // Try to deserialize the JSON string to the object 151 | let value: Result = serde_json::from_str(&env_value); 152 | 153 | // If deserialization succeeds, return the value 154 | if let Ok(val) = value { 155 | return Ok(val); 156 | } 157 | 158 | // Try to parse as a plain string 159 | let plain_value: Result = serde_json::from_str(&format!("\"{}\"", env_value)); 160 | if let Ok(val) = plain_value { 161 | return Ok(val); 162 | } 163 | } 164 | } 165 | 166 | // If not found in runtime_output.env, try the original .env 167 | dotenvy::from_filename_override(ENV_FILE_PATH).ok(); 168 | 169 | // Get the environment variable 170 | let env_value = env::var(variable_name)?; 171 | 172 | // Try to deserialize the JSON string to the object 173 | let value: Result = serde_json::from_str(&env_value); 174 | 175 | // If deserialization fails, try to parse it as a plain string 176 | match value { 177 | Ok(val) => Ok(val), 178 | Err(_) => { 179 | // Attempt to parse as a plain string or integer 180 | let plain_value: T = serde_json::from_str(&format!("\"{}\"", env_value))?; 181 | Ok(plain_value) 182 | } 183 | } 184 | } 185 | 186 | pub fn get_rpc_client() -> Result> { 187 | dotenvy::from_filename_override(ENV_FILE_PATH).ok(); 188 | 189 | let client = RpcClient::new_with_commitment( 190 | String::from(env::var("RPC_URL")?), 191 | CommitmentConfig::confirmed(), 192 | ); 193 | Ok(client) 194 | } 195 | 196 | pub fn get_non_blocking_rpc_client() -> Result> { 197 | dotenvy::from_filename_override(ENV_FILE_PATH).ok(); 198 | 199 | let client = NonBlockingRpcClient::new_with_commitment( 200 | String::from(env::var("RPC_URL")?), 201 | CommitmentConfig::confirmed(), 202 | ); 203 | Ok(client) 204 | } 205 | 206 | /// Spawns a blocking task to generate both AeKey and ElGamalKeypair from a given signer. 207 | /// This utility function helps avoid Tokio runtime conflicts by isolating blocking operations. 208 | pub async fn tokio_spawn_blocking_turnkey_signer_keys( 209 | private_key_id_env: &str, 210 | public_key_env: &str, 211 | ) -> Result<(Box, AeKey, ElGamalKeypair), String> { 212 | let private_key_id = private_key_id_env.to_string(); 213 | let public_key = public_key_env.to_string(); 214 | 215 | tokio::task::spawn_blocking(move || -> Result<(Box, AeKey, ElGamalKeypair), String> { 216 | let signer = get_turnkey_signer(&private_key_id, &public_key) 217 | .map_err(|e| e.to_string())?; 218 | 219 | let elgamal_keypair = ElGamalKeypair::new_from_signer(&signer, &signer.pubkey().to_bytes()) 220 | .map_err(|e| e.to_string())?; 221 | 222 | let aes_key = AeKey::new_from_signer(&signer, &signer.pubkey().to_bytes()) 223 | .map_err(|e| e.to_string())?; 224 | 225 | Ok((signer, aes_key, elgamal_keypair)) 226 | }) 227 | .await 228 | .map_err(|e| e.to_string())? 229 | } 230 | 231 | pub fn get_turnkey_signers_from_env( 232 | private_key_id_env: &str, 233 | public_key_env: &str, 234 | ) -> Result, String> { 235 | let private_key_id = private_key_id_env.to_string(); 236 | let public_key = public_key_env.to_string(); 237 | 238 | let signer = get_turnkey_signer(&private_key_id, &public_key) 239 | .map_err(|e| e.to_string())?; 240 | 241 | Ok(signer) 242 | } 243 | 244 | pub async fn get_gcp_signer_from_env( 245 | resource_name: &str, 246 | ) -> Result> { 247 | dotenvy::from_filename_override(ENV_FILE_PATH).ok(); 248 | 249 | let signer = GcpSigner::new(resource_name.to_string()).await?; 250 | Ok(signer) 251 | } 252 | 253 | pub async fn run_with_retry( 254 | max_retries: usize, 255 | operation: F, 256 | ) -> Result<(), Box> 257 | where 258 | F: Fn() -> Fut, 259 | Fut: std::future::Future>>, 260 | { 261 | for attempt in 1..=max_retries { 262 | println!("Attempt {} of {}", attempt, max_retries); 263 | match operation().await { 264 | Ok(_) => return Ok(()), 265 | Err(e) => { 266 | println!("Error: {}. Retrying...", e); 267 | if attempt == max_retries { 268 | return Err(e); 269 | } 270 | } 271 | } 272 | } 273 | Ok(()) 274 | } 275 | 276 | pub fn print_transaction_url(pre_text: &str, signature: &str) { 277 | const SOLANA_EXPLORER_URL: &str = "https://explorer.solana.com/tx/"; 278 | 279 | let cluster = match env::var("RPC_URL").unwrap_or_default() { 280 | url if url.contains("devnet") => "?cluster=devnet", 281 | url if url.contains("testnet") => "?cluster=testnet", 282 | _ => "", 283 | }; 284 | 285 | println!( 286 | "\n{}: {}{}{}", 287 | pre_text, 288 | SOLANA_EXPLORER_URL, 289 | signature, 290 | cluster 291 | ); 292 | } 293 | --------------------------------------------------------------------------------