Client Integration
This guide shows how to interact with the Private Raffle program using TypeScript.Setup
Copy
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { PrivateLottery } from "../target/types/private_lottery";
import {
PublicKey,
Keypair,
SystemProgram,
Connection,
SYSVAR_INSTRUCTIONS_PUBKEY,
Transaction
} from "@solana/web3.js";
import nacl from "tweetnacl";
import { encryptValue } from "@inco/solana-sdk/encryption";
import { decrypt } from "@inco/solana-sdk/attested-decrypt";
import { hexToBuffer, handleToBuffer, plaintextToBuffer } from "@inco/solana-sdk/utils";
const INCO_LIGHTNING_PROGRAM_ID = new PublicKey(
"5sjEbPiqgZrYwR31ahR6Uk9wf5awoX61YGg7jExQSwaj"
);
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.privateLottery as Program<PrivateLottery>;
const wallet = (provider.wallet as any).payer as Keypair;
Derive PDAs
Copy
const lotteryId = Math.floor(Date.now() / 1000);
const idBuffer = Buffer.alloc(8);
idBuffer.writeBigUInt64LE(BigInt(lotteryId));
const [lotteryPda] = PublicKey.findProgramAddressSync(
[Buffer.from("lottery"), idBuffer],
program.programId
);
const [vaultPda] = PublicKey.findProgramAddressSync(
[Buffer.from("vault"), lotteryPda.toBuffer()],
program.programId
);
const [ticketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("ticket"), lotteryPda.toBuffer(), wallet.publicKey.toBuffer()],
program.programId
);
Helper Functions
Derive Allowance PDA
Copy
function deriveAllowancePda(handle: bigint): [PublicKey, number] {
const buf = Buffer.alloc(16);
let v = handle;
for (let i = 0; i < 16; i++) {
buf[i] = Number(v & BigInt(0xff));
v >>= BigInt(8);
}
return PublicKey.findProgramAddressSync(
[buf, wallet.publicKey.toBuffer()],
INCO_LIGHTNING_PROGRAM_ID
);
}
Decrypt Handle
Copy
async function decryptHandle(handle: string): Promise<{
plaintext: string;
ed25519Instructions: any[];
} | null> {
await new Promise(r => setTimeout(r, 2000));
try {
const result = await decrypt([handle], {
address: wallet.publicKey,
signMessage: async (msg: Uint8Array) =>
nacl.sign.detached(msg, wallet.secretKey),
});
return {
plaintext: result.plaintexts[0],
ed25519Instructions: result.ed25519Instructions,
};
} catch {
return null;
}
}
Get Handle from Simulation
Copy
async function getHandleFromSimulation(
tx: Transaction,
prefix: string
): Promise<bigint | null> {
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = wallet.publicKey;
tx.sign(wallet);
const sim = await connection.simulateTransaction(tx);
for (const log of sim.value.logs || []) {
if (log.includes(prefix)) {
const match = log.match(/(\d+)/);
if (match) return BigInt(match[1]);
}
}
return null;
}
Step-by-Step Flow
1. Create Raffle
Copy
const TICKET_PRICE = 10_000_000; // 0.01 SOL
await program.methods
.createLottery(new anchor.BN(lotteryId), new anchor.BN(TICKET_PRICE))
.accounts({
authority: wallet.publicKey,
lottery: lotteryPda,
vault: vaultPda,
systemProgram: SystemProgram.programId,
})
.rpc();
console.log("Raffle created! Guess a number 1-100");
2. Buy Ticket with Encrypted Guess
Copy
const MY_GUESS = 42;
console.log("My guess:", MY_GUESS, "(encrypted, nobody sees this!)");
const encryptedGuess = await encryptValue(BigInt(MY_GUESS));
await program.methods
.buyTicket(hexToBuffer(encryptedGuess))
.accounts({
buyer: wallet.publicKey,
lottery: lotteryPda,
ticket: ticketPda,
vault: vaultPda,
systemProgram: SystemProgram.programId,
incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
})
.rpc();
console.log("Ticket purchased!");
3. Authority Draws Winning Number
Copy
const WINNING_NUMBER = 42;
console.log("Winning number:", WINNING_NUMBER, "(encrypted, nobody sees!)");
const encryptedWinning = await encryptValue(BigInt(WINNING_NUMBER));
await program.methods
.drawWinner(hexToBuffer(encryptedWinning))
.accounts({
authority: wallet.publicKey,
lottery: lotteryPda,
systemProgram: SystemProgram.programId,
incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
})
.rpc();
console.log("Winning number set!");
4. Check If Won (Encrypted Comparison)
Currently, checking the result requires a transaction. When attested compute is introduced, this will be done off-chain without a transaction.
Copy
// First simulate to get the result handle
const txForSim = await program.methods
.checkWinner()
.accounts({
checker: wallet.publicKey,
lottery: lotteryPda,
ticket: ticketPda,
systemProgram: SystemProgram.programId,
incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
})
.transaction();
const resultHandle = await getHandleFromSimulation(txForSim, "Result handle:");
if (resultHandle) {
// Derive allowance PDA and submit with remaining accounts
const [allowancePda] = deriveAllowancePda(resultHandle);
await program.methods
.checkWinner()
.accounts({
checker: wallet.publicKey,
lottery: lotteryPda,
ticket: ticketPda,
systemProgram: SystemProgram.programId,
incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
})
.remainingAccounts([
{ pubkey: allowancePda, isSigner: false, isWritable: true },
{ pubkey: wallet.publicKey, isSigner: false, isWritable: false },
])
.rpc();
// Decrypt the result
const result = await decryptHandle(resultHandle.toString());
if (result) {
const won = result.plaintext === "1";
console.log("Did I win?", won ? "YES!" : "No");
}
}
5. Claim Prize
Copy
const txForSim = await program.methods
.claimPrize()
.accounts({
claimer: wallet.publicKey,
lottery: lotteryPda,
ticket: ticketPda,
vault: vaultPda,
systemProgram: SystemProgram.programId,
incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
})
.transaction();
const prizeHandle = await getHandleFromSimulation(txForSim, "Prize handle:");
if (prizeHandle) {
const [allowancePda] = deriveAllowancePda(prizeHandle);
await program.methods
.claimPrize()
.accounts({
claimer: wallet.publicKey,
lottery: lotteryPda,
ticket: ticketPda,
vault: vaultPda,
systemProgram: SystemProgram.programId,
incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
})
.remainingAccounts([
{ pubkey: allowancePda, isSigner: false, isWritable: true },
{ pubkey: wallet.publicKey, isSigner: false, isWritable: false },
])
.rpc();
console.log("Claim processed!");
}
6. Withdraw Prize (with On-Chain Verification)
Copy
// Fetch ticket to get prize handle
const ticket = await program.account.ticket.fetch(ticketPda);
const prizeHandle = ticket.prizeHandle.toString();
if (prizeHandle === "0") {
console.log("No prize to claim");
return;
}
// Decrypt prize amount (includes Ed25519 instructions for verification)
const result = await decryptHandle(prizeHandle);
if (!result) {
console.log("Failed to decrypt");
return;
}
const prize = BigInt(result.plaintext);
console.log("Prize amount:", Number(prize) / 1e9, "SOL");
if (prize > 0) {
// Build transaction with Ed25519 signature verification
const withdrawIx = await program.methods
.withdrawPrize(
handleToBuffer(prizeHandle),
plaintextToBuffer(result.plaintext)
)
.accounts({
winner: wallet.publicKey,
lottery: lotteryPda,
ticket: ticketPda,
vault: vaultPda,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
systemProgram: SystemProgram.programId,
incoLightningProgram: INCO_LIGHTNING_PROGRAM_ID,
})
.instruction();
const tx = new Transaction();
// Add Ed25519 signature instructions FIRST
result.ed25519Instructions.forEach(ix => tx.add(ix));
// Then add withdraw instruction
tx.add(withdrawIx);
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = wallet.publicKey;
const signedTx = await provider.wallet.signTransaction(tx);
const sig = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(sig, "confirmed");
console.log("Prize withdrawn!", sig);
} else {
console.log("Not a winner - prize is 0");
}
Non-Winner Flow
When a player doesn’t win:check_winnerreturns encryptedfalse(decrypts to “0”)claim_prizeusese_selectto setprize_handleto encrypted 0withdraw_prizefails withNotWinnererror (prize amount is 0)
Copy
// Prize is 0 - cannot withdraw
try {
await program.methods
.withdrawPrize(handleToBuffer(prizeHandle), plaintextToBuffer("0"))
.accounts({...})
.rpc();
} catch (e) {
// Error: NotWinner - this is expected!
console.log("Correctly rejected - not a winner");
}
Running Tests
Copy
anchor test --provider.cluster devnet