Skip to main content

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
loading
boolean
True while fetching data from the blockchain
error
string | null
Error message if the fetch failed
refetch
() => Promise<void>
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

data
NullResultWithKey[]
Array of null result accounts, each with a publicKey field
loading
boolean
True while fetching data
error
string | null
Error message if fetch failed
refetch
() => Promise<void>
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

bountyKey
PublicKey
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

hypothesis
number[]
required
UTF-8 encoded hypothesis string as byte array
methodology
number[]
required
UTF-8 encoded methodology description as byte array
expectedOutcome
number[]
required
UTF-8 encoded expected outcome as byte array
actualOutcome
number[]
required
UTF-8 encoded actual outcome as byte array
pValue
number
required
Statistical p-value (0.0 to 1.0)
sampleSize
number
required
Number of samples in the study
dataHash
number[]
required
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

Build docs developers (and LLMs) love