├── .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