Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ target
node_modules
test-ledger
.surfpool
web/client
web/client
.env
22 changes: 13 additions & 9 deletions programs/samizdat/src/instructions/register_publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct RegisterPublisher<'info> {
#[account(
init,
init_if_needed,
payer = authority,
space = 8 + PublisherAccount::INIT_SPACE,
seeds = [PUBLISHER_SEED, authority.key().as_ref()],
Expand All @@ -19,13 +19,17 @@ pub struct RegisterPublisher<'info> {
}

pub fn process_register_publisher(ctx: Context<RegisterPublisher>) -> Result<()> {
ctx.accounts.publisher_account.set_inner(PublisherAccount {
authority: ctx.accounts.authority.key(),
total_campaigns: 0,
total_spent: 0,
registered_at: Clock::get()?.unix_timestamp,
status: PublisherStatus::Active,
bump: ctx.bumps.publisher_account,
});
// Initialize only on first registration, keep existing state on reruns.
if ctx.accounts.publisher_account.authority == Pubkey::default() {
ctx.accounts.publisher_account.set_inner(PublisherAccount {
authority: ctx.accounts.authority.key(),
total_campaigns: 0,
total_spent: 0,
registered_at: Clock::get()?.unix_timestamp,
status: PublisherStatus::Active,
bump: ctx.bumps.publisher_account,
});
}

Ok(())
}
77 changes: 59 additions & 18 deletions tests/samizdat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, test, before } from "node:test";
import assert from "node:assert";
import { connect, type Connection, getPDAAndBump } from "solana-kite";
import { type Address, type TransactionSigner, lamports } from "@solana/kit";
import { readFileSync } from "node:fs";
import { createKeyPairSignerFromBytes } from "@solana/kit";

import {
SAMIZDAT_PROGRAM_ADDRESS,
Expand All @@ -26,12 +28,13 @@ import {
CampaignStatus,
} from "@client/index";

const CAMPAIGN_ID = 1n;
const RUN_SEED = BigInt(Date.now());
const CAMPAIGN_ID = RUN_SEED;
const BOUNTY_PER_PLAY = 100_000n; // lamports
const TOTAL_PLAYS = 10n;
const TAG_MASK = 0n; // no content tags
const CLAIM_COOLDOWN = 0n; // no cooldown for happy-path tests
const NODE_ID = 1n;
const NODE_ID = RUN_SEED;
const CLAIM_NONCE = 1n;
const CID_INDEX = 0;

Expand All @@ -55,6 +58,8 @@ const SAMPLE_TARGET_FILTERS: TargetFiltersArgs = {
requiredLandmarks: [],
};

const CLUSTER = (process.env.CLUSTER ?? "localnet") as "localnet" | "devnet";

describe("Samizdat Program – Happy Path", () => {
let connection: Connection;
let publisher: TransactionSigner;
Expand All @@ -66,12 +71,35 @@ describe("Samizdat Program – Happy Path", () => {
let playRecordPDA: Address;
let claimCooldownPDA: Address;

before(async () => {
connection = connect("localnet");
// Snapshot values captured before each section
let initialTotalCampaigns: bigint;
let initialTotalSpent: bigint;

const [maybePublisher, maybeOperator] = await connection.createWallets(2);
publisher = maybePublisher!;
operator = maybeOperator!;
before(async () => {
connection = connect(CLUSTER);

if (CLUSTER === "localnet") {
const [pub, op] = await connection.createWallets(2);
publisher = pub!;
operator = op!;
} else {
const getBytes = (envVar: string | undefined, filePath: string) => {
const raw = envVar
? JSON.parse(envVar)
: JSON.parse(readFileSync(filePath, "utf-8"));
return new Uint8Array(raw);
};

const publisherPath = `${process.env.HOME}/.config/solana/devnet-test.json`;
const operatorPath = `${process.env.HOME}/.config/solana/devnet-operator.json`;

publisher = await createKeyPairSignerFromBytes(
getBytes(process.env.KEYPAIR_ONE, publisherPath),
);
operator = await createKeyPairSignerFromBytes(
getBytes(process.env.KEYPAIR_TWO, operatorPath),
);
}

({ pda: publisherAccountPDA } = await getPDAAndBump(
SAMIZDAT_PROGRAM_ADDRESS,
Expand Down Expand Up @@ -111,27 +139,37 @@ describe("Samizdat Program – Happy Path", () => {
playRecordPDA,
claimCooldownPDA,
});
});

describe("Publisher Registration", () => {
test("registers a publisher account", async () => {
// Register publisher if not already registered (idempotent)
try {
const ix = await getRegisterPublisherInstructionAsync({
authority: publisher,
});

await connection.sendTransactionFromInstructions({
feePayer: publisher,
instructions: [ix],
});
} catch (error) {
throw error;
}

// Snapshot current publisher state
const pubAccount = await fetchPublisherAccount(
connection.rpc,
publisherAccountPDA,
);
initialTotalCampaigns = pubAccount.data.totalCampaigns;
initialTotalSpent = pubAccount.data.totalSpent;
});

describe("Publisher Registration", () => {
test("registers a publisher account", async () => {
// Already registered in before() hook — just verify it exists
const account = await fetchPublisherAccount(
connection.rpc,
publisherAccountPDA,
);

assert.strictEqual(account.data.authority, publisher.address);
assert.strictEqual(account.data.totalCampaigns, 0n);
assert.strictEqual(account.data.totalSpent, 0n);
});
});

Expand Down Expand Up @@ -173,7 +211,7 @@ describe("Samizdat Program – Happy Path", () => {
connection.rpc,
publisherAccountPDA,
);
assert.strictEqual(pub.data.totalCampaigns, 1n);
assert.strictEqual(pub.data.totalCampaigns, initialTotalCampaigns + 1n);
});

test("funds the campaign with additional lamports", async () => {
Expand Down Expand Up @@ -325,7 +363,7 @@ describe("Samizdat Program – Happy Path", () => {
});
});

describe("Play Cycle Claim & Confirm", () => {
describe("Play Cycle - Claim & Confirm", () => {
test("operator claims a campaign", async () => {
const campaignBefore = await fetchCampaignAccount(
connection.rpc,
Expand Down Expand Up @@ -423,7 +461,10 @@ describe("Samizdat Program – Happy Path", () => {
connection.rpc,
publisherAccountPDA,
);
assert.strictEqual(pub.data.totalSpent, BOUNTY_PER_PLAY);
assert.strictEqual(
pub.data.totalSpent,
initialTotalSpent + BOUNTY_PER_PLAY,
);
});
});

Expand Down Expand Up @@ -473,7 +514,7 @@ describe("Samizdat Program – Happy Path", () => {
});

describe("Close Campaign", () => {
const CLOSE_CAMPAIGN_ID = 99n;
const CLOSE_CAMPAIGN_ID = RUN_SEED + 1000n;
let closeCampaignPDA: Address;

before(async () => {
Expand Down
6 changes: 5 additions & 1 deletion web/src/pages/publisher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,11 @@ function CreateCampaignModal({
await pollUntilReady(
async () => {
const campaigns = await service.getCampaigns();
const found = campaigns.find(c => Number(c.campaignId) === data.campaignId && c.publisherAccount === selectedWalletAccount?.address);
const found = campaigns.find(
c =>
Number(c.campaignId) === data.campaignId &&
c.publisherAccount === selectedWalletAccount?.address
);
if (!found) throw new Error('Campaign not found');
return found;
},
Expand Down
8 changes: 7 additions & 1 deletion web/src/pages/twin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,13 @@ function DigitalTwinContent() {
service.getCampaigns(),
]);
setNode(n);
setAvailableCampaigns(camps.filter(c => c.status === CampaignStatus.Active && c.publisherAccount !== selectedWalletAccount?.address));
setAvailableCampaigns(
camps.filter(
c =>
c.status === CampaignStatus.Active &&
c.publisherAccount !== selectedWalletAccount?.address
)
);
} catch (err) {
console.error('Failed to load twin data:', err);
} finally {
Expand Down