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
Local Validator (Recommended)
Run tests against a local Solana validator that starts and stops automatically:
This command:
Starts a local Solana validator
Deploys the program
Runs all tests in tests/nullgraph.ts
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
Protocol Initialization
Double-Init Rejection
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
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
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
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)
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
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
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 Category Tests Coverage Protocol Initialization 2 Singleton creation, duplicate init rejection NKA Submission 2 PDA creation, multiple researchers, counter increments Bounty Creation 1 Bounty + vault creation, BIO escrow Bounty Submission 2 Submission PDA creation, invalid status rejection Bounty Approval 1 Payout calculation, fee distribution, vault CPI Bounty Closure 2 Refund mechanics, fulfilled bounty rejection Total 10 Full 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