Project Structure
The template follows Next.js App Router conventions with a clean separation of concerns.
Core Files Explained
app/layout.tsx
The root layout wraps your entire app with the wallet provider:
import WalletProvider from "@/wallet/provider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<WalletProvider>{children}</WalletProvider>
</body>
</html>
);
}
app/page.tsx
The main page composes all components:
"use client";
import Header from "@/components/header";
import Padder from "@/components/padder";
import EncryptedInput from "@/components/encrypted-input";
import Balance from "@/components/balance";
const Page = () => {
return (
<Padder>
<Header />
<div className="max-w-md mx-auto">
<EncryptedInput />
<Balance />
</div>
</Padder>
);
};
export default Page;
utils/constants.ts
Contains program IDs, PDA derivation, and helper functions:
import { PublicKey } from "@solana/web3.js";
// Your deployed program ID (from IDL)
export const PROGRAM_ID = new PublicKey(idl.address);
// Inco Lightning program on devnet
export const INCO_LIGHTNING_PROGRAM_ID = new PublicKey(
"5sjEbPiqgZrYwR31ahR6Uk9wf5awoX61YGg7jExQSwaj"
);
// Account discriminators for filtering
export const INCO_MINT_DISCRIMINATOR = [254, 129, 245, 169, 202, 143, 198, 4];
export const INCO_ACCOUNT_DISCRIMINATOR = [18, 233, 131, 18, 230, 173, 249, 89];
Key Helper Functions
getAllowancePda
Derives the allowance PDA for a handle and wallet:
export const getAllowancePda = (
handle: bigint,
allowedAddress: PublicKey
): [PublicKey, number] => {
// Convert handle to little-endian bytes
const handleBuffer = Buffer.alloc(16);
let h = handle;
for (let i = 0; i < 16; i++) {
handleBuffer[i] = Number(h & BigInt(0xff));
h = h >> BigInt(8);
}
return PublicKey.findProgramAddressSync(
[handleBuffer, allowedAddress.toBuffer()],
INCO_LIGHTNING_PROGRAM_ID
);
};
extractHandle
Extracts the encrypted handle from account data:
export const extractHandle = (data: Buffer): bigint => {
// Handle is stored as u128 at offset 72-88 (little-endian)
const bytes = data.slice(72, 88);
let result = BigInt(0);
for (let i = 15; i >= 0; i--) {
result = result * BigInt(256) + BigInt(bytes[i]);
}
return result;
};
The offset 72-88 is specific to the Inco Token program’s IncoAccount structure. Adjust the offset based on your program’s account layout.
fetchUserMint
Finds the user’s mint account:
export const fetchUserMint = async (
connection: Connection,
wallet: PublicKey
): Promise<{ pubkey: PublicKey; data: Buffer } | null> => {
const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [
{
memcmp: {
offset: 0,
bytes: bs58.encode(Buffer.from(INCO_MINT_DISCRIMINATOR)),
},
},
{ memcmp: { offset: 9, bytes: wallet.toBase58() } },
],
});
return accounts.length
? { pubkey: accounts[0].pubkey, data: accounts[0].account.data as Buffer }
: null;
};
fetchUserTokenAccount
Finds the user’s token account for a specific mint:
export const fetchUserTokenAccount = async (
connection: Connection,
wallet: PublicKey,
mint: PublicKey
): Promise<{ pubkey: PublicKey; data: Buffer } | null> => {
const accounts = await connection.getProgramAccounts(PROGRAM_ID, {
filters: [
{
memcmp: {
offset: 0,
bytes: bs58.encode(Buffer.from(INCO_ACCOUNT_DISCRIMINATOR)),
},
},
{ memcmp: { offset: 8, bytes: mint.toBase58() } },
{ memcmp: { offset: 40, bytes: wallet.toBase58() } },
],
});
return accounts.length
? { pubkey: accounts[0].pubkey, data: accounts[0].account.data as Buffer }
: null;
};
getProgram
Creates an Anchor program instance:
export const getProgram = (connection: Connection, wallet: AnchorWallet) => {
const provider = new AnchorProvider(connection, wallet, {
commitment: "confirmed",
});
return new Program(idl as Idl, provider);
};
Next Steps
Learn how to set up Wallet Integration for your dApp.