Skip to main content

Client Integration

This guide shows how to interact with the Private Raffle program using TypeScript.

Setup

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

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

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

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

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

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

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

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.
// 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

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)

// 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:
  1. check_winner returns encrypted false (decrypts to “0”)
  2. claim_prize uses e_select to set prize_handle to encrypted 0
  3. withdraw_prize fails with NotWinner error (prize amount is 0)
// 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

anchor test --provider.cluster devnet
The test file includes both winner and non-winner flows to verify the entire system works correctly.