import { readFileSync, writeFileSync } from "node:fs";
import {
CLOAK_PRODUCTION_RELAY_URL,
CLOAK_PROGRAM_ID,
NATIVE_SOL_MINT,
createCloakRpc,
createUtxo,
createZeroUtxo,
fullWithdraw,
generateKeyPairSigner,
generateUtxoKeypair,
partialWithdraw,
serializeUtxo,
signerFromSecretKey,
transact,
} from "@cloak.dev/sdk";
const connection = createCloakRpc("https://api.mainnet-beta.solana.com");
// `relayUrl` has no default and the SDK reads no environment variable for it.
// Pass the endpoint explicitly on every call. This build accepts exactly one
// origin, `CLOAK_PRODUCTION_RELAY_URL` ("https://api.cloak.ag"); any other host
// throws before a request is made.
const relayUrl = CLOAK_PRODUCTION_RELAY_URL;
// Placeholder: point KEYPAIR_PATH at your own keypair file. Never inline a
// secret key, and never read one from a raw env var.
const keypairPath = process.env.KEYPAIR_PATH;
if (!keypairPath) {
throw new Error("Set KEYPAIR_PATH to a Solana keypair file.");
}
const secretKeyBytes = Uint8Array.from(
JSON.parse(readFileSync(keypairPath, "utf8")) as number[],
);
const signer = await signerFromSecretKey(secretKeyBytes);
const amount = 1_000_000_000n; // 1 SOL
const owner = await generateUtxoKeypair();
const depositOutput = await createUtxo(amount, owner, NATIVE_SOL_MINT);
const deposited = await transact(
{
inputUtxos: [await createZeroUtxo(NATIVE_SOL_MINT)],
outputUtxos: [depositOutput],
externalAmount: amount,
depositor: signer.address,
},
{
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl,
depositorKeypair: signer,
walletPublicKey: signer.address,
},
);
// Persist the deposit note before the withdrawal below can run. The deposit is
// confirmed on chain by this line, but its blinding exists only in this
// process, so a withdrawal that throws (the 401 in the warning above is the
// usual one) exits with the SOL shielded and nothing left anywhere that can
// spend it. Same process is not the same as one atomic step.
const depositNotes = deposited.outputUtxos.filter((utxo) => utxo.amount > 0n);
for (const [i, note] of depositNotes.entries()) {
writeFileSync(`deposit-${deposited.signature}-${i}.note`, serializeUtxo(note));
}
const recipient = (await generateKeyPairSigner()).address;
const withdrawOptions = {
connection,
programId: CLOAK_PROGRAM_ID,
relayUrl,
depositorKeypair: signer,
walletPublicKey: signer.address,
cachedMerkleTree: deposited.merkleTree,
};
// Pick one. Both are submitted for you and both need an authenticated sender,
// which `depositorKeypair` supplies here.
const withdrawEverything: boolean = true;
if (withdrawEverything) {
// One note in, so this withdraws directly: nothing is left shielded and there is
// no change note. That holds for one or two notes only. Given three or more,
// `fullWithdraw` merges them on chain first, and a failure part way through
// strands the merged note. See /sdk/utxo-transactions for the full rule.
await fullWithdraw(deposited.outputUtxos, recipient, withdrawOptions);
} else {
// Partial withdrawal: withdraw a portion, keep private change in shielded state.
const withdrawal = await partialWithdraw(
deposited.outputUtxos,
recipient,
200_000_000n,
withdrawOptions,
);
// Persist the change note before anything else can go wrong. This is the 0.8
// SOL you did not withdraw, and its blinding lives only in this process, so a
// change note you never store is gone for good.
const changeNotes = withdrawal.outputUtxos.filter((utxo) => utxo.amount > 0n);
for (const [i, note] of changeNotes.entries()) {
writeFileSync(`change-${withdrawal.signature}-${i}.note`, serializeUtxo(note));
}
}