Overview
The NullGraph SDK provides 9 React hooks for interacting with the Solana program:
- Read Hooks: Fetch on-chain data with automatic updates
- Write Hooks: Submit transactions and modify state
Read Hooks
useProtocolState
Fetch the global protocol state including counters, fees, and treasury.
function useProtocolState(): {
data: ProtocolStateAccount | null;
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
Usage Example
import { useProtocolState } from '@/hooks/useProtocolState';
function ProtocolStats() {
const { data, loading, error, refetch } = useProtocolState();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return null;
return (
<div>
<p>Total NKAs: {data.nkaCounter.toString()}</p>
<p>Total Bounties: {data.bountyCounter.toString()}</p>
<p>Fee: {data.feeBasisPoints / 100}%</p>
<button onClick={refetch}>Refresh</button>
</div>
);
}
Return Fields
data
ProtocolStateAccount | null
The protocol state account data, or null if not yet loaded
True while fetching data from the blockchain
Error message if the fetch failed
Function to manually refresh the data
useNullResults
Fetch all null result (NKA) submissions, sorted by specimen number descending.
function useNullResults(): {
data: NullResultWithKey[];
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
Usage Example
import { useNullResults } from '@/hooks/useNullResults';
function NullResultList() {
const { data, loading, error } = useNullResults();
if (loading) return <div>Loading null results...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{data.map((result) => (
<li key={result.publicKey.toString()}>
NKA-{result.specimenNumber.toString().padStart(4, '0')}
<br />
Researcher: {result.researcher.toString()}
<br />
P-Value: {result.pValue}
<br />
Sample Size: {result.sampleSize}
</li>
))}
</ul>
);
}
Return Fields
Array of null result accounts, each with a publicKey field
Error message if fetch failed
Manually refresh the data
useBounties
Fetch all bounties, sorted by bounty number descending.
function useBounties(): {
data: NullBountyWithKey[];
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
Usage Example
import { useBounties } from '@/hooks/useBounties';
import { BOUNTY_STATUS } from '@/types';
function BountyList() {
const { data, loading, error } = useBounties();
if (loading) return <div>Loading bounties...</div>;
if (error) return <div>Error: {error}</div>;
const openBounties = data.filter(b => b.status === BOUNTY_STATUS.OPEN);
return (
<ul>
{openBounties.map((bounty) => (
<li key={bounty.publicKey.toString()}>
NB-{bounty.bountyNumber.toString().padStart(4, '0')}
<br />
Reward: {bounty.rewardAmount.toString()} BIO
<br />
Creator: {bounty.creator.toString()}
</li>
))}
</ul>
);
}
useBountySubmissions
Fetch submissions for a specific bounty, or all submissions if no bounty is specified.
function useBountySubmissions(bountyKey?: PublicKey): {
data: BountySubmissionWithKey[];
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
Usage Example
import { useBountySubmissions } from '@/hooks/useBountySubmissions';
import { PublicKey } from '@solana/web3.js';
import { SUBMISSION_STATUS } from '@/types';
function BountySubmissionList({ bountyKey }: { bountyKey: PublicKey }) {
const { data, loading, error } = useBountySubmissions(bountyKey);
if (loading) return <div>Loading submissions...</div>;
if (error) return <div>Error: {error}</div>;
const pending = data.filter(s => s.status === SUBMISSION_STATUS.PENDING);
return (
<div>
<h3>Pending Submissions: {pending.length}</h3>
<ul>
{pending.map((submission) => (
<li key={submission.publicKey.toString()}>
Researcher: {submission.researcher.toString()}
<br />
NKA: {submission.nullResult.toString()}
</li>
))}
</ul>
</div>
);
}
Parameters
Public key of the bounty to filter submissions. If omitted, returns all submissions.
Write Hooks
useSubmitNullResult
Submit a new null result (NKA) to the protocol.
interface SubmitNullResultArgs {
hypothesis: number[];
methodology: number[];
expectedOutcome: number[];
actualOutcome: number[];
pValue: number;
sampleSize: number;
dataHash: number[];
}
function useSubmitNullResult(): {
submit: (args: SubmitNullResultArgs) => Promise<number | null>;
loading: boolean;
}
Usage Example
import { useSubmitNullResult } from '@/hooks/useSubmitNullResult';
import { useState } from 'react';
function SubmitNullResultForm() {
const { submit, loading } = useSubmitNullResult();
const [pValue, setPValue] = useState('');
const [sampleSize, setSampleSize] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const args = {
hypothesis: Buffer.from('H0: No effect observed').toJSON().data,
methodology: Buffer.from('Double-blind RCT').toJSON().data,
expectedOutcome: Buffer.from('Significant result').toJSON().data,
actualOutcome: Buffer.from('No significant difference').toJSON().data,
pValue: parseFloat(pValue),
sampleSize: parseInt(sampleSize),
dataHash: new Array(32).fill(0), // Replace with actual hash
};
const specimenNumber = await submit(args);
if (specimenNumber) {
console.log(`Submitted NKA-${specimenNumber.toString().padStart(4, '0')}`);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="number"
step="0.001"
placeholder="P-Value"
value={pValue}
onChange={(e) => setPValue(e.target.value)}
required
/>
<input
type="number"
placeholder="Sample Size"
value={sampleSize}
onChange={(e) => setSampleSize(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Submitting...' : 'Submit NKA'}
</button>
</form>
);
}
Parameters
UTF-8 encoded hypothesis string as byte array
UTF-8 encoded methodology description as byte array
UTF-8 encoded expected outcome as byte array
UTF-8 encoded actual outcome as byte array
Statistical p-value (0.0 to 1.0)
Number of samples in the study
32-byte hash of the underlying data
Return Value
Returns the specimen number (e.g., 1, 2, 3) on success, or null if the transaction failed.
useCreateBounty
Create a new bounty with BIO token rewards.
interface CreateBountyArgs {
description: number[];
rewardAmount: number; // in BIO (6 decimals)
deadline: number; // Unix timestamp
}
function useCreateBounty(): {
create: (args: CreateBountyArgs) => Promise<number | null>;
loading: boolean;
}
Usage Example
import { useCreateBounty } from '@/hooks/useCreateBounty';
import { useState } from 'react';
function CreateBountyForm() {
const { create, loading } = useCreateBounty();
const [description, setDescription] = useState('');
const [rewardAmount, setRewardAmount] = useState('');
const [deadline, setDeadline] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const args = {
description: Buffer.from(description).toJSON().data,
rewardAmount: parseFloat(rewardAmount) * 1_000_000, // Convert to lamports
deadline: new Date(deadline).getTime() / 1000, // Convert to Unix timestamp
};
const bountyNumber = await create(args);
if (bountyNumber) {
console.log(`Created NB-${bountyNumber.toString().padStart(4, '0')}`);
}
};
return (
<form onSubmit={handleSubmit}>
<textarea
placeholder="Bounty description"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
<input
type="number"
placeholder="Reward (BIO)"
value={rewardAmount}
onChange={(e) => setRewardAmount(e.target.value)}
required
/>
<input
type="datetime-local"
value={deadline}
onChange={(e) => setDeadline(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Bounty'}
</button>
</form>
);
}
The hook automatically handles BIO token transfers from the creator’s associated token account to the bounty vault.
useSubmitToBounty
Submit an existing null result to a bounty.
function useSubmitToBounty(): {
submit: (bountyKey: PublicKey, nullResultKey: PublicKey) => Promise<boolean>;
loading: boolean;
}
Usage Example
import { useSubmitToBounty } from '@/hooks/useSubmitToBounty';
import { PublicKey } from '@solana/web3.js';
function SubmitToBountyButton({
bountyKey,
nullResultKey
}: {
bountyKey: PublicKey;
nullResultKey: PublicKey;
}) {
const { submit, loading } = useSubmitToBounty();
const handleSubmit = async () => {
const success = await submit(bountyKey, nullResultKey);
if (success) {
console.log('Submission created!');
}
};
return (
<button onClick={handleSubmit} disabled={loading}>
{loading ? 'Submitting...' : 'Submit to Bounty'}
</button>
);
}
useApproveBountySubmission
Approve a bounty submission and transfer rewards to the researcher.
function useApproveBountySubmission(): {
approve: (
bounty: NullBountyWithKey,
submission: BountySubmissionWithKey
) => Promise<boolean>;
loading: boolean;
}
Usage Example
import { useApproveBountySubmission } from '@/hooks/useApproveBountySubmission';
import type { NullBountyWithKey, BountySubmissionWithKey } from '@/types';
function ApproveSubmissionButton({
bounty,
submission
}: {
bounty: NullBountyWithKey;
submission: BountySubmissionWithKey;
}) {
const { approve, loading } = useApproveBountySubmission();
const handleApprove = async () => {
const success = await approve(bounty, submission);
if (success) {
console.log('Bounty approved and BIO transferred!');
}
};
return (
<button onClick={handleApprove} disabled={loading}>
{loading ? 'Approving...' : 'Approve Submission'}
</button>
);
}
This hook automatically creates associated token accounts for the researcher and treasury if they don’t exist. The creator pays for account rent.
useCloseBounty
Close an open bounty and refund BIO tokens to the creator.
function useCloseBounty(): {
close: (bounty: NullBountyWithKey) => Promise<boolean>;
loading: boolean;
}
Usage Example
import { useCloseBounty } from '@/hooks/useCloseBounty';
import type { NullBountyWithKey } from '@/types';
import { BOUNTY_STATUS } from '@/types';
function CloseBountyButton({ bounty }: { bounty: NullBountyWithKey }) {
const { close, loading } = useCloseBounty();
const handleClose = async () => {
if (bounty.status !== BOUNTY_STATUS.OPEN) {
alert('Only open bounties can be closed');
return;
}
const success = await close(bounty);
if (success) {
console.log('Bounty closed and BIO refunded!');
}
};
return (
<button onClick={handleClose} disabled={loading}>
{loading ? 'Closing...' : 'Close Bounty'}
</button>
);
}
Hook Patterns
Error Handling
All write hooks display toast notifications automatically. Read hooks return error messages:
const { data, error } = useProtocolState();
if (error) {
return <ErrorComponent message={error} />;
}
Loading States
All hooks provide a loading boolean for UI feedback:
const { submit, loading } = useSubmitNullResult();
return (
<button disabled={loading}>
{loading ? 'Submitting...' : 'Submit'}
</button>
);
Refetching Data
Read hooks provide a refetch function for manual updates:
const { data, refetch } = useNullResults();
useEffect(() => {
// Refetch every 30 seconds
const interval = setInterval(refetch, 30000);
return () => clearInterval(interval);
}, [refetch]);
Next Steps
PDA Derivation
Learn how to derive Program Derived Addresses for accounts
Types
Explore TypeScript interfaces for type-safe development