@cloak.dev/sdk 0.1.8 → 0.2.1
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/LICENSE +202 -0
- package/README.md +573 -205
- package/dist/chunk-KNM6K5NG.js +507 -0
- package/dist/index.cjs +6039 -4624
- package/dist/index.d.cts +1817 -862
- package/dist/index.d.ts +1817 -862
- package/dist/index.js +4624 -3525
- package/dist/{utxo-LSTVI4HH.js → utxo-XPEGGWZ6.js} +5 -3
- package/package.json +51 -20
- package/dist/chunk-2SOX3JNO.js +0 -255
package/README.md
CHANGED
|
@@ -1,53 +1,104 @@
|
|
|
1
1
|
# @cloak.dev/sdk
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for
|
|
3
|
+
TypeScript SDK for Cloak: shielded transactions on Solana. Shield SOL, USDC or USDT into a
|
|
4
|
+
per-mint pool, send privately inside the pool, withdraw to any address, or swap shielded SOL to
|
|
5
|
+
USDC/USDT with the output landing in a public token account. Groth16 proofs are generated
|
|
6
|
+
client-side (snarkjs) and verified on-chain by the Cloak program.
|
|
4
7
|
|
|
5
|
-
|
|
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
|
|
8
|
+
Version 0.2.0 targets the mainnet program deployed on 2026-08-24 and the ceremony-frozen
|
|
9
|
+
`cloak-transaction-0.2.0` circuit bundle. See [Version and compatibility](#version-and-compatibility).
|
|
13
10
|
|
|
14
11
|
## Installation
|
|
15
12
|
|
|
16
13
|
```bash
|
|
17
|
-
npm install @cloak.dev/sdk @solana/web3.js
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
npm install @cloak.dev/sdk@^0.2.0 @solana/web3.js
|
|
15
|
+
# swaps and SPL pools also need:
|
|
16
|
+
npm install @solana/spl-token
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Node >= 18. Ships ESM and CJS builds with type declarations. Runs in Node and in the browser
|
|
20
|
+
(wallet-adapter signing); the on-chain Merkle-tree rebuild fallback is not available in browsers.
|
|
21
|
+
|
|
22
|
+
## Networks
|
|
23
|
+
|
|
24
|
+
### Mainnet (defaults)
|
|
25
|
+
|
|
26
|
+
| Setting | Value |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| Program | `zh1eLd6rSphLejbFfJEneUwzHRfMKxgzrgkfwA6qRkW` (`CLOAK_PROGRAM_ID`) |
|
|
29
|
+
| Cloak endpoint | `https://api.cloak.ag` — pinned into the build, not configurable (see below) |
|
|
30
|
+
| Circuits | `https://storage.googleapis.com/cloak-circuits/circuits/0.2.0` (the SDK default) |
|
|
31
|
+
| Pools | WSOL (`NATIVE_SOL_MINT`), USDC `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`, USDT `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` |
|
|
32
|
+
| RPC | any mainnet RPC; use `new Connection(url, "confirmed")` |
|
|
33
|
+
|
|
34
|
+
The endpoint is fixed when the SDK is built and cannot be changed by a consumer. A published build
|
|
35
|
+
carries `RELAY_ORIGIN_ALLOWLIST` as a compiled-in constant, every request is checked against it
|
|
36
|
+
before it leaves the process, and there is no option, environment variable, or bundler define that
|
|
37
|
+
moves it. Name it by importing the constant rather than typing a host:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { CLOAK_PRODUCTION_RELAY_URL } from "@cloak.dev/sdk";
|
|
41
|
+
|
|
42
|
+
relayUrl: CLOAK_PRODUCTION_RELAY_URL
|
|
22
43
|
```
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
It is still never defaulted: omitting the option throws before anything is signed or sent, so a
|
|
46
|
+
script that forgets it cannot ship its proof anywhere. `relayUrl: ""` means the opposite — no
|
|
47
|
+
endpoint at all, so a deposit is signed and submitted by the caller.
|
|
48
|
+
|
|
49
|
+
A build pinned somewhere else is made by editing source and rebuilding, which is also what makes a
|
|
50
|
+
published build unrepointable:
|
|
51
|
+
|
|
25
52
|
```bash
|
|
26
|
-
|
|
53
|
+
# in a checkout of this repo
|
|
54
|
+
$ $EDITOR src/config/relay.ts # replace the RELAY_ORIGIN_ALLOWLIST entry, do not append
|
|
55
|
+
$ npm run build # only needed by consumers that import dist/
|
|
27
56
|
```
|
|
28
57
|
|
|
29
|
-
|
|
58
|
+
The same edit unlocks the second half of the lock: a build pinned to a non-local origin refuses to
|
|
59
|
+
run against an RPC served by the local machine, and a build pinned to a local origin does not. One
|
|
60
|
+
constant, both behaviours.
|
|
30
61
|
|
|
31
|
-
|
|
62
|
+
Every flow needs it, deposits included: the SDK registers the wallet's viewing key through the relay
|
|
63
|
+
before its first transaction (see [Recovery and discovery](#recovery-and-discovery-viewing-key)),
|
|
64
|
+
deposits fetch the signed risk quote the program requires from `<relayUrl>/range-quote`, and sends,
|
|
65
|
+
withdrawals and swaps are submitted through it. `enforceViewingKeyRegistration: false` skips only the
|
|
66
|
+
registration step (some of the shipped examples set it on a local fork); it is not a no-relay mode.
|
|
32
67
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- `transact`, `partialWithdraw`, and `fullWithdraw` already include stale-root retry handling.
|
|
36
|
-
- For simple CLI sends, require only `SOLANA_RPC_URL` and `KEYPAIR_PATH`.
|
|
68
|
+
Do not expose `programId`, the relay URL or the circuits base as end-user input. Wallet adapters are
|
|
69
|
+
the in-app signer; keypair files belong to scripts only.
|
|
37
70
|
|
|
38
|
-
###
|
|
71
|
+
### Devnet and local
|
|
39
72
|
|
|
40
|
-
|
|
73
|
+
The same SDK runs against a devnet or local deployment by pointing the three coordinates at it:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
SOLANA_RPC_URL=https://api.devnet.solana.com # or http://127.0.0.1:8899 for a local fork
|
|
77
|
+
CLOAK_RELAY_URL=<endpoint serving that deployment> # read by examples/ and scripts/, NOT by the SDK
|
|
78
|
+
CLOAK_PROGRAM_ID=<program id of that deployment> # examples read this; pass it as programId in your code
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`CLOAK_RELAY_URL` configures the shipped examples and scripts, which pass its value as `relayUrl`.
|
|
82
|
+
The SDK itself does not read it: the endpoint a build may talk to comes from `src/config/relay.ts`,
|
|
83
|
+
so pointing at a devnet or local deployment means editing that file in a checkout and building it.
|
|
84
|
+
|
|
85
|
+
The shipped examples default to a local fork (`http://127.0.0.1:8899`) with a local relay
|
|
86
|
+
(`http://127.0.0.1:5500`); see [Examples](#examples).
|
|
87
|
+
|
|
88
|
+
## Quick start: private SOL send (single file, keypair)
|
|
89
|
+
|
|
90
|
+
Shield from the signer, then unshield to the recipient. Use this shape for one-shot scripts.
|
|
41
91
|
|
|
42
92
|
```ts
|
|
43
93
|
import { readFileSync } from "fs";
|
|
44
94
|
import {
|
|
45
95
|
CLOAK_PROGRAM_ID,
|
|
46
96
|
NATIVE_SOL_MINT,
|
|
47
|
-
|
|
97
|
+
createRecoverableDepositUtxo,
|
|
48
98
|
createZeroUtxo,
|
|
49
99
|
fullWithdraw,
|
|
50
100
|
generateUtxoKeypair,
|
|
101
|
+
getNkFromUtxoPrivateKey,
|
|
51
102
|
transact,
|
|
52
103
|
} from "@cloak.dev/sdk";
|
|
53
104
|
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
|
|
@@ -59,45 +110,54 @@ async function main() {
|
|
|
59
110
|
}
|
|
60
111
|
|
|
61
112
|
const rpcUrl = process.env.SOLANA_RPC_URL;
|
|
113
|
+
const relayUrl = process.env.CLOAK_RELAY_URL;
|
|
62
114
|
const keypairPath = process.env.KEYPAIR_PATH;
|
|
63
|
-
if (!rpcUrl || !keypairPath) {
|
|
64
|
-
throw new Error("Set SOLANA_RPC_URL and KEYPAIR_PATH");
|
|
115
|
+
if (!rpcUrl || !relayUrl || !keypairPath) {
|
|
116
|
+
throw new Error("Set SOLANA_RPC_URL, CLOAK_RELAY_URL and KEYPAIR_PATH");
|
|
65
117
|
}
|
|
66
118
|
|
|
67
119
|
const connection = new Connection(rpcUrl, "confirmed");
|
|
68
120
|
const signer = Keypair.fromSecretKey(
|
|
69
121
|
Uint8Array.from(JSON.parse(readFileSync(keypairPath, "utf8"))),
|
|
70
122
|
);
|
|
71
|
-
|
|
72
123
|
const recipient = new PublicKey(recipientArg);
|
|
73
|
-
const
|
|
124
|
+
const amount = BigInt(lamportsArg);
|
|
74
125
|
|
|
126
|
+
// The wallet's viewing base (nk). A real wallet derives it from its seed; a throwaway
|
|
127
|
+
// keypair keeps this script self-contained.
|
|
75
128
|
const owner = await generateUtxoKeypair();
|
|
76
|
-
const
|
|
129
|
+
const nk = getNkFromUtxoPrivateKey(owner.privateKey);
|
|
77
130
|
|
|
131
|
+
// Shield. The note's keypair and blinding are derived from (nk, noteSalt) and the salt is
|
|
132
|
+
// published inside the nk-encrypted chain note, so a scan holding only nk can rebuild it.
|
|
133
|
+
const { utxo, noteSalt } = await createRecoverableDepositUtxo(amount, nk, NATIVE_SOL_MINT);
|
|
78
134
|
const deposited = await transact(
|
|
79
135
|
{
|
|
80
136
|
inputUtxos: [await createZeroUtxo(NATIVE_SOL_MINT)],
|
|
81
|
-
outputUtxos: [
|
|
82
|
-
externalAmount:
|
|
137
|
+
outputUtxos: [utxo],
|
|
138
|
+
externalAmount: amount,
|
|
83
139
|
depositor: signer.publicKey,
|
|
84
140
|
},
|
|
85
141
|
{
|
|
86
142
|
connection,
|
|
87
143
|
programId: CLOAK_PROGRAM_ID,
|
|
144
|
+
relayUrl,
|
|
88
145
|
depositorKeypair: signer,
|
|
89
|
-
|
|
90
|
-
|
|
146
|
+
chainNoteViewingKeyNk: nk,
|
|
147
|
+
chainNoteSalt: noteSalt,
|
|
91
148
|
},
|
|
92
149
|
);
|
|
93
150
|
|
|
94
|
-
|
|
151
|
+
// Unshield to the recipient. outputUtxos[0] is the deposited note ([1] is the zero pad).
|
|
152
|
+
// The program takes its fee here (0.005 SOL + 0.3%).
|
|
153
|
+
const withdrawn = await fullWithdraw([deposited.outputUtxos[0]], recipient, {
|
|
95
154
|
connection,
|
|
96
155
|
programId: CLOAK_PROGRAM_ID,
|
|
156
|
+
relayUrl,
|
|
97
157
|
depositorKeypair: signer,
|
|
98
158
|
walletPublicKey: signer.publicKey,
|
|
159
|
+
chainNoteViewingKeyNk: nk,
|
|
99
160
|
cachedMerkleTree: deposited.merkleTree,
|
|
100
|
-
enforceViewingKeyRegistration: false,
|
|
101
161
|
});
|
|
102
162
|
|
|
103
163
|
console.log(withdrawn.signature);
|
|
@@ -112,245 +172,553 @@ main().catch((e) => {
|
|
|
112
172
|
Run:
|
|
113
173
|
|
|
114
174
|
```bash
|
|
115
|
-
SOLANA_RPC_URL="https
|
|
175
|
+
SOLANA_RPC_URL="https://<your mainnet rpc>" \
|
|
176
|
+
CLOAK_RELAY_URL="https://api.cloak.ag" \
|
|
116
177
|
KEYPAIR_PATH="/absolute/path/to/id.json" \
|
|
117
178
|
npx tsx send-sol-private.ts <recipientPubkey> <lamports>
|
|
118
179
|
```
|
|
119
180
|
|
|
120
|
-
|
|
181
|
+
Rules for minimal scripts:
|
|
121
182
|
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
-
|
|
183
|
+
- Take lamports from the CLI and keep all amount math in `bigint`; never parse SOL with float math.
|
|
184
|
+
- `<lamports>` must be at least `MIN_DEPOSIT_LAMPORTS` (0.01 SOL) and the withdrawal must exceed
|
|
185
|
+
its fee (see [Fees and limits](#fees-and-limits)).
|
|
186
|
+
- Use `KEYPAIR_PATH`; do not read raw private keys from the environment.
|
|
187
|
+
- Keep `programId` fixed to `CLOAK_PROGRAM_ID`.
|
|
188
|
+
- `transact`, `partialWithdraw` and `fullWithdraw` already retry on a stale Merkle root; do not
|
|
189
|
+
wrap them in your own retry loop.
|
|
126
190
|
|
|
127
|
-
###
|
|
191
|
+
### Browser (wallet adapter)
|
|
128
192
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
193
|
+
Replace `depositorKeypair` with the adapter's signers:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
await transact(params, {
|
|
197
|
+
connection,
|
|
198
|
+
programId: CLOAK_PROGRAM_ID,
|
|
199
|
+
relayUrl,
|
|
200
|
+
signTransaction: (tx) => wallet.signTransaction(tx),
|
|
201
|
+
signMessage: (message) => wallet.signMessage(message), // signs the viewing-key registration challenge
|
|
202
|
+
depositorPublicKey: wallet.publicKey,
|
|
203
|
+
walletPublicKey: wallet.publicKey,
|
|
204
|
+
chainNoteViewingKeyNk: nk,
|
|
205
|
+
chainNoteSalt: noteSalt,
|
|
206
|
+
});
|
|
139
207
|
```
|
|
140
208
|
|
|
141
|
-
`
|
|
209
|
+
`signTransaction` must accept both a legacy `Transaction` and a `VersionedTransaction` (v0 with
|
|
210
|
+
address lookup tables); deposits use either depending on the path taken. `signMessage` is required
|
|
211
|
+
unless `enforceViewingKeyRegistration: false` is set; without it the call fails with
|
|
212
|
+
"Viewing key registration is mandatory: signMessage (wallet) or depositorKeypair is required."
|
|
142
213
|
|
|
143
|
-
|
|
214
|
+
A deposit is signed by the wallet and goes straight to chain. A private send, a withdrawal and a
|
|
215
|
+
swap are submitted for you, and each one carries an authenticated sender — so those three need
|
|
216
|
+
`signMessage` and `walletPublicKey` even when `enforceViewingKeyRegistration` is off:
|
|
144
217
|
|
|
145
|
-
|
|
218
|
+
```ts
|
|
219
|
+
// Private send (externalAmount 0) from a wallet adapter. Same for fullWithdraw / partialWithdraw
|
|
220
|
+
// (externalAmount < 0) and swapUtxo / swapWithChange.
|
|
221
|
+
await transact(
|
|
222
|
+
{ inputUtxos, outputUtxos, externalAmount: BigInt(0) },
|
|
223
|
+
{
|
|
224
|
+
connection,
|
|
225
|
+
programId: CLOAK_PROGRAM_ID,
|
|
226
|
+
relayUrl,
|
|
227
|
+
signMessage: (message) => wallet.signMessage(message),
|
|
228
|
+
walletPublicKey: wallet.publicKey, // becomes the authenticated sender
|
|
229
|
+
chainNoteViewingKeyNk: nk,
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
```
|
|
146
233
|
|
|
147
|
-
`
|
|
234
|
+
`signMessage` must return the raw 64-byte ed25519 detached signature over the bytes it is given;
|
|
235
|
+
adapters that re-encode or wrap the result are rejected by name. `walletPublicKey` must be the end
|
|
236
|
+
user's own wallet — it is the identity the transaction is authenticated as. Passing neither a
|
|
237
|
+
`depositorKeypair` nor both of these fails immediately, before any proof is generated.
|
|
148
238
|
|
|
149
|
-
`
|
|
239
|
+
`depositorKeypair`, `walletPublicKey` and `depositorPublicKey` are three names for the SAME end
|
|
240
|
+
user. That one key is the request's authenticated sender, the key screened for sanctions, and the
|
|
241
|
+
wallet a viewing key is registered against, so setting two of the names to two different keys would
|
|
242
|
+
screen one person and authenticate another. Passing two different wallets is refused at the call.
|
|
150
243
|
|
|
151
|
-
|
|
244
|
+
### The approval window
|
|
152
245
|
|
|
153
|
-
|
|
246
|
+
The request's timestamp is stamped when the bytes are built, because it is part of what the wallet
|
|
247
|
+
signs, and the relay accepts a first-use request for 300 seconds from that moment. An approval left
|
|
248
|
+
sitting in a hardware wallet past that window cannot be rescued by re-stamping it, so the SDK stops
|
|
249
|
+
there and says so instead of shipping a request that can only come back as a 401. Retry the
|
|
250
|
+
operation and approve the prompt when it appears.
|
|
154
251
|
|
|
155
|
-
|
|
252
|
+
`auth_issued_at` comes from the machine's own clock, and the relay rejects anything more than 30
|
|
253
|
+
seconds ahead of its own. A user whose clock is fast cannot authenticate at all until it is
|
|
254
|
+
corrected; `explainRelayAuthRejection` turns that rejection, and the rest of the relay's 401s, into
|
|
255
|
+
a sentence that names the cause. The SDK applies it to its own relay errors already.
|
|
156
256
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
257
|
+
A private send or withdrawal signs exactly ONCE: every network retry re-POSTs the same signed
|
|
258
|
+
bytes, so the user sees one prompt. A swap re-proves on every retry, so each retry needs its own
|
|
259
|
+
approval; `maxWalletApprovals` (default 5) bounds how many times one swap may interrupt the user,
|
|
260
|
+
independently of `maxRootRetries`. A `depositorKeypair` signs silently and is unaffected.
|
|
160
261
|
|
|
161
|
-
|
|
162
|
-
const connection = new Connection("https://api.devnet.solana.com");
|
|
163
|
-
const keypair = Keypair.fromSecretKey(/* your secret key */);
|
|
262
|
+
### Authenticating a call you build yourself
|
|
164
263
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
keypairBytes: keypair.secretKey,
|
|
168
|
-
network: "devnet",
|
|
169
|
-
});
|
|
264
|
+
To authenticate a call you build yourself (a swap retry that posts only `retry_request_id`, for
|
|
265
|
+
example), use the exported primitives rather than re-deriving the scheme:
|
|
170
266
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.log("Deposited! Leaf index:", depositResult.leafIndex);
|
|
267
|
+
```ts
|
|
268
|
+
import { buildRelayAuthPreimage, TRANSACT_SWAP_AUTH_FIELDS } from "@cloak.dev/sdk";
|
|
174
269
|
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
270
|
+
const body = { retry_request_id: requestId, slippage_bps: 500 };
|
|
271
|
+
const preimage = buildRelayAuthPreimage(
|
|
272
|
+
"/transact_swap",
|
|
273
|
+
CLOAK_PROGRAM_ID,
|
|
274
|
+
body,
|
|
275
|
+
wallet.publicKey,
|
|
276
|
+
undefined,
|
|
277
|
+
TRANSACT_SWAP_AUTH_FIELDS,
|
|
181
278
|
);
|
|
182
|
-
|
|
279
|
+
const signature = await wallet.signMessage(preimage.message);
|
|
280
|
+
|
|
281
|
+
const payload = {
|
|
282
|
+
...body,
|
|
283
|
+
sender: preimage.sender,
|
|
284
|
+
auth_issued_at: preimage.auth_issued_at,
|
|
285
|
+
auth_nonce: preimage.auth_nonce,
|
|
286
|
+
auth_signature: Buffer.from(signature).toString("base64"),
|
|
287
|
+
};
|
|
183
288
|
```
|
|
184
289
|
|
|
185
|
-
|
|
290
|
+
Reuse one `preimage` for every retry of the same logical request; rebuilding it produces a new
|
|
291
|
+
nonce and a new request.
|
|
292
|
+
|
|
293
|
+
`slippage_bps` in that snippet is not decoration. Every other field in both lists is optional on
|
|
294
|
+
the relay side, so omitting one and signing it as `null` matches what the relay signs. `slippage_bps`
|
|
295
|
+
is the exception: it is not optional, and a request that omits it is read by the relay as **500**.
|
|
296
|
+
Omit it and you sign `null` while the relay signs `500`, the digests differ, and the answer is a 401
|
|
297
|
+
that points at nothing. Set it explicitly, to the same value the body carries. The SDK's own swap
|
|
298
|
+
path always does; a hand-built body that leaves it out is refused before it is signed.
|
|
299
|
+
|
|
300
|
+
`canonicalJson` is exported for the same reason, and it is the SDK's half of a byte-for-byte
|
|
301
|
+
agreement with one Rust function over one fixed schema: ASCII keys over strings, small unsigned
|
|
302
|
+
integers, booleans, nulls, arrays and plain objects. Amounts travel as decimal strings, not numbers,
|
|
303
|
+
because JavaScript and `serde_json` do not spell every number the same way. Anything outside that
|
|
304
|
+
schema that could serialize differently on the two sides is refused rather than silently signed.
|
|
305
|
+
|
|
306
|
+
## The UTXO model
|
|
307
|
+
|
|
308
|
+
- A shielded balance is a set of notes (`Utxo`): `{ amount, keypair, blinding, mintAddress, index?, commitment? }`.
|
|
309
|
+
Amounts are `bigint` in base units (lamports, or 10^-6 for USDC/USDT).
|
|
310
|
+
- Every transaction is 2-in / 2-out; `transact` pads unused slots with zero notes for you.
|
|
311
|
+
`createZeroUtxo(mint)` is exported if you want to pass them explicitly.
|
|
312
|
+
- `externalAmount > 0` is a deposit (funds enter from `depositor`), `< 0` is a withdrawal to
|
|
313
|
+
`recipient`, `0` is a private send inside the pool.
|
|
314
|
+
- Pools are per mint. All inputs and outputs of one transaction live in the same pool;
|
|
315
|
+
`NATIVE_SOL_MINT` is the SOL pool.
|
|
316
|
+
- `TransactResult.outputUtxos` are the notes you can spend next (leaf `index` set). Persist them
|
|
317
|
+
with `serializeUtxo` / `deserializeUtxo`. Pass `result.merkleTree` as `cachedMerkleTree` to the
|
|
318
|
+
next call to skip re-fetching commitments.
|
|
319
|
+
- The Merkle tree is read at `confirmed`, so a note is spendable as soon as its deposit is confirmed.
|
|
320
|
+
|
|
321
|
+
Entry points: `transact` (general), `transfer`, `partialWithdraw`, `fullWithdraw`,
|
|
322
|
+
`swapUtxo` / `swapWithChange`.
|
|
323
|
+
|
|
324
|
+
### Private send (shield-to-shield)
|
|
186
325
|
|
|
187
|
-
```
|
|
188
|
-
import {
|
|
189
|
-
import { useWallet, useConnection } from "@solana/wallet-adapter-react";
|
|
190
|
-
import { PublicKey } from "@solana/web3.js";
|
|
326
|
+
```ts
|
|
327
|
+
import { deriveViewingKeyFromNk, transfer } from "@cloak.dev/sdk";
|
|
191
328
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
console.log("Deposited!", result.signature);
|
|
212
|
-
};
|
|
329
|
+
const result = await transfer(
|
|
330
|
+
[myNote], // inputs to spend (same pool)
|
|
331
|
+
recipientUtxoPublicKey, // bigint: the recipient's UTXO public key
|
|
332
|
+
amount,
|
|
333
|
+
{
|
|
334
|
+
connection,
|
|
335
|
+
programId: CLOAK_PROGRAM_ID,
|
|
336
|
+
relayUrl,
|
|
337
|
+
depositorKeypair: signer,
|
|
338
|
+
walletPublicKey: signer.publicKey,
|
|
339
|
+
chainNoteViewingKeyNk: myNk,
|
|
340
|
+
// The recipient's X25519 viewing public key. With it the recipient can discover the note
|
|
341
|
+
// from chain with their own key (scanRecipientDeliveryNotes). Without it the note is still
|
|
342
|
+
// valid but must be handed over out of band.
|
|
343
|
+
recipientViewingPublicKey: deriveViewingKeyFromNk(recipientNk).publicKey,
|
|
344
|
+
cachedMerkleTree: previous.merkleTree,
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
```
|
|
213
348
|
|
|
214
|
-
|
|
215
|
-
|
|
349
|
+
`transfer` builds the recipient note as output 0 and returns the change (owned by the input's
|
|
350
|
+
keypair) as output 1. Calling `transact` directly with `externalAmount: 0n` does the same; keep the
|
|
351
|
+
recipient note at `outputUtxos[0]`, which is the one the delivery carrier is built for.
|
|
352
|
+
The recipient shares two values with the sender: their UTXO public key (`UtxoKeypair.publicKey`)
|
|
353
|
+
and their viewing public key (`deriveViewingKeyFromNk(nk).publicKey`).
|
|
354
|
+
|
|
355
|
+
### Withdraw (unshield)
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
await fullWithdraw([note], recipientWallet, options); // whole note
|
|
359
|
+
await partialWithdraw([note], recipientWallet, amount, options); // amount out, change stays shielded
|
|
216
360
|
```
|
|
217
361
|
|
|
218
|
-
|
|
362
|
+
For SPL pools the recipient receives tokens in their associated token account. The program charges
|
|
363
|
+
its fee on the withdrawn amount ([Fees and limits](#fees-and-limits)).
|
|
219
364
|
|
|
220
|
-
###
|
|
365
|
+
### SPL pools (USDC, USDT)
|
|
221
366
|
|
|
222
|
-
|
|
367
|
+
Same API with the pool mint on every note:
|
|
223
368
|
|
|
224
|
-
```
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
369
|
+
```ts
|
|
370
|
+
const usdc = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
|
|
371
|
+
const { utxo, noteSalt } = await createRecoverableDepositUtxo(2_000_000n, nk, usdc); // 2.00 USDC
|
|
372
|
+
await transact(
|
|
373
|
+
{ inputUtxos: [await createZeroUtxo(usdc)], outputUtxos: [utxo], externalAmount: 2_000_000n, depositor: signer.publicKey },
|
|
374
|
+
{ connection, programId: CLOAK_PROGRAM_ID, relayUrl, depositorKeypair: signer, chainNoteViewingKeyNk: nk, chainNoteSalt: noteSalt },
|
|
375
|
+
);
|
|
228
376
|
```
|
|
229
377
|
|
|
230
|
-
|
|
378
|
+
Minimum SPL deposit is 1.00 token. SOL deposits fit in a single v0 packet with the production
|
|
379
|
+
lookup table; SPL deposits currently still create a small supplemental lookup table
|
|
380
|
+
(about 0.0056 SOL rent, reclaimable by the depositor).
|
|
231
381
|
|
|
232
|
-
|
|
382
|
+
### Shielded swap (SOL to USDC/USDT)
|
|
233
383
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
384
|
+
Spends shielded SOL and delivers the output token to a public associated token account, routed
|
|
385
|
+
through Jupiter. Swap input is SOL only (the pool is wSOL-locked).
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { swapWithChange } from "@cloak.dev/sdk";
|
|
389
|
+
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
|
|
390
|
+
|
|
391
|
+
const recipientAta = getAssociatedTokenAddressSync(usdc, recipientWallet);
|
|
392
|
+
|
|
393
|
+
const swap = await swapWithChange(
|
|
394
|
+
[solNote],
|
|
395
|
+
swapAmount, // lamports leaving the pool; the program's fee is taken from this
|
|
396
|
+
usdc,
|
|
397
|
+
recipientAta,
|
|
398
|
+
minOutputAmount, // proof-bound floor; quote the fee-adjusted amount and use the quote's threshold
|
|
399
|
+
{
|
|
400
|
+
connection,
|
|
401
|
+
programId: CLOAK_PROGRAM_ID,
|
|
402
|
+
relayUrl,
|
|
403
|
+
depositorKeypair: signer,
|
|
404
|
+
walletPublicKey: signer.publicKey,
|
|
405
|
+
chainNoteViewingKeyNk: nk,
|
|
406
|
+
swapSlippageBps: 100,
|
|
407
|
+
},
|
|
408
|
+
recipientWallet, // required on mainnet: the wallet the swap's risk quote is issued for
|
|
240
409
|
);
|
|
410
|
+
|
|
411
|
+
swap.signature; // the executed swap; swapWithChange resolves only once execution has completed
|
|
412
|
+
swap.refund; // persist: the refund-fallback secret (see below)
|
|
241
413
|
```
|
|
242
414
|
|
|
243
|
-
|
|
415
|
+
`swapUtxo` is the lower-level form taking `UtxoSwapParams` and an explicit change note. Quote the
|
|
416
|
+
amount after the protocol fee (`swapAmount - calculateFeeBigint(swapAmount)` for SOL) so
|
|
417
|
+
`minOutputAmount` is achievable. `outputMint`, `recipientAta` and `minOutputAmount` are bound into
|
|
418
|
+
the proof and cannot be changed after it is generated.
|
|
419
|
+
|
|
420
|
+
#### Timed-out swaps
|
|
244
421
|
|
|
245
|
-
|
|
422
|
+
`swapWithChange` polls the swap status and throws if the swap is refunded, cancelled or fails, or if
|
|
423
|
+
polling runs out (`swapStatusMaxAttempts` x `swapStatusDelayMs`, default 60 x 2 s); the result above
|
|
424
|
+
exists only for a completed swap. On a timeout the program returns the principal, net of the swap
|
|
425
|
+
fee, to the SOL pool as a refund note, which is spent through the normal withdraw path. When the
|
|
426
|
+
swap was submitted with `chainNoteViewingKeyNk` (`swap.refund.derivedFromNk === true`) that note is
|
|
427
|
+
discoverable from the key alone:
|
|
246
428
|
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
429
|
+
```ts
|
|
430
|
+
import { discoverSwapRefunds, fullWithdraw, NATIVE_SOL_MINT, type Utxo } from "@cloak.dev/sdk";
|
|
431
|
+
|
|
432
|
+
const refunds = await discoverSwapRefunds(connection, CLOAK_PROGRAM_ID, nk); // DiscoveredSwapRefund[]
|
|
433
|
+
for (const refund of refunds) {
|
|
434
|
+
const note: Utxo = {
|
|
435
|
+
amount: refund.amount, // principal net of the swap fee
|
|
436
|
+
keypair: refund.keypair,
|
|
437
|
+
blinding: refund.blinding,
|
|
438
|
+
mintAddress: NATIVE_SOL_MINT,
|
|
439
|
+
commitment: refund.commitment,
|
|
440
|
+
index: Number(refund.leafIndex),
|
|
441
|
+
};
|
|
442
|
+
await fullWithdraw([note], recipientWallet, options);
|
|
443
|
+
}
|
|
252
444
|
```
|
|
253
445
|
|
|
254
|
-
|
|
446
|
+
Swaps that executed leave no refund note, and an empty result only says that none of the scanned
|
|
447
|
+
leaves derive from this `nk`. A swap submitted without `chainNoteViewingKeyNk` has `swap.refund`
|
|
448
|
+
(hex `privateKey`, `publicKey`, `blinding`) as the only copy of its secret; rebuild the note from it
|
|
449
|
+
with the on-chain `SwapState.sol_amount` as the amount and
|
|
450
|
+
`computeSwapRefundCommitment(amount, publicKey, blinding)` as the commitment, then look up the leaf
|
|
451
|
+
index as in [Recovery and discovery](#recovery-and-discovery-viewing-key).
|
|
452
|
+
|
|
453
|
+
## Recovery and discovery (viewing key)
|
|
454
|
+
|
|
455
|
+
The viewing base `nk` is derived from the wallet's spend key (`getNkFromUtxoPrivateKey(privateKey)`
|
|
456
|
+
or `expandSpendKey(skSpend).nsk`). It decrypts the chain notes the SDK attaches to transactions and
|
|
457
|
+
opens notes delivered by other people's private sends. Recoverable deposit notes and swap refund
|
|
458
|
+
notes also derive their secrets from it, which is what makes key-only recovery possible, so store
|
|
459
|
+
it with the same care as the spend key.
|
|
255
460
|
|
|
256
|
-
|
|
461
|
+
Before a wallet's first transaction the SDK registers `nk` with the relay for compliance scanning:
|
|
462
|
+
it signs a one-time challenge with `signMessage` (wallet) or `depositorKeypair` and submits `nk`
|
|
463
|
+
with that signature. Nothing viewing-key related is written on-chain. The step is on by default;
|
|
464
|
+
`enforceViewingKeyRegistration: false` disables it. `registerViewingKey(relayUrl, userPubkey, nk,
|
|
465
|
+
signMessage)` performs the same registration on its own.
|
|
257
466
|
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
467
|
+
```ts
|
|
468
|
+
import { scanTransactions, fetchCommitments, type Utxo } from "@cloak.dev/sdk";
|
|
469
|
+
|
|
470
|
+
const scan = await scanTransactions({
|
|
471
|
+
connection,
|
|
472
|
+
programId: CLOAK_PROGRAM_ID,
|
|
473
|
+
viewingKeyNk: nk,
|
|
474
|
+
ownerUtxoPublicKey: owner.publicKey, // authenticates delivered notes' commitments
|
|
475
|
+
deliveryMints: [NATIVE_SOL_MINT],
|
|
476
|
+
untilSignature: lastScan?.lastSignature, // incremental scans
|
|
262
477
|
});
|
|
478
|
+
|
|
479
|
+
scan.recoveredDepositNotes; // own deposits built with createRecoverableDepositUtxo, in spendable form
|
|
480
|
+
scan.deliveredNotes; // notes sent to this wallet (amount + blinding; the keypair is your own)
|
|
481
|
+
scan.transactions; // history rows for compliance reporting
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
`scanRecipientDeliveryNotes` does the delivery sweep alone, returning `{ notes, rpcCalls }`
|
|
485
|
+
instead of a full scan result. Deposit recovery applies to deposits built with
|
|
486
|
+
`createRecoverableDepositUtxo` (the default in the examples); a deposit built with
|
|
487
|
+
`createUtxo` has a random blinding that is written nowhere and appears as history only.
|
|
488
|
+
|
|
489
|
+
A recovered or delivered note needs its leaf index before it can be spent. Look it up by commitment.
|
|
490
|
+
The two record types differ: `RecoveredDepositNoteRecord` carries the note's own `keypair`, a
|
|
491
|
+
`bigint` `commitment` and a `PublicKey` `mintAddress`; `DeliveredNote` carries a hex `commitment`,
|
|
492
|
+
a base58 `mint` (set only when the scan was given `ownerUtxoPublicKey`) and no keypair, because the
|
|
493
|
+
keypair is yours.
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
import { PublicKey } from "@solana/web3.js";
|
|
497
|
+
|
|
498
|
+
const entries = await fetchCommitments(relayUrl, { mint: NATIVE_SOL_MINT });
|
|
499
|
+
const indexOf = (commitment: bigint) =>
|
|
500
|
+
entries.find((e) => BigInt("0x" + e.commitment.replace(/^0x/, "")) === commitment)?.index;
|
|
501
|
+
|
|
502
|
+
// Own deposit recovered from nk
|
|
503
|
+
const recovered = scan.recoveredDepositNotes[0];
|
|
504
|
+
const ownNote: Utxo = {
|
|
505
|
+
amount: recovered.amount,
|
|
506
|
+
keypair: recovered.keypair,
|
|
507
|
+
blinding: recovered.blinding,
|
|
508
|
+
mintAddress: recovered.mintAddress,
|
|
509
|
+
commitment: recovered.commitment,
|
|
510
|
+
index: indexOf(recovered.commitment),
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Note delivered by someone else's private send
|
|
514
|
+
const delivered = scan.deliveredNotes[0];
|
|
515
|
+
if (!delivered.commitmentVerified || !delivered.mint) {
|
|
516
|
+
throw new Error("scan with ownerUtxoPublicKey to authenticate delivered notes");
|
|
517
|
+
}
|
|
518
|
+
const deliveredCommitment = BigInt("0x" + delivered.commitment);
|
|
519
|
+
const deliveredNote: Utxo = {
|
|
520
|
+
amount: delivered.amount,
|
|
521
|
+
keypair: owner, // the UtxoKeypair whose publicKey the sender used
|
|
522
|
+
blinding: delivered.blinding,
|
|
523
|
+
mintAddress: new PublicKey(delivered.mint),
|
|
524
|
+
commitment: deliveredCommitment,
|
|
525
|
+
index: indexOf(deliveredCommitment),
|
|
526
|
+
};
|
|
263
527
|
```
|
|
264
528
|
|
|
265
|
-
|
|
529
|
+
`discoverSwapRefunds(connection, programId, nk)` returns `DiscoveredSwapRefund` records (`keypair`,
|
|
530
|
+
`blinding`, `amount`, `commitment`, `leafIndex: bigint`); build the `Utxo` from one as shown under
|
|
531
|
+
[Timed-out swaps](#timed-out-swaps).
|
|
266
532
|
|
|
267
|
-
|
|
268
|
-
- **Variable Fee**: 0.3% of deposit amount
|
|
533
|
+
`toComplianceReport(scan)` and `formatComplianceCsv(report)` turn a scan into a report.
|
|
269
534
|
|
|
270
|
-
|
|
535
|
+
## Nullifiers and spent checks
|
|
271
536
|
|
|
272
|
-
|
|
273
|
-
|
|
537
|
+
The nullifier export is `computeUtxoNullifier(utxo)`, the circuit's scheme:
|
|
538
|
+
`Poseidon(commitment, index, signature)`. This is the value the program records when a note is
|
|
539
|
+
spent, so it is what a spent check compares against. It is async and requires `utxo.index` to be
|
|
540
|
+
set.
|
|
274
541
|
|
|
275
|
-
|
|
276
|
-
|
|
542
|
+
Prefer the helpers, which derive the nullifier and check the on-chain PDA for you:
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
import { verifyUtxos, preflightNullifiers } from "@cloak.dev/sdk";
|
|
546
|
+
|
|
547
|
+
const { spent } = await verifyUtxos(notes, connection, CLOAK_PROGRAM_ID);
|
|
548
|
+
await preflightNullifiers(notes, connection, CLOAK_PROGRAM_ID); // throws UtxoAlreadySpentError
|
|
277
549
|
```
|
|
278
550
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
551
|
+
`transact` runs the preflight itself before generating a proof.
|
|
552
|
+
|
|
553
|
+
## Fees and limits
|
|
554
|
+
|
|
555
|
+
Fees are collected on-chain by the program from each pool's `PoolConfig` account
|
|
556
|
+
(`["pool_config", mint]`). They apply to withdrawals and swaps, on the amount leaving the pool.
|
|
557
|
+
Deposits and private sends carry no protocol fee.
|
|
558
|
+
|
|
559
|
+
| Pool | Fixed fee | Rate | Minimum deposit |
|
|
560
|
+
| --- | --- | --- | --- |
|
|
561
|
+
| SOL | 0.005 SOL | 0.3% | 0.01 SOL |
|
|
562
|
+
| USDC | 0.45 USDC | 0.3% | 1.00 USDC |
|
|
563
|
+
| USDT | 0.45 USDT | 0.3% | 1.00 USDT |
|
|
564
|
+
|
|
565
|
+
A withdrawal or swap must exceed its fee or the program rejects it; deposits below the minimum are
|
|
566
|
+
rejected with `DepositTooSmall`.
|
|
567
|
+
|
|
568
|
+
`shared/fees.ts` mirrors the SOL pool for estimates in the UI:
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
import { calculateFeeBigint, getDistributableAmount, isWithdrawAmountSufficient } from "@cloak.dev/sdk";
|
|
572
|
+
|
|
573
|
+
calculateFeeBigint(1_000_000_000n); // 8_000_000n (0.005 SOL + 0.3% of 1 SOL)
|
|
574
|
+
getDistributableAmount(100_000_000); // 94_700_000 (0.1 SOL withdrawal, net to recipient)
|
|
575
|
+
isWithdrawAmountSufficient(6_000_000n); // true: 0.006 SOL covers the 0.005018 SOL fee
|
|
295
576
|
```
|
|
296
577
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
578
|
+
For USDC/USDT compute `450_000n + amount * 3n / 1000n`. Treat these as estimates: the deployed
|
|
579
|
+
`PoolConfig` is the source of truth. Solana transaction fees and, for SPL deposits, the supplemental
|
|
580
|
+
lookup-table rent are separate.
|
|
581
|
+
|
|
582
|
+
## Circuit artifacts
|
|
583
|
+
|
|
584
|
+
Proving uses the `transaction` circuit from bundle `cloak-transaction-0.2.0` (multi-party ceremony,
|
|
585
|
+
6 contributors plus a public final beacon; 42,672 constraints, 9 public inputs). The verifying key is
|
|
586
|
+
embedded in the program, so the SDK never fetches it; for the record, the exported
|
|
587
|
+
`transaction.vkey.json` from the same ceremony is SHA-256
|
|
588
|
+
`deb40e7b94eae17db2975d23dcf26c26db2a36a4f02d14a25830dee3e88fb93c`.
|
|
589
|
+
|
|
590
|
+
| Artifact | SHA-256 |
|
|
591
|
+
| --- | --- |
|
|
592
|
+
| `transaction_js/transaction.wasm` | `02ec02e954ae3932827ad9de51afa597ca95569aa97fec8410879c937a58aa2b` |
|
|
593
|
+
| `transaction_final.zkey` | `9da7db8cb1370fc497d36a0365f1f107ab0b0c13ca66fa9f0287e5f96ee68d25` |
|
|
594
|
+
|
|
595
|
+
The SDK downloads both artifacts once per process from
|
|
596
|
+
`DEFAULT_TRANSACTION_CIRCUITS_URL` (`https://storage.googleapis.com/cloak-circuits/circuits/0.2.0`),
|
|
597
|
+
hashes them, and refuses to prove if any digest differs from the pinned values. No configuration is
|
|
598
|
+
needed for mainnet.
|
|
599
|
+
|
|
600
|
+
To load the artifacts from a local directory instead (an air-gapped machine, a React Native bundle,
|
|
601
|
+
or the ceremony output — the directory must contain `transaction_js/transaction.wasm` and
|
|
602
|
+
`transaction_final.zkey` with the same digests):
|
|
603
|
+
|
|
604
|
+
```ts
|
|
605
|
+
import { resolveCircuitsBase, setCircuitsPath } from "@cloak.dev/sdk";
|
|
606
|
+
|
|
607
|
+
// explicit argument, else the pinned default; no environment variable is consulted
|
|
608
|
+
setCircuitsPath(resolveCircuitsBase());
|
|
609
|
+
setCircuitsPath("/opt/cloak/ceremony-0.2.0"); // or a local directory you control
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
Never write the artifact URL out by hand: the version segment and the digests are declared together
|
|
613
|
+
in `src/proving/circuits.ts` so they cannot drift.
|
|
614
|
+
|
|
615
|
+
Where the artifacts may be read from is fixed when the SDK is built. `transaction.wasm` is the
|
|
616
|
+
witness generator, so it is handed your spend key, your note secrets, the amounts and the recipient
|
|
617
|
+
— a mirror on another host is therefore not accepted, and the SHA-256 check over the bytes cannot be
|
|
618
|
+
switched off by any option or environment variable. A local directory is accepted (nothing leaves
|
|
619
|
+
the machine, and the digest check still decides whether the bytes are usable); an `http(s)` base
|
|
620
|
+
must be the bundle base this build pins. To point a build somewhere else, edit
|
|
621
|
+
`RELAY_ORIGIN_ALLOWLIST` in `src/config/relay.ts` and rebuild — the one edit that also unlocks the
|
|
622
|
+
relay endpoint and the localhost-RPC check.
|
|
623
|
+
|
|
624
|
+
## Version and compatibility
|
|
625
|
+
|
|
626
|
+
- SDK 0.2.0 pairs with the mainnet program at `zh1eLd6rSphLejbFfJEneUwzHRfMKxgzrgkfwA6qRkW`
|
|
627
|
+
(deployed 2026-08-24) and the 0.2.0 circuit bundle. All flows in this README were exercised on
|
|
628
|
+
mainnet on 2026-08-25: deposit, private send and withdraw on SOL, USDC and USDT; swaps SOL to
|
|
629
|
+
USDC and USDT; viewing-key discovery followed by a spend; keypair-only deposit recovery.
|
|
630
|
+
- Proofs generated with the 0.1.x circuits are rejected by the mainnet program. SDK 0.1.x builds
|
|
631
|
+
(which ship the 0.1.0 bundle) are not usable against it; upgrade to 0.2.0.
|
|
632
|
+
- The note-based `CloakSDK` class is removed in 0.2.0. It targeted the pre-0.2.0 instruction layout
|
|
633
|
+
and the unpublished 0.1.0 `withdraw_*` circuits, so it could not work against the current program.
|
|
634
|
+
Use the UTXO API above: `transact`, `transfer`, `partialWithdraw`, `fullWithdraw`, `swapUtxo`.
|
|
635
|
+
|
|
636
|
+
## Examples
|
|
637
|
+
|
|
638
|
+
```bash
|
|
639
|
+
# SOL pool
|
|
640
|
+
npm run example:deposit # move lamports into the shielded SOL pool
|
|
641
|
+
npm run example:private-send # shield-to-shield send the recipient can discover
|
|
642
|
+
npm run example:withdraw # partial withdraw, then full withdraw of the change
|
|
643
|
+
npm run example:swap # SOL to USDC through Jupiter
|
|
644
|
+
|
|
645
|
+
# SPL pools (USDC, USDT). There is no SPL swap: swap input is wSOL-locked.
|
|
646
|
+
npm run example:spl-deposit
|
|
647
|
+
npm run example:spl-private-send
|
|
648
|
+
npm run example:spl-withdraw
|
|
649
|
+
|
|
650
|
+
# Not pool specific
|
|
651
|
+
npm run example:viewing-keys # derive a viewing key, register it, read your history
|
|
652
|
+
|
|
653
|
+
# Swap variants
|
|
654
|
+
npm run example:swap-usdc # same swap, with the canonical swap markers
|
|
655
|
+
npm run example:swap-brz # BRZ with automatic USDC fallback
|
|
656
|
+
|
|
657
|
+
npm run test:examples # dry-run all eight (no wallet, no RPC)
|
|
332
658
|
```
|
|
333
659
|
|
|
334
|
-
|
|
660
|
+
Examples log by default. Set `CLOAK_DEBUG=0` to silence them.
|
|
661
|
+
|
|
662
|
+
Environment read by the examples:
|
|
663
|
+
|
|
664
|
+
| Variable | Default |
|
|
665
|
+
| --- | --- |
|
|
666
|
+
| `SOLANA_RPC_URL` | `http://127.0.0.1:8899` (local fork) |
|
|
667
|
+
| `CLOAK_RELAY_URL` | `http://127.0.0.1:5500` (local endpoint) — read by the examples, not by the SDK; it must name an origin the SDK build is pinned to |
|
|
668
|
+
| `CLOAK_PROGRAM_ID` | `CLOAK_PROGRAM_ID` |
|
|
669
|
+
| `CLOAK_ALT_ADDRESSES` | unset; the production lookup tables are resolved automatically |
|
|
335
670
|
|
|
336
|
-
|
|
337
|
-
|
|
671
|
+
The examples fund throwaway keypairs from `~/.config/solana/id.json` and print stable
|
|
672
|
+
`FULL_SIG|<example>|<step>|<signature>` markers. They import `@cloak.dev/sdk`, the published
|
|
673
|
+
package name; inside this repo `tsconfig.json` maps that specifier to `src/index.ts` so the
|
|
674
|
+
examples run against source.
|
|
675
|
+
|
|
676
|
+
## Error handling
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
import {
|
|
680
|
+
CloakError,
|
|
681
|
+
UtxoAlreadySpentError,
|
|
682
|
+
SanctionsQuoteError,
|
|
683
|
+
RelayInternalError,
|
|
684
|
+
SettlementVerificationError,
|
|
685
|
+
} from "@cloak.dev/sdk";
|
|
338
686
|
|
|
339
687
|
try {
|
|
340
|
-
await
|
|
688
|
+
await fullWithdraw([note], recipient, options);
|
|
341
689
|
} catch (error) {
|
|
342
|
-
if (error instanceof
|
|
343
|
-
|
|
344
|
-
|
|
690
|
+
if (error instanceof UtxoAlreadySpentError) {
|
|
691
|
+
// the note was spent elsewhere; rescan
|
|
692
|
+
} else if (error instanceof SettlementVerificationError) {
|
|
693
|
+
// error.outcome tells whether it is safe to retry; error.signature is what to look up
|
|
694
|
+
} else if (error instanceof CloakError) {
|
|
695
|
+
console.log(error.category, error.retryable);
|
|
345
696
|
}
|
|
346
697
|
}
|
|
347
698
|
```
|
|
348
699
|
|
|
700
|
+
Catch the typed errors instead of matching message strings. `confirmTransactSettlement` re-checks a
|
|
701
|
+
signature from chain state when an outcome was reported as unknown.
|
|
702
|
+
|
|
703
|
+
## Development
|
|
704
|
+
|
|
705
|
+
```bash
|
|
706
|
+
npm install
|
|
707
|
+
npm run lint # tsc --noEmit
|
|
708
|
+
npm test # jest
|
|
709
|
+
npm run build # tsup -> dist/
|
|
710
|
+
npm run verify:dist # dist is byte-identical to a fresh build
|
|
711
|
+
npm run test:examples # dry-run the examples
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Notes: `docs/DEPOSIT-SIZE-NOTES.md` (deposit transaction size, confirmed-tree reads,
|
|
715
|
+
supplemental lookup tables), `docs/transact-split.md` (how `flows/transact.ts` is split),
|
|
716
|
+
`docs/cleanup-plan.md` (the root/scripts/examples cleanup this repo is executing).
|
|
717
|
+
|
|
349
718
|
## Links
|
|
350
719
|
|
|
351
720
|
- Website: [https://cloak.ag](https://cloak.ag)
|
|
352
721
|
- Documentation: [https://docs.cloak.ag](https://docs.cloak.ag)
|
|
353
|
-
- GitHub: [https://github.com/cloak-ag/sdk](https://github.com/cloak-ag/sdk)
|
|
354
722
|
|
|
355
723
|
## License
|
|
356
724
|
|