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:
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
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
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
The public key of the researcher submitting the null result
The specimen number (NKA counter value). Can be a JavaScript number or BN instance.
Seed Structure
Buffer.from('null_result')
researcher.toBuffer() - 32 bytes
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
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
The public key of the bounty creator
The bounty number (bounty counter value)
Seed Structure
Buffer.from('null_bounty')
creator.toBuffer() - 32 bytes
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
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
The public key of the bounty account
Seed Structure
Buffer.from('bounty_vault')
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
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
The public key of the bounty
The public key of the null result being submitted
Seed Structure
Buffer.from('bounty_submission')
bountyPDA.toBuffer() - 32 bytes
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
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:
The derived Program Derived Address
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