@cloak.dev/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +268 -0
- package/dist/chunk-2SOX3JNO.js +255 -0
- package/dist/index.cjs +9785 -0
- package/dist/index.d.cts +3430 -0
- package/dist/index.d.ts +3430 -0
- package/dist/index.js +9338 -0
- package/dist/utxo-LSTVI4HH.js +42 -0
- package/package.json +120 -0
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# @cloak.dev/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the Cloak Protocol - Private transactions on Solana using zero-knowledge proofs.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔒 **Private Transfers**: Send SOL privately using zero-knowledge proofs
|
|
8
|
+
- 👥 **Multi-Recipient**: Support for 1-5 recipients in a single transaction
|
|
9
|
+
- 💱 **Token Swaps**: Swap SOL for SPL tokens privately (SOL → USDC, etc.)
|
|
10
|
+
- 🔐 **Type-Safe**: Full TypeScript support with comprehensive types
|
|
11
|
+
- 🌐 **Cross-Platform**: Works in browser (React, Next.js) and Node.js
|
|
12
|
+
- ⚡ **Simple API**: Easy-to-use high-level client with wallet adapter support
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @cloak.dev/sdk @solana/web3.js
|
|
18
|
+
# or
|
|
19
|
+
yarn add @cloak.dev/sdk @solana/web3.js
|
|
20
|
+
# or
|
|
21
|
+
pnpm add @cloak.dev/sdk @solana/web3.js
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Note**: For swap functionality, you'll also need `@solana/spl-token`:
|
|
25
|
+
```bash
|
|
26
|
+
npm install @solana/spl-token
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Risk Quote Configuration (Recommended)
|
|
32
|
+
|
|
33
|
+
- Default production flow: set `CLOAK_RELAY_URL` and let relay handle Range quoting.
|
|
34
|
+
- Keep `RANGE_API_KEY` only in relay/service backend, not in SDK clients.
|
|
35
|
+
- SDK auto-uses `${CLOAK_RELAY_URL}/range-quote` when `relayUrl` is provided.
|
|
36
|
+
- SDK-side `rangeApiKey` exists only as a server-side fallback (no-relay scenarios).
|
|
37
|
+
|
|
38
|
+
### Maintained Examples
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm run example:fast-send
|
|
42
|
+
npm run example:fast-usdc-send
|
|
43
|
+
npm run example:usdc-pool-transfer
|
|
44
|
+
npm run example:swap
|
|
45
|
+
npm run example:swap-recovery
|
|
46
|
+
npm run example:swap-usdc
|
|
47
|
+
npm run example:swap-brz
|
|
48
|
+
npm run example:transfer
|
|
49
|
+
npm run test:examples
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`example:transfer` runs deposit -> shield-to-shield transfer (`public_amount=0`) -> recipient withdraw verification and prints stable `FULL_SIG|transfer|deposit|...`, `FULL_SIG|transfer|tx|...`, and `COMMITMENT_INDICES|transfer|[...]` markers.
|
|
53
|
+
|
|
54
|
+
`example:fast-usdc-send` is the one-shot private send path for USDC recipients (deposit SOL, swap privately to USDC, deliver to recipient ATA).
|
|
55
|
+
|
|
56
|
+
`example:usdc-pool-transfer` is the mint-scoped USDC pool transfer path (User A deposits USDC into Cloak USDC pool, sends shielded USDC to User B, and User B spends the received shielded note).
|
|
57
|
+
|
|
58
|
+
`example:swap-usdc` is the canonical Nora swap evidence path (SOL -> USDC) and prints quote/route details, `FULL_SIG|swap-usdc|transact_swap|...`, `FULL_SIG|swap-usdc|swap_completed|...`, and `RECIPIENT_USDC_BALANCE|...` markers.
|
|
59
|
+
|
|
60
|
+
`example:swap-recovery` focuses on pending/timeout behavior. It submits a swap, tracks `swap_phase` + `slots_remaining` from relay `/status`, and can optionally call `close_timed_out` (`AUTO_CLOSE_TIMED_OUT=1`) once `can_recover=true`.
|
|
61
|
+
|
|
62
|
+
`example:swap-brz` first attempts BRZ (`FtgGSFADXBtroxq8VCausXRr2of47QBf5AS1NtZCu4GD`) and automatically falls back to USDC if BRZ routing is unavailable. It always emits `QUOTE_ROUTE|...`, `SWAP_OUTPUT_MINT|...`, and (when fallback happens) `BRZ_FALLBACK_TO|...`.
|
|
63
|
+
|
|
64
|
+
`test:examples` runs all maintained examples in `CLOAK_EXAMPLE_DRY_RUN=1` mode so CI/local checks validate script wiring without requiring funded wallets or live RPC execution.
|
|
65
|
+
|
|
66
|
+
### Node.js (with Keypair)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { CloakSDK } from "@cloak.dev/sdk";
|
|
70
|
+
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
|
|
71
|
+
|
|
72
|
+
// Initialize connection and keypair
|
|
73
|
+
const connection = new Connection("https://api.devnet.solana.com");
|
|
74
|
+
const keypair = Keypair.fromSecretKey(/* your secret key */);
|
|
75
|
+
|
|
76
|
+
// Initialize SDK
|
|
77
|
+
const sdk = new CloakSDK({
|
|
78
|
+
keypairBytes: keypair.secretKey,
|
|
79
|
+
network: "devnet",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Deposit SOL into the privacy pool
|
|
83
|
+
const depositResult = await sdk.deposit(connection, 100_000_000); // 0.1 SOL
|
|
84
|
+
console.log("Deposited! Leaf index:", depositResult.leafIndex);
|
|
85
|
+
|
|
86
|
+
// Withdraw to a recipient
|
|
87
|
+
const withdrawResult = await sdk.withdraw(
|
|
88
|
+
connection,
|
|
89
|
+
depositResult.note,
|
|
90
|
+
new PublicKey("RECIPIENT_ADDRESS"),
|
|
91
|
+
{ withdrawAll: true }
|
|
92
|
+
);
|
|
93
|
+
console.log("Withdrawn! TX:", withdrawResult.signature);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### React/Next.js (with Wallet Adapter)
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { CloakSDK } from "@cloak.dev/sdk";
|
|
100
|
+
import { useWallet, useConnection } from "@solana/wallet-adapter-react";
|
|
101
|
+
import { PublicKey } from "@solana/web3.js";
|
|
102
|
+
|
|
103
|
+
function PrivateTransfer() {
|
|
104
|
+
const { publicKey, signTransaction, sendTransaction } = useWallet();
|
|
105
|
+
const { connection } = useConnection();
|
|
106
|
+
|
|
107
|
+
// Initialize SDK with wallet adapter
|
|
108
|
+
const sdk = useMemo(() => {
|
|
109
|
+
if (!publicKey) return null;
|
|
110
|
+
return new CloakSDK({
|
|
111
|
+
network: "devnet",
|
|
112
|
+
wallet: {
|
|
113
|
+
publicKey,
|
|
114
|
+
signTransaction: (tx) => signTransaction!(tx),
|
|
115
|
+
sendTransaction: (tx, conn, opts) => sendTransaction(tx, conn, opts),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}, [publicKey, signTransaction, sendTransaction]);
|
|
119
|
+
|
|
120
|
+
const handleDeposit = async () => {
|
|
121
|
+
const result = await sdk.deposit(connection, 100_000_000);
|
|
122
|
+
console.log("Deposited!", result.signature);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return <button onClick={handleDeposit}>Deposit 0.1 SOL</button>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Core Methods
|
|
130
|
+
|
|
131
|
+
### Deposit
|
|
132
|
+
|
|
133
|
+
Deposit SOL into the privacy pool:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const result = await sdk.deposit(connection, 100_000_000); // 0.1 SOL
|
|
137
|
+
// Save the note securely - you need it to withdraw!
|
|
138
|
+
console.log(result.note);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Withdraw
|
|
142
|
+
|
|
143
|
+
Withdraw to a single recipient:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
const result = await sdk.withdraw(
|
|
147
|
+
connection,
|
|
148
|
+
note,
|
|
149
|
+
recipientPublicKey,
|
|
150
|
+
{ withdrawAll: true }
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Send to Multiple Recipients
|
|
155
|
+
|
|
156
|
+
Send to up to 5 recipients:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const result = await sdk.send(connection, note, [
|
|
160
|
+
{ recipient: addr1, amount: 50_000_000 },
|
|
161
|
+
{ recipient: addr2, amount: 47_000_000 },
|
|
162
|
+
]);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Swap
|
|
166
|
+
|
|
167
|
+
Swap SOL for SPL tokens:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const result = await sdk.swap(connection, note, recipientPublicKey, {
|
|
171
|
+
outputMint: "TOKEN_MINT_ADDRESS",
|
|
172
|
+
minOutputAmount: 1000000,
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Fee Structure
|
|
177
|
+
|
|
178
|
+
- **Fixed Fee**: 0.005 SOL (5,000,000 lamports)
|
|
179
|
+
- **Variable Fee**: 0.3% of deposit amount
|
|
180
|
+
|
|
181
|
+
Use `getDistributableAmount()` to calculate the amount after fees:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { getDistributableAmount } from "@cloak.dev/sdk";
|
|
185
|
+
|
|
186
|
+
const deposited = 100_000_000; // 0.1 SOL
|
|
187
|
+
const afterFees = getDistributableAmount(deposited); // ~94,700,000 lamports
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Notes
|
|
191
|
+
|
|
192
|
+
A **Cloak Note** is a cryptographic commitment representing a private amount of SOL:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
interface CloakNote {
|
|
196
|
+
version: string;
|
|
197
|
+
amount: number; // Amount in lamports
|
|
198
|
+
commitment: string; // Commitment hash
|
|
199
|
+
sk_spend: string; // Spending key (keep secret!)
|
|
200
|
+
r: string; // Randomness
|
|
201
|
+
timestamp: number;
|
|
202
|
+
network: Network;
|
|
203
|
+
leafIndex?: number; // Set after deposit
|
|
204
|
+
depositSignature?: string;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**⚠️ Important**: Save your notes securely! Without the note, you cannot withdraw your funds.
|
|
209
|
+
|
|
210
|
+
## Compliance Chain Scanning
|
|
211
|
+
|
|
212
|
+
Cloak supports a viewing-key commitment flow for compliance and self-discovery:
|
|
213
|
+
|
|
214
|
+
- The viewing key itself is never written on-chain.
|
|
215
|
+
- Only `viewing_key_commitment = SHA256(viewing_key_public)` is stored on-chain.
|
|
216
|
+
- A scanner recomputes this commitment from `viewing_key_public` and matches program transactions.
|
|
217
|
+
|
|
218
|
+
```mermaid
|
|
219
|
+
sequenceDiagram
|
|
220
|
+
autonumber
|
|
221
|
+
participant U as User Wallet + SDK
|
|
222
|
+
participant R as Relay
|
|
223
|
+
participant C as Cloak Program (Solana)
|
|
224
|
+
participant S as Scanner
|
|
225
|
+
|
|
226
|
+
U->>U: Generate (vk_priv, vk_pub)
|
|
227
|
+
U->>U: vkc = SHA256(vk_pub)
|
|
228
|
+
U->>R: Register vk_priv (signed)
|
|
229
|
+
U->>R: POST /transact + metadata_bundle + viewing_key_commitment=vkc
|
|
230
|
+
R->>C: Submit instruction [proof|public_inputs|vkc]
|
|
231
|
+
C-->>R: Confirm tx signature
|
|
232
|
+
R-->>U: Return signature
|
|
233
|
+
|
|
234
|
+
S->>S: Compute target_vkc from vk_pub
|
|
235
|
+
S->>C: Fetch recent program txs
|
|
236
|
+
S->>S: Decode ix data, extract vkc bytes, filter target_vkc
|
|
237
|
+
alt Relay enrichment enabled
|
|
238
|
+
S->>R: POST /admin/compliance/decrypt (admin signed)
|
|
239
|
+
R-->>S: Decrypted metadata rows (amount/recipient/type)
|
|
240
|
+
S->>S: Join on commitment + public_amount hints
|
|
241
|
+
end
|
|
242
|
+
S-->>U: Matching transactions
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Error Handling
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { CloakError } from "@cloak.dev/sdk";
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await sdk.withdraw(connection, note, recipient);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof CloakError) {
|
|
254
|
+
console.log("Category:", error.category); // 'wallet', 'network', 'prover', etc.
|
|
255
|
+
console.log("Retryable:", error.retryable);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Links
|
|
261
|
+
|
|
262
|
+
- Website: [https://cloak.ag](https://cloak.ag)
|
|
263
|
+
- Documentation: [https://docs.cloak.ag](https://docs.cloak.ag)
|
|
264
|
+
- GitHub: [https://github.com/cloak-ag/sdk](https://github.com/cloak-ag/sdk)
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
Apache-2.0
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/core/utxo.ts
|
|
9
|
+
import { PublicKey } from "@solana/web3.js";
|
|
10
|
+
import { buildPoseidon } from "circomlibjs";
|
|
11
|
+
import { blake3 } from "@noble/hashes/blake3";
|
|
12
|
+
var FIELD_MODULUS = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617");
|
|
13
|
+
var UTXO_KEY_DOMAIN = new TextEncoder().encode("cloak_utxo_priv_v1");
|
|
14
|
+
var NATIVE_SOL_MINT = new PublicKey("So11111111111111111111111111111111111111112");
|
|
15
|
+
var poseidonInstance = null;
|
|
16
|
+
async function getPoseidon() {
|
|
17
|
+
if (!poseidonInstance) {
|
|
18
|
+
poseidonInstance = await buildPoseidon();
|
|
19
|
+
}
|
|
20
|
+
return poseidonInstance;
|
|
21
|
+
}
|
|
22
|
+
function randomFieldElement() {
|
|
23
|
+
const bytes = new Uint8Array(32);
|
|
24
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
25
|
+
crypto.getRandomValues(bytes);
|
|
26
|
+
} else {
|
|
27
|
+
const { randomBytes } = __require("crypto");
|
|
28
|
+
const nodeBytes = randomBytes(32);
|
|
29
|
+
for (let i = 0; i < 32; i++) {
|
|
30
|
+
bytes[i] = nodeBytes[i];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let value = BigInt(0);
|
|
34
|
+
for (let i = 0; i < 32; i++) {
|
|
35
|
+
value = value << BigInt(8) | BigInt(bytes[i]);
|
|
36
|
+
}
|
|
37
|
+
return value % (FIELD_MODULUS >> BigInt(4));
|
|
38
|
+
}
|
|
39
|
+
async function generateUtxoKeypair() {
|
|
40
|
+
const poseidon = await getPoseidon();
|
|
41
|
+
const privateKey = randomFieldElement();
|
|
42
|
+
const publicKeyHash = poseidon([privateKey, BigInt(0)]);
|
|
43
|
+
const publicKey = poseidon.F.toObject(publicKeyHash);
|
|
44
|
+
return { privateKey, publicKey };
|
|
45
|
+
}
|
|
46
|
+
async function deriveUtxoKeypairFromSpendKey(skSpend) {
|
|
47
|
+
if (!skSpend || !(skSpend instanceof Uint8Array) || skSpend.length !== 32) {
|
|
48
|
+
throw new Error("skSpend must be 32 bytes");
|
|
49
|
+
}
|
|
50
|
+
const preimage = new Uint8Array(UTXO_KEY_DOMAIN.length + skSpend.length);
|
|
51
|
+
preimage.set(UTXO_KEY_DOMAIN, 0);
|
|
52
|
+
preimage.set(skSpend, UTXO_KEY_DOMAIN.length);
|
|
53
|
+
const hash = blake3(preimage);
|
|
54
|
+
let value = BigInt(0);
|
|
55
|
+
for (let i = 0; i < 32; i++) {
|
|
56
|
+
value = value << BigInt(8) | BigInt(hash[i]);
|
|
57
|
+
}
|
|
58
|
+
const privateKey = value % (FIELD_MODULUS >> BigInt(4));
|
|
59
|
+
const publicKey = await derivePublicKey(privateKey);
|
|
60
|
+
return { privateKey, publicKey };
|
|
61
|
+
}
|
|
62
|
+
async function derivePublicKey(privateKey) {
|
|
63
|
+
const poseidon = await getPoseidon();
|
|
64
|
+
const hash = poseidon([privateKey, BigInt(0)]);
|
|
65
|
+
return poseidon.F.toObject(hash);
|
|
66
|
+
}
|
|
67
|
+
async function createUtxo(amount, keypair, mintAddress = NATIVE_SOL_MINT) {
|
|
68
|
+
const blinding = randomFieldElement();
|
|
69
|
+
const utxo = {
|
|
70
|
+
amount,
|
|
71
|
+
keypair,
|
|
72
|
+
blinding,
|
|
73
|
+
mintAddress
|
|
74
|
+
};
|
|
75
|
+
utxo.commitment = await computeCommitment(utxo);
|
|
76
|
+
return utxo;
|
|
77
|
+
}
|
|
78
|
+
async function createZeroUtxo(mintAddress = NATIVE_SOL_MINT, salt) {
|
|
79
|
+
const poseidon = await getPoseidon();
|
|
80
|
+
const effectiveSalt = salt ?? randomFieldElement();
|
|
81
|
+
const publicKeyHash = poseidon([effectiveSalt, BigInt(0)]);
|
|
82
|
+
const publicKey = poseidon.F.toObject(publicKeyHash);
|
|
83
|
+
const keypair = { privateKey: effectiveSalt, publicKey };
|
|
84
|
+
const utxo = {
|
|
85
|
+
amount: BigInt(0),
|
|
86
|
+
keypair,
|
|
87
|
+
blinding: BigInt(0),
|
|
88
|
+
mintAddress
|
|
89
|
+
};
|
|
90
|
+
utxo.commitment = await computeCommitment(utxo);
|
|
91
|
+
utxo.index = 0;
|
|
92
|
+
return utxo;
|
|
93
|
+
}
|
|
94
|
+
function pubkeyToFieldElement(pubkey) {
|
|
95
|
+
const bytes = pubkey.toBytes();
|
|
96
|
+
let value = BigInt(0);
|
|
97
|
+
for (let i = 0; i < 32; i++) {
|
|
98
|
+
value = value << BigInt(8) | BigInt(bytes[i]);
|
|
99
|
+
}
|
|
100
|
+
return value % FIELD_MODULUS;
|
|
101
|
+
}
|
|
102
|
+
async function computeCommitment(utxo) {
|
|
103
|
+
const poseidon = await getPoseidon();
|
|
104
|
+
const mintField = pubkeyToFieldElement(utxo.mintAddress);
|
|
105
|
+
const hash = poseidon([
|
|
106
|
+
utxo.amount,
|
|
107
|
+
utxo.keypair.publicKey,
|
|
108
|
+
utxo.blinding,
|
|
109
|
+
mintField
|
|
110
|
+
]);
|
|
111
|
+
return poseidon.F.toObject(hash);
|
|
112
|
+
}
|
|
113
|
+
async function computeSignature(privateKey, commitment, pathIndex) {
|
|
114
|
+
const poseidon = await getPoseidon();
|
|
115
|
+
const hash = poseidon([privateKey, commitment, pathIndex]);
|
|
116
|
+
return poseidon.F.toObject(hash);
|
|
117
|
+
}
|
|
118
|
+
async function computeNullifier(utxo) {
|
|
119
|
+
if (utxo.index === void 0) {
|
|
120
|
+
throw new Error("UTXO must have an index to compute nullifier");
|
|
121
|
+
}
|
|
122
|
+
const poseidon = await getPoseidon();
|
|
123
|
+
const commitment = utxo.commitment ?? await computeCommitment(utxo);
|
|
124
|
+
const pathIndex = BigInt(utxo.index);
|
|
125
|
+
const signature = await computeSignature(
|
|
126
|
+
utxo.keypair.privateKey,
|
|
127
|
+
commitment,
|
|
128
|
+
pathIndex
|
|
129
|
+
);
|
|
130
|
+
const hash = poseidon([commitment, pathIndex, signature]);
|
|
131
|
+
return poseidon.F.toObject(hash);
|
|
132
|
+
}
|
|
133
|
+
function serializeUtxo(utxo) {
|
|
134
|
+
const buffer = new ArrayBuffer(128);
|
|
135
|
+
const view = new DataView(buffer);
|
|
136
|
+
const amountBytes = bigintToBytes(utxo.amount, 8);
|
|
137
|
+
for (let i = 0; i < 8; i++) {
|
|
138
|
+
view.setUint8(i, amountBytes[i]);
|
|
139
|
+
}
|
|
140
|
+
const privkeyBytes = bigintToBytes(utxo.keypair.privateKey, 32);
|
|
141
|
+
for (let i = 0; i < 32; i++) {
|
|
142
|
+
view.setUint8(8 + i, privkeyBytes[i]);
|
|
143
|
+
}
|
|
144
|
+
const blindingBytes = bigintToBytes(utxo.blinding, 32);
|
|
145
|
+
for (let i = 0; i < 32; i++) {
|
|
146
|
+
view.setUint8(40 + i, blindingBytes[i]);
|
|
147
|
+
}
|
|
148
|
+
const mintBytes = utxo.mintAddress.toBytes();
|
|
149
|
+
for (let i = 0; i < 32; i++) {
|
|
150
|
+
view.setUint8(72 + i, mintBytes[i]);
|
|
151
|
+
}
|
|
152
|
+
view.setUint32(104, utxo.index ?? 0, true);
|
|
153
|
+
return new Uint8Array(buffer);
|
|
154
|
+
}
|
|
155
|
+
async function deserializeUtxo(bytes) {
|
|
156
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset);
|
|
157
|
+
const amountBytes = bytes.slice(0, 8);
|
|
158
|
+
const amount = bytesToBigint(amountBytes);
|
|
159
|
+
const privkeyBytes = bytes.slice(8, 40);
|
|
160
|
+
const privateKey = bytesToBigint(privkeyBytes);
|
|
161
|
+
const publicKey = await derivePublicKey(privateKey);
|
|
162
|
+
const blindingBytes = bytes.slice(40, 72);
|
|
163
|
+
const blinding = bytesToBigint(blindingBytes);
|
|
164
|
+
const mintBytes = bytes.slice(72, 104);
|
|
165
|
+
const mintAddress = new PublicKey(mintBytes);
|
|
166
|
+
const index = view.getUint32(104, true);
|
|
167
|
+
const utxo = {
|
|
168
|
+
amount,
|
|
169
|
+
keypair: { privateKey, publicKey },
|
|
170
|
+
blinding,
|
|
171
|
+
mintAddress,
|
|
172
|
+
index: index > 0 ? index : void 0
|
|
173
|
+
};
|
|
174
|
+
utxo.commitment = await computeCommitment(utxo);
|
|
175
|
+
return utxo;
|
|
176
|
+
}
|
|
177
|
+
function bigintToBytes(value, length) {
|
|
178
|
+
const result = new Uint8Array(length);
|
|
179
|
+
let remaining = value;
|
|
180
|
+
for (let i = 0; i < length; i++) {
|
|
181
|
+
result[i] = Number(remaining & BigInt(255));
|
|
182
|
+
remaining >>= BigInt(8);
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
function bytesToBigint(bytes) {
|
|
187
|
+
let result = BigInt(0);
|
|
188
|
+
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
189
|
+
result = result << BigInt(8) | BigInt(bytes[i]);
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
function bigintToHex(value) {
|
|
194
|
+
return value.toString(16).padStart(64, "0");
|
|
195
|
+
}
|
|
196
|
+
function hexToBigint(hex) {
|
|
197
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
198
|
+
return BigInt("0x" + cleanHex);
|
|
199
|
+
}
|
|
200
|
+
function bigintToBytes32(value) {
|
|
201
|
+
const result = new Uint8Array(32);
|
|
202
|
+
let remaining = value;
|
|
203
|
+
for (let i = 31; i >= 0; i--) {
|
|
204
|
+
result[i] = Number(remaining & BigInt(255));
|
|
205
|
+
remaining >>= BigInt(8);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
async function utxoEquals(a, b) {
|
|
210
|
+
const commitmentA = a.commitment ?? await computeCommitment(a);
|
|
211
|
+
const commitmentB = b.commitment ?? await computeCommitment(b);
|
|
212
|
+
return commitmentA === commitmentB;
|
|
213
|
+
}
|
|
214
|
+
function sumUtxoAmounts(utxos) {
|
|
215
|
+
return utxos.reduce((sum, utxo) => sum + utxo.amount, BigInt(0));
|
|
216
|
+
}
|
|
217
|
+
function selectUtxos(available, targetAmount) {
|
|
218
|
+
const sorted = [...available].sort(
|
|
219
|
+
(a, b) => Number(b.amount - a.amount)
|
|
220
|
+
);
|
|
221
|
+
const selected = [];
|
|
222
|
+
let total = BigInt(0);
|
|
223
|
+
for (const utxo of sorted) {
|
|
224
|
+
if (total >= targetAmount) break;
|
|
225
|
+
selected.push(utxo);
|
|
226
|
+
total += utxo.amount;
|
|
227
|
+
}
|
|
228
|
+
if (total < targetAmount) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return selected;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
__require,
|
|
236
|
+
NATIVE_SOL_MINT,
|
|
237
|
+
randomFieldElement,
|
|
238
|
+
generateUtxoKeypair,
|
|
239
|
+
deriveUtxoKeypairFromSpendKey,
|
|
240
|
+
derivePublicKey,
|
|
241
|
+
createUtxo,
|
|
242
|
+
createZeroUtxo,
|
|
243
|
+
pubkeyToFieldElement,
|
|
244
|
+
computeCommitment,
|
|
245
|
+
computeSignature,
|
|
246
|
+
computeNullifier,
|
|
247
|
+
serializeUtxo,
|
|
248
|
+
deserializeUtxo,
|
|
249
|
+
bigintToHex,
|
|
250
|
+
hexToBigint,
|
|
251
|
+
bigintToBytes32,
|
|
252
|
+
utxoEquals,
|
|
253
|
+
sumUtxoAmounts,
|
|
254
|
+
selectUtxos
|
|
255
|
+
};
|