Overview
The NullGraph SDK provides fully typed TypeScript interfaces for all on-chain accounts, ensuring type safety when building frontend applications.
Account Interfaces
All account types are exported from types/index.ts:
import type { PublicKey } from '@solana/web3.js';
import type { BN } from '@coral-xyz/anchor';
ProtocolStateAccount
The global protocol state containing counters, fees, and treasury information.
interface ProtocolStateAccount {
authority: PublicKey;
nkaCounter: BN;
bountyCounter: BN;
feeBasisPoints: number;
treasury: PublicKey;
bump: number;
}
Fields
The protocol authority that can update fees and treasury
Total number of null results submitted. Next submission will be nkaCounter + 1
Total number of bounties created. Next bounty will be bountyCounter + 1
Protocol fee in basis points (e.g., 250 = 2.5%). Applied when bounties are fulfilled.
Treasury address that receives protocol fees
PDA bump seed for the protocol state account
Usage Example
import { useProtocolState } from '@/hooks/useProtocolState';
import type { ProtocolStateAccount } from '@/types';
function ProtocolInfo() {
const { data } = useProtocolState();
if (!data) return null;
const feePercent = data.feeBasisPoints / 100;
return (
<div>
<p>Total NKAs: {data.nkaCounter.toString()}</p>
<p>Total Bounties: {data.bountyCounter.toString()}</p>
<p>Protocol Fee: {feePercent}%</p>
<p>Treasury: {data.treasury.toString()}</p>
</div>
);
}
NullResultAccount
A null result (NKA) submission containing research metadata and statistical information.
interface NullResultAccount {
researcher: PublicKey;
specimenNumber: BN;
hypothesis: number[];
methodology: number[];
expectedOutcome: number[];
actualOutcome: number[];
pValue: number;
sampleSize: number;
dataHash: number[];
status: number;
createdAt: BN;
bump: number;
}
Fields
Public key of the researcher who submitted this null result
Unique specimen number (e.g., 1 for NKA-0001)
UTF-8 encoded hypothesis string as byte array (max 256 bytes)
UTF-8 encoded methodology description as byte array (max 512 bytes)
UTF-8 encoded expected outcome as byte array (max 256 bytes)
UTF-8 encoded actual outcome as byte array (max 256 bytes)
Statistical p-value (0.0 to 1.0) indicating significance
Number of samples in the study
32-byte hash of the underlying research data for verification
Current status: 0 = Pending, 1 = Verified, 2 = Disputed
Unix timestamp when the null result was submitted
Usage Example
import { useNullResults } from '@/hooks/useNullResults';
import { NKA_STATUS } from '@/types';
function NullResultCard({ result }: { result: NullResultAccount }) {
const hypothesis = Buffer.from(result.hypothesis).toString('utf-8');
const methodology = Buffer.from(result.methodology).toString('utf-8');
const actualOutcome = Buffer.from(result.actualOutcome).toString('utf-8');
const statusLabel = result.status === NKA_STATUS.PENDING
? 'Pending'
: result.status === NKA_STATUS.VERIFIED
? 'Verified'
: 'Disputed';
return (
<div>
<h3>NKA-{result.specimenNumber.toString().padStart(4, '0')}</h3>
<p><strong>Status:</strong> {statusLabel}</p>
<p><strong>Hypothesis:</strong> {hypothesis}</p>
<p><strong>Methodology:</strong> {methodology}</p>
<p><strong>Actual Outcome:</strong> {actualOutcome}</p>
<p><strong>P-Value:</strong> {result.pValue.toFixed(4)}</p>
<p><strong>Sample Size:</strong> {result.sampleSize}</p>
</div>
);
}
NullBountyAccount
A bounty offering rewards for matching null results.
interface NullBountyAccount {
creator: PublicKey;
bountyNumber: BN;
description: number[];
rewardAmount: BN;
usdcMint: PublicKey;
vault: PublicKey;
deadline: BN;
status: number;
matchedSubmission: PublicKey;
createdAt: BN;
vaultBump: number;
bump: number;
}
Fields
Public key of the bounty creator
Unique bounty number (e.g., 1 for NB-0001)
UTF-8 encoded bounty description as byte array (max 512 bytes)
Reward amount in token lamports (BIO has 6 decimals)
Mint address of the reward token (typically BIO token)
Token account holding the bounty reward
Unix timestamp when the bounty expires
Current status: 0 = Open, 1 = Matched, 2 = Fulfilled, 3 = Closed
Public key of the approved submission (if status is Matched or Fulfilled)
Unix timestamp when the bounty was created
PDA bump seed for the vault account
PDA bump seed for the bounty account
Usage Example
import { useBounties } from '@/hooks/useBounties';
import { BOUNTY_STATUS } from '@/types';
function BountyCard({ bounty }: { bounty: NullBountyAccount }) {
const description = Buffer.from(bounty.description).toString('utf-8');
const rewardBIO = bounty.rewardAmount.toNumber() / 1_000_000; // Convert from lamports
const deadlineDate = new Date(bounty.deadline.toNumber() * 1000);
const statusLabel = bounty.status === BOUNTY_STATUS.OPEN
? 'Open'
: bounty.status === BOUNTY_STATUS.MATCHED
? 'Matched'
: bounty.status === BOUNTY_STATUS.FULFILLED
? 'Fulfilled'
: 'Closed';
const isExpired = Date.now() > deadlineDate.getTime();
return (
<div>
<h3>NB-{bounty.bountyNumber.toString().padStart(4, '0')}</h3>
<p><strong>Status:</strong> {statusLabel}</p>
<p><strong>Reward:</strong> {rewardBIO} BIO</p>
<p><strong>Description:</strong> {description}</p>
<p><strong>Deadline:</strong> {deadlineDate.toLocaleDateString()}</p>
{isExpired && <p style={{ color: 'red' }}>Expired</p>}
</div>
);
}
BountySubmissionAccount
A submission linking a null result to a bounty.
interface BountySubmissionAccount {
researcher: PublicKey;
nullResult: PublicKey;
bounty: PublicKey;
status: number;
createdAt: BN;
bump: number;
}
Fields
Public key of the researcher who submitted
Public key of the null result being submitted
Submission status: 0 = Pending, 1 = Approved, 2 = Rejected
Unix timestamp when submission was created
Usage Example
import { useBountySubmissions } from '@/hooks/useBountySubmissions';
import { SUBMISSION_STATUS } from '@/types';
import type { PublicKey } from '@solana/web3.js';
function SubmissionList({ bountyKey }: { bountyKey: PublicKey }) {
const { data: submissions } = useBountySubmissions(bountyKey);
const pending = submissions.filter(s => s.status === SUBMISSION_STATUS.PENDING);
const approved = submissions.filter(s => s.status === SUBMISSION_STATUS.APPROVED);
const rejected = submissions.filter(s => s.status === SUBMISSION_STATUS.REJECTED);
return (
<div>
<h4>Pending: {pending.length}</h4>
<h4>Approved: {approved.length}</h4>
<h4>Rejected: {rejected.length}</h4>
</div>
);
}
Extended Types with Public Keys
For convenience, the SDK provides extended interfaces that include the account’s public key:
NullResultWithKey
interface NullResultWithKey extends NullResultAccount {
publicKey: PublicKey;
}
Returned by useNullResults() hook.
NullBountyWithKey
interface NullBountyWithKey extends NullBountyAccount {
publicKey: PublicKey;
}
Returned by useBounties() hook.
BountySubmissionWithKey
interface BountySubmissionWithKey extends BountySubmissionAccount {
publicKey: PublicKey;
}
Returned by useBountySubmissions() hook.
Status Enums
NKA_STATUS
Status values for null results:
export const NKA_STATUS = {
PENDING: 0,
VERIFIED: 1,
DISPUTED: 2,
} as const;
Usage Example
import { NKA_STATUS } from '@/types';
import type { NullResultAccount } from '@/types';
function getStatusLabel(result: NullResultAccount): string {
switch (result.status) {
case NKA_STATUS.PENDING:
return 'Pending Review';
case NKA_STATUS.VERIFIED:
return 'Verified';
case NKA_STATUS.DISPUTED:
return 'Under Dispute';
default:
return 'Unknown';
}
}
BOUNTY_STATUS
Status values for bounties:
export const BOUNTY_STATUS = {
OPEN: 0,
MATCHED: 1,
FULFILLED: 2,
CLOSED: 3,
} as const;
Status Flow
- OPEN (0): Bounty is accepting submissions
- MATCHED (1): Creator approved a submission but hasn’t transferred funds yet
- FULFILLED (2): Reward has been paid out to researcher
- CLOSED (3): Bounty was closed by creator without fulfillment
Usage Example
import { BOUNTY_STATUS } from '@/types';
import type { NullBountyAccount } from '@/types';
function canSubmit(bounty: NullBountyAccount): boolean {
return bounty.status === BOUNTY_STATUS.OPEN;
}
function canClose(bounty: NullBountyAccount): boolean {
return bounty.status === BOUNTY_STATUS.OPEN;
}
SUBMISSION_STATUS
Status values for bounty submissions:
export const SUBMISSION_STATUS = {
PENDING: 0,
APPROVED: 1,
REJECTED: 2,
} as const;
Usage Example
import { SUBMISSION_STATUS } from '@/types';
import type { BountySubmissionAccount } from '@/types';
function getSubmissionColor(submission: BountySubmissionAccount): string {
switch (submission.status) {
case SUBMISSION_STATUS.PENDING:
return 'yellow';
case SUBMISSION_STATUS.APPROVED:
return 'green';
case SUBMISSION_STATUS.REJECTED:
return 'red';
default:
return 'gray';
}
}
Type Guards
Create type guards for runtime type checking:
import type { NullResultWithKey, NullBountyWithKey } from '@/types';
function isNullResult(account: unknown): account is NullResultWithKey {
return (
typeof account === 'object' &&
account !== null &&
'specimenNumber' in account &&
'researcher' in account &&
'pValue' in account
);
}
function isNullBounty(account: unknown): account is NullBountyWithKey {
return (
typeof account === 'object' &&
account !== null &&
'bountyNumber' in account &&
'creator' in account &&
'rewardAmount' in account
);
}
Utility Functions
Converting Byte Arrays to Strings
function bytesToString(bytes: number[]): string {
// Remove trailing zeros
const trimmed = bytes.filter(b => b !== 0);
return Buffer.from(trimmed).toString('utf-8');
}
// Usage
const hypothesis = bytesToString(nullResult.hypothesis);
Converting Strings to Byte Arrays
function stringToBytes(str: string, maxLength: number): number[] {
const buffer = Buffer.from(str, 'utf-8');
if (buffer.length > maxLength) {
throw new Error(`String exceeds max length of ${maxLength} bytes`);
}
return buffer.toJSON().data;
}
// Usage
const hypothesis = stringToBytes('My hypothesis', 256);
import type { BN } from '@coral-xyz/anchor';
function formatTimestamp(timestamp: BN): string {
const date = new Date(timestamp.toNumber() * 1000);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
// Usage
const createdDate = formatTimestamp(nullResult.createdAt);
function formatBIO(lamports: BN): string {
const bio = lamports.toNumber() / 1_000_000;
return `${bio.toFixed(2)} BIO`;
}
// Usage
const reward = formatBIO(bounty.rewardAmount);
Type Safety Best Practices
Always use the typed interfaces when working with account data. This prevents runtime errors from typos or incorrect field access.
When converting byte arrays to strings, always check for trailing zeros and handle empty arrays gracefully.
Use BN.toNumber() carefully with large numbers. For display purposes, consider using BN.toString() instead to avoid precision loss.
Complete Type Import
Import all types in one statement:
import type {
ProtocolStateAccount,
NullResultAccount,
NullBountyAccount,
BountySubmissionAccount,
NullResultWithKey,
NullBountyWithKey,
BountySubmissionWithKey,
} from '@/types';
import {
NKA_STATUS,
BOUNTY_STATUS,
SUBMISSION_STATUS,
} from '@/types';
Next Steps
Hooks
Learn how to fetch and manipulate these types with React hooks
PDA Derivation
Understand how to derive addresses for these accounts