Skip to main content

Overview

Program Derived Addresses (PDAs) are deterministic addresses derived from seeds and the program ID. NullGraph uses PDAs for all on-chain accounts to ensure predictable, collision-free addressing.

Seed Constants

All PDA seeds are defined in the constants file:
lib/constants.ts
export const SEEDS = {
  PROTOCOL_STATE: 'protocol_state',
  NULL_RESULT: 'null_result',
  NULL_BOUNTY: 'null_bounty',
  BOUNTY_VAULT: 'bounty_vault',
  BOUNTY_SUBMISSION: 'bounty_submission',
} as const;

PDA Functions

All PDA derivation functions are located in lib/pda.ts and follow the same pattern:
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { PROGRAM_ID, SEEDS } from './constants';

findProtocolStatePDA

Derive the global protocol state account address.
function findProtocolStatePDA(): [PublicKey, number]

Seed Structure

seeds[0]
Buffer
Buffer.from('protocol_state')

Usage Example

import { findProtocolStatePDA } from '@/lib/pda';

const [protocolStatePDA, bump] = findProtocolStatePDA();
console.log('Protocol State:', protocolStatePDA.toString());
console.log('Bump:', bump);

Implementation

lib/pda.ts
export function findProtocolStatePDA(): [PublicKey, number] {
  return PublicKey.findProgramAddressSync(
    [Buffer.from(SEEDS.PROTOCOL_STATE)],
    PROGRAM_ID
  );
}
There is only one protocol state account per program deployment.

findNullResultPDA

Derive the address for a specific null result (NKA) submission.
function findNullResultPDA(
  researcher: PublicKey,
  specimenNumber: number | BN
): [PublicKey, number]

Parameters

researcher
PublicKey
required
The public key of the researcher submitting the null result
specimenNumber
number | BN
required
The specimen number (NKA counter value). Can be a JavaScript number or BN instance.

Seed Structure

seeds[0]
Buffer
Buffer.from('null_result')
seeds[1]
Buffer
researcher.toBuffer() - 32 bytes
seeds[2]
Buffer
specimenNumber as little-endian u64 - 8 bytes

Usage Example

import { findNullResultPDA } from '@/lib/pda';
import { useWallet } from '@solana/wallet-adapter-react';
import { useProtocolState } from '@/hooks/useProtocolState';

function MyComponent() {
  const { publicKey } = useWallet();
  const { data: protocol } = useProtocolState();

  if (!publicKey || !protocol) return null;

  const nextSpecimen = protocol.nkaCounter.toNumber() + 1;
  const [nullResultPDA, bump] = findNullResultPDA(publicKey, nextSpecimen);

  console.log(`Next NKA-${String(nextSpecimen).padStart(4, '0')} PDA:`, nullResultPDA.toString());
}

Implementation

lib/pda.ts
export function findNullResultPDA(
  researcher: PublicKey,
  specimenNumber: number | BN
): [PublicKey, number] {
  const bn = typeof specimenNumber === 'number' ? new BN(specimenNumber) : specimenNumber;
  return PublicKey.findProgramAddressSync(
    [
      Buffer.from(SEEDS.NULL_RESULT),
      researcher.toBuffer(),
      bn.toArrayLike(Buffer, 'le', 8),
    ],
    PROGRAM_ID
  );
}

findBountyPDA

Derive the address for a specific bounty.
function findBountyPDA(
  creator: PublicKey,
  bountyNumber: number | BN
): [PublicKey, number]

Parameters

creator
PublicKey
required
The public key of the bounty creator
bountyNumber
number | BN
required
The bounty number (bounty counter value)

Seed Structure

seeds[0]
Buffer
Buffer.from('null_bounty')
seeds[1]
Buffer
creator.toBuffer() - 32 bytes
seeds[2]
Buffer
bountyNumber as little-endian u64 - 8 bytes

Usage Example

import { findBountyPDA } from '@/lib/pda';
import { useWallet } from '@solana/wallet-adapter-react';

function MyBounties() {
  const { publicKey } = useWallet();

  if (!publicKey) return null;

  // Derive PDA for my first bounty
  const [bounty1PDA, bump] = findBountyPDA(publicKey, 1);
  console.log('My first bounty:', bounty1PDA.toString());
}

Implementation

lib/pda.ts
export function findBountyPDA(
  creator: PublicKey,
  bountyNumber: number | BN
): [PublicKey, number] {
  const bn = typeof bountyNumber === 'number' ? new BN(bountyNumber) : bountyNumber;
  return PublicKey.findProgramAddressSync(
    [
      Buffer.from(SEEDS.NULL_BOUNTY),
      creator.toBuffer(),
      bn.toArrayLike(Buffer, 'le', 8),
    ],
    PROGRAM_ID
  );
}

findVaultPDA

Derive the token vault address for a specific bounty.
function findVaultPDA(bountyPDA: PublicKey): [PublicKey, number]

Parameters

bountyPDA
PublicKey
required
The public key of the bounty account

Seed Structure

seeds[0]
Buffer
Buffer.from('bounty_vault')
seeds[1]
Buffer
bountyPDA.toBuffer() - 32 bytes

Usage Example

import { findBountyPDA, findVaultPDA } from '@/lib/pda';
import { PublicKey } from '@solana/web3.js';

const creator = new PublicKey('...');
const [bountyPDA] = findBountyPDA(creator, 1);
const [vaultPDA, bump] = findVaultPDA(bountyPDA);

console.log('Bounty vault:', vaultPDA.toString());

Implementation

lib/pda.ts
export function findVaultPDA(bountyPDA: PublicKey): [PublicKey, number] {
  return PublicKey.findProgramAddressSync(
    [Buffer.from(SEEDS.BOUNTY_VAULT), bountyPDA.toBuffer()],
    PROGRAM_ID
  );
}
The vault is a token account that holds BIO tokens for the bounty reward. It’s owned by the program and uses the bounty PDA as a seed.

findSubmissionPDA

Derive the address for a bounty submission.
function findSubmissionPDA(
  bountyPDA: PublicKey,
  nullResultPDA: PublicKey
): [PublicKey, number]

Parameters

bountyPDA
PublicKey
required
The public key of the bounty
nullResultPDA
PublicKey
required
The public key of the null result being submitted

Seed Structure

seeds[0]
Buffer
Buffer.from('bounty_submission')
seeds[1]
Buffer
bountyPDA.toBuffer() - 32 bytes
seeds[2]
Buffer
nullResultPDA.toBuffer() - 32 bytes

Usage Example

import { findSubmissionPDA, findBountyPDA, findNullResultPDA } from '@/lib/pda';
import { PublicKey } from '@solana/web3.js';

const creator = new PublicKey('...');
const researcher = new PublicKey('...');

const [bountyPDA] = findBountyPDA(creator, 1);
const [nullResultPDA] = findNullResultPDA(researcher, 5);
const [submissionPDA, bump] = findSubmissionPDA(bountyPDA, nullResultPDA);

console.log('Submission PDA:', submissionPDA.toString());

Implementation

lib/pda.ts
export function findSubmissionPDA(
  bountyPDA: PublicKey,
  nullResultPDA: PublicKey
): [PublicKey, number] {
  return PublicKey.findProgramAddressSync(
    [
      Buffer.from(SEEDS.BOUNTY_SUBMISSION),
      bountyPDA.toBuffer(),
      nullResultPDA.toBuffer(),
    ],
    PROGRAM_ID
  );
}
Each null result can only be submitted to a bounty once. The PDA ensures uniqueness.

PDA Relationships

Here’s how PDAs relate to each other:

Common Patterns

Deriving Next Account

import { useProtocolState } from '@/hooks/useProtocolState';
import { findNullResultPDA } from '@/lib/pda';
import { useWallet } from '@solana/wallet-adapter-react';

function useNextNullResultPDA() {
  const { publicKey } = useWallet();
  const { data: protocol } = useProtocolState();

  if (!publicKey || !protocol) return null;

  const nextSpecimen = protocol.nkaCounter.toNumber() + 1;
  return findNullResultPDA(publicKey, nextSpecimen);
}

Checking Account Existence

import { useProgram } from '@/context/ProgramContext';
import { findNullResultPDA } from '@/lib/pda';
import { PublicKey } from '@solana/web3.js';

async function checkNullResultExists(
  program: Program,
  researcher: PublicKey,
  specimenNumber: number
): Promise<boolean> {
  try {
    const [pda] = findNullResultPDA(researcher, specimenNumber);
    await program.account.nullResult.fetch(pda);
    return true;
  } catch {
    return false;
  }
}

Batch Deriving PDAs

import { findNullResultPDA } from '@/lib/pda';
import { PublicKey } from '@solana/web3.js';

function deriveMultipleNullResults(
  researcher: PublicKey,
  startNum: number,
  count: number
): PublicKey[] {
  return Array.from({ length: count }, (_, i) => {
    const [pda] = findNullResultPDA(researcher, startNum + i);
    return pda;
  });
}

// Example: Get PDAs for NKA-0001 through NKA-0010
const researcher = new PublicKey('...');
const pdas = deriveMultipleNullResults(researcher, 1, 10);

Return Values

All PDA functions return a tuple:
[0]
PublicKey
The derived Program Derived Address
[1]
number
The bump seed (0-255) used to find the PDA off the ed25519 curve

Best Practices

Always use the PDA derivation functions instead of hardcoding addresses. PDAs are deterministic and will be the same across all clients.
When incrementing counters (specimenNumber, bountyNumber), always fetch the latest protocol state first to avoid PDA collisions.
The bump seed is automatically stored in account data by the program. You typically don’t need to use it in frontend code.

Next Steps

Hooks

Learn how hooks use PDAs internally for transactions

Types

Explore account structures that live at these PDAs

Build docs developers (and LLMs) love