Skip to main content

Overview

NullGraph uses Anchor’s testing framework with TypeScript and Mocha to verify the full protocol lifecycle. The test suite covers protocol initialization, NKA submission, bounty creation, submission matching, approval, and closure.

Running Tests

Run tests against a local Solana validator that starts and stops automatically:
anchor test
This command:
  1. Starts a local Solana validator
  2. Deploys the program
  3. Runs all tests in tests/nullgraph.ts
  4. Stops the validator after completion
The local validator provides a clean, isolated testing environment and is the recommended approach for development.

Existing Devnet Deployment

Run tests against an existing devnet deployment (skips local validator):
anchor test --skip-local-validator --provider.cluster devnet
Testing against devnet requires devnet SOL and will execute real transactions on the devnet cluster.

Test Suite Coverage

The test suite in tests/nullgraph.ts covers the complete protocol lifecycle:

Phase 1: Protocol Initialization

it("initialize_protocol creates ProtocolState with correct values", async () => {
  const tx = await program.methods
    .initializeProtocol(250) // 2.5% fee
    .accounts({
      authority: authority.publicKey,
      protocolState: protocolStatePDA,
      treasury: treasuryKeypair.publicKey,
      systemProgram: SystemProgram.programId,
    })
    .rpc();

  const state = await program.account.protocolState.fetch(protocolStatePDA);
  assert.ok(state.authority.equals(authority.publicKey));
  assert.equal(state.nkaCounter.toNumber(), 0);
  assert.equal(state.bountyCounter.toNumber(), 0);
  assert.equal(state.feeBasisPoints, 250);
});
Verifies:
  • ProtocolState singleton created with correct authority
  • Counters initialized at zero
  • Fee rate set to 250 basis points (2.5%)
  • Treasury address configured
  • Duplicate initialization rejected (PDA already exists)

Phase 2: Null Knowledge Asset Submission

it("submit_null_result creates NKA with correct fields", async () => {
  const hypothesis = encodeString("Compound X inhibits enzyme Y", 128);
  const methodology = encodeString("Double-blind RCT, n=200", 128);
  const pValue = 8700; // 0.8700
  const sampleSize = 200;

  await program.methods
    .submitNullResult(
      hypothesis,
      methodology,
      expectedOutcome,
      actualOutcome,
      pValue,
      sampleSize,
      dataHash
    )
    .accounts({ /* ... */ })
    .rpc();

  const nr = await program.account.nullResult.fetch(firstNullResultPDA);
  assert.equal(nr.specimenNumber.toNumber(), 1);
  assert.equal(nr.pValue, 8700);
  assert.equal(nr.status, 0); // Pending
});
Verifies:
  • NullResult PDA created with all metadata fields
  • Auto-incrementing specimen number (NKA-0001, NKA-0002, etc.)
  • Global nka_counter increments correctly
  • Multiple researchers can submit independently
  • Data hash stored correctly (SHA-256 fingerprint)

Phase 3: Bounty Marketplace

1

Bounty Creation + Escrow

it("create_bounty creates bounty + vault, escrows USDC", async () => {
  const rewardAmount = 1_000_000; // 1 USDC (6 decimals)
  
  await program.methods
    .createBounty(
      encodeString("Looking for CRISPR efficiency null result", 256),
      new anchor.BN(rewardAmount),
      new anchor.BN(deadline)
    )
    .accounts({ /* ... */ })
    .rpc();

  // Verify bounty
  const bounty = await program.account.nullBounty.fetch(bountyPDA);
  assert.equal(bounty.status, 0); // Open
  
  // Verify vault holds USDC
  const vaultAccount = await getAccount(provider.connection, vaultPDA);
  assert.equal(Number(vaultAccount.amount), rewardAmount);
});
Verifies: Bounty PDA and vault created, BIO tokens escrowed, counter increments
2

Bounty Submission

it("submit_to_bounty creates submission and sets bounty to Matched", async () => {
  await program.methods
    .submitToBounty()
    .accounts({
      researcher: authority.publicKey,
      nullResult: firstNullResultPDA,
      bounty: bountyPDA,
      submission: submissionPDA,
    })
    .rpc();

  const submission = await program.account.bountySubmission.fetch(submissionPDA);
  assert.equal(submission.status, 0); // Pending
  
  const bounty = await program.account.nullBounty.fetch(bountyPDA);
  assert.equal(bounty.status, 1); // Matched
});
Verifies: BountySubmission PDA created, bounty transitions to Matched
3

Invalid Submission Rejection

it("submit_to_bounty fails if bounty not Open", async () => {
  try {
    await program.methods
      .submitToBounty()
      .accounts({ /* second submission */ })
      .rpc();
    assert.fail("Should have thrown");
  } catch (err: any) {
    expect(err.toString()).to.include("InvalidBountyStatus");
  }
});
Verifies: Cannot submit to already-matched bounty (status guard)
4

Approval + Payout

it("approve_bounty_submission transfers correct payout + fee", async () => {
  await program.methods
    .approveBountySubmission()
    .accounts({ /* ... */ })
    .rpc();

  // Verify 2.5% fee = 25,000; payout = 975,000
  const fee = Math.floor((rewardAmount * 250) / 10000);
  const payout = rewardAmount - fee;
  
  const treasuryAccount = await getAccount(provider.connection, treasuryUsdcAta);
  assert.equal(Number(treasuryAccount.amount), fee);
  
  const vaultAccount = await getAccount(provider.connection, vaultPDA);
  assert.equal(Number(vaultAccount.amount), 0); // Vault emptied
});
Verifies: 97.5% to researcher, 2.5% to treasury, vault emptied, statuses updated
5

Bounty Closure + Refund

it("close_bounty refunds full amount to creator", async () => {
  await program.methods
    .closeBounty()
    .accounts({ /* ... */ })
    .rpc();

  const bounty = await program.account.nullBounty.fetch(bounty2PDA);
  assert.equal(bounty.status, 3); // Closed
  
  // Verify full refund
  assert.equal(
    Number(creatorAfter.amount) - Number(creatorBefore.amount),
    500_000
  );
});
Verifies: Full vault balance returned to creator, bounty status becomes Closed
6

Close Fulfilled Bounty Rejection

it("close_bounty fails if already Fulfilled", async () => {
  try {
    await program.methods
      .closeBounty()
      .accounts({ bounty: bountyPDA /* Fulfilled */ })
      .rpc();
    assert.fail("Should have thrown");
  } catch (err: any) {
    expect(err.toString()).to.include("InvalidBountyStatus");
  }
});
Verifies: Cannot close already-fulfilled bounty (status guard)

Test Coverage Summary

Test CategoryTestsCoverage
Protocol Initialization2Singleton creation, duplicate init rejection
NKA Submission2PDA creation, multiple researchers, counter increments
Bounty Creation1Bounty + vault creation, BIO escrow
Bounty Submission2Submission PDA creation, invalid status rejection
Bounty Approval1Payout calculation, fee distribution, vault CPI
Bounty Closure2Refund mechanics, fulfilled bounty rejection
Total10Full protocol lifecycle

Test Data Setup

The test suite uses mock SPL tokens to simulate BIO token transfers:
// Create a mock USDC/BIO mint with 6 decimals
usdcMint = await createMint(
  provider.connection,
  (authority as any).payer,
  authority.publicKey,
  null,
  6 // 6 decimals like USDC/BIO
);

// Mint tokens to creator's ATA
await mintTo(
  provider.connection,
  (authority as any).payer,
  usdcMint,
  creatorUsdcAta,
  authority.publicKey,
  10_000_000 // 10 USDC
);
All token operations use the SPL Token Interface (@solana/spl-token), ensuring compatibility with any SPL-compliant token including BIO.

Running Specific Tests

To run a specific test by name:
npm test -- --grep "protocol initialization"
To run tests with verbose output:
npm test -- --reporter spec

Test Scripts

The root package.json defines test scripts:
{
  "scripts": {
    "test": "ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts",
    "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check",
    "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w"
  }
}
  • npm test - Run all tests with 1000-second timeout
  • npm run lint - Check code formatting
  • npm run lint:fix - Auto-fix formatting issues

Next Steps

Build docs developers (and LLMs) love