From e9d553c1003536a0f85cb0164a2be2be6cc93f84 Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 4 Mar 2026 12:37:15 +0530 Subject: [PATCH 1/9] tests: update tests to run on devnet with pre-funded wallets Signed-off-by: Ayush --- tests/samizdat.test.ts | 969 ++++++++++++++++++++--------------------- 1 file changed, 478 insertions(+), 491 deletions(-) diff --git a/tests/samizdat.test.ts b/tests/samizdat.test.ts index af02956..2a81a80 100644 --- a/tests/samizdat.test.ts +++ b/tests/samizdat.test.ts @@ -2,532 +2,519 @@ 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, - getRegisterPublisherInstructionAsync, - getCreateCampaignInstructionAsync, - getFundCampaignInstructionAsync, - getUpdateCampaignInstructionAsync, - getAddCidsToCampaignInstructionAsync, - getCloseCampaignInstructionAsync, - getRegisterNodeInstructionAsync, - getUpdateNodeMetadataInstruction, - getClaimCampaignInstructionAsync, - getConfirmPlayInstruction, - fetchPublisherAccount, - fetchCampaignAccount, - fetchNodeAccount, - fetchPlayRecord, - fetchClaimCooldown, - ScreenSize, - PlayStatus, - type TargetFiltersArgs, - CampaignStatus, + SAMIZDAT_PROGRAM_ADDRESS, + getRegisterPublisherInstructionAsync, + getCreateCampaignInstructionAsync, + getFundCampaignInstructionAsync, + getUpdateCampaignInstructionAsync, + getAddCidsToCampaignInstructionAsync, + getCloseCampaignInstructionAsync, + getRegisterNodeInstructionAsync, + getUpdateNodeMetadataInstruction, + getClaimCampaignInstructionAsync, + getConfirmPlayInstruction, + fetchPublisherAccount, + fetchCampaignAccount, + fetchNodeAccount, + fetchPlayRecord, + fetchClaimCooldown, + ScreenSize, + PlayStatus, + type TargetFiltersArgs, + 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; -const SAMPLE_CIDS = [ - "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", -]; +const SAMPLE_CIDS = ["bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"]; const SAMPLE_LOCATION = { - latitude: 407_128_000n, // 40.7128°N (NYC) × 1e7 - longitude: -740_060_000n, // -74.0060°W × 1e7 + latitude: 407_128_000n, // 40.7128°N (NYC) × 1e7 + longitude: -740_060_000n, // -74.0060°W × 1e7 }; const SAMPLE_RESOLUTION = { width: 1920, height: 1080 }; const SAMPLE_TARGET_FILTERS: TargetFiltersArgs = { - minFootfall: null, - maxFootfall: null, - screenSizes: [], - geoBounds: null, - establishmentTypes: [], - requiredLandmarks: [], + minFootfall: null, + maxFootfall: null, + screenSizes: [], + geoBounds: null, + establishmentTypes: [], + requiredLandmarks: [], }; describe("Samizdat Program – Happy Path", () => { - let connection: Connection; - let publisher: TransactionSigner; - let operator: TransactionSigner; - - let publisherAccountPDA: Address; - let campaignAccountPDA: Address; - let nodeAccountPDA: Address; - let playRecordPDA: Address; - let claimCooldownPDA: Address; - - before(async () => { - connection = connect("localnet"); - - const [maybePublisher, maybeOperator] = await connection.createWallets(2); - publisher = maybePublisher!; - operator = maybeOperator!; - - ({ pda: publisherAccountPDA } = await getPDAAndBump( - SAMIZDAT_PROGRAM_ADDRESS, - ["publisher", publisher.address], - )); - - ({ pda: campaignAccountPDA } = await getPDAAndBump( - SAMIZDAT_PROGRAM_ADDRESS, - ["campaign", publisherAccountPDA, CAMPAIGN_ID], - )); - - ({ pda: nodeAccountPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "node_account", - operator.address, - NODE_ID, - ])); - - ({ pda: playRecordPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "play_record", - campaignAccountPDA, - nodeAccountPDA, - CLAIM_NONCE, - ])); - - ({ pda: claimCooldownPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "cooldown", - campaignAccountPDA, - nodeAccountPDA, - ])); - - console.log("accounts:", { - publisher: publisher.address, - operator: operator.address, - publisherAccountPDA, - campaignAccountPDA, - nodeAccountPDA, - playRecordPDA, - claimCooldownPDA, - }); - }); - - describe("Publisher Registration", () => { - test("registers a publisher account", async () => { - const ix = await getRegisterPublisherInstructionAsync({ - authority: publisher, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - 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); - }); - }); - - describe("Campaign Lifecycle", () => { - test("creates a campaign with upfront funding", async () => { - const ix = await getCreateCampaignInstructionAsync({ - authority: publisher, - campaignId: CAMPAIGN_ID, - cids: SAMPLE_CIDS, - bountyPerPlay: BOUNTY_PER_PLAY, - totalPlays: TOTAL_PLAYS, - tagMask: TAG_MASK, - targetFilters: SAMPLE_TARGET_FILTERS, - claimCooldown: CLAIM_COOLDOWN, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const campaign = await fetchCampaignAccount( - connection.rpc, - campaignAccountPDA, - ); - - assert.strictEqual(campaign.data.publisherAccount, publisherAccountPDA); - assert.strictEqual(campaign.data.campaignId, CAMPAIGN_ID); - assert.strictEqual(campaign.data.bountyPerPlay, BOUNTY_PER_PLAY); - assert.strictEqual(campaign.data.playsRemaining, TOTAL_PLAYS); - assert.strictEqual(campaign.data.playsCompleted, 0n); - assert.strictEqual(campaign.data.tagMask, TAG_MASK); - assert.strictEqual(campaign.data.status, CampaignStatus.Active); - assert.strictEqual(campaign.data.claimCooldown, CLAIM_COOLDOWN); - assert.deepStrictEqual(campaign.data.cids, SAMPLE_CIDS); - - // Publisher total_campaigns incremented - const pub = await fetchPublisherAccount( - connection.rpc, - publisherAccountPDA, - ); - assert.strictEqual(pub.data.totalCampaigns, 1n); - }); - - test("funds the campaign with additional lamports", async () => { - const additionalFunding = 500_000n; - - const balanceBefore = await connection.getLamportBalance( - campaignAccountPDA, - "confirmed", - ); - - const ix = await getFundCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - authority: publisher, - amount: additionalFunding, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const balanceAfter = await connection.getLamportBalance( - campaignAccountPDA, - "confirmed", - ); - - assert.strictEqual(balanceAfter - balanceBefore, additionalFunding); - }); + let connection: Connection; + let publisher: TransactionSigner; + let operator: TransactionSigner; - test("pauses the campaign", async () => { - const ix = await getUpdateCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - authority: publisher, - tagMask: null, - targetFilters: null, - status: CampaignStatus.Paused, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const campaign = await fetchCampaignAccount( - connection.rpc, - campaignAccountPDA, - ); - assert.strictEqual(campaign.data.status, CampaignStatus.Paused); - }); + let publisherAccountPDA: Address; + let campaignAccountPDA: Address; + let nodeAccountPDA: Address; + let playRecordPDA: Address; + let claimCooldownPDA: Address; - test("resumes the campaign", async () => { - const ix = await getUpdateCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - authority: publisher, - tagMask: null, - targetFilters: null, - status: CampaignStatus.Active, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const campaign = await fetchCampaignAccount( - connection.rpc, - campaignAccountPDA, - ); - assert.strictEqual(campaign.data.status, CampaignStatus.Active); - }); + // Snapshot values captured before each section + let initialTotalCampaigns: bigint; + let initialTotalSpent: bigint; - test("adds CIDs to the campaign", async () => { - const newCid = - "bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq"; - - const ix = await getAddCidsToCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - authority: publisher, - newCids: [newCid], - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const campaign = await fetchCampaignAccount( - connection.rpc, - campaignAccountPDA, - ); - assert.strictEqual(campaign.data.cids.length, 2); - assert.strictEqual(campaign.data.cids[1], newCid); - }); - }); - - describe("Node Registration", () => { - test("registers a node", async () => { - const ix = await getRegisterNodeInstructionAsync({ - authority: operator, - nodeId: NODE_ID, - location: SAMPLE_LOCATION, - screenSize: ScreenSize.Large, - resolution: SAMPLE_RESOLUTION, - landmarks: ["Times Square"], - blockedTagMask: 0n, - estimatedFootfall: 5000, - establishmentType: "retail", - }); - - await connection.sendTransactionFromInstructions({ - feePayer: operator, - instructions: [ix], - }); - - const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); - - assert.strictEqual(node.data.authority, operator.address); - assert.strictEqual(node.data.nodeId, NODE_ID); - assert.strictEqual(node.data.screenSize, ScreenSize.Large); - assert.strictEqual(node.data.resolution.width, 1920); - assert.strictEqual(node.data.resolution.height, 1080); - assert.strictEqual(node.data.estimatedFootfall, 5000); - assert.strictEqual(node.data.establishmentType, "retail"); - assert.strictEqual(node.data.totalPlays, 0n); - assert.strictEqual(node.data.totalEarnings, 0n); - assert.deepStrictEqual(node.data.landmarks, ["Times Square"]); + before(async () => { + connection = connect("devnet"); + + // Load the pre-funded devnet wallet as the publisher/fee payer + const publisherKeypairBytes = new Uint8Array( + JSON.parse( + readFileSync(`${process.env.HOME}/.config/solana/devnet-test.json`, "utf-8"), + ), + ); + publisher = await createKeyPairSignerFromBytes(publisherKeypairBytes); + + // Load the pre-funded operator wallet + const operatorKeypairBytes = new Uint8Array( + JSON.parse( + readFileSync(`${process.env.HOME}/.config/solana/devnet-operator.json`, "utf-8"), + ), + ); + operator = await createKeyPairSignerFromBytes(operatorKeypairBytes); + + ({ pda: publisherAccountPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "publisher", + publisher.address, + ])); + + ({ pda: campaignAccountPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "campaign", + publisherAccountPDA, + CAMPAIGN_ID, + ])); + + ({ pda: nodeAccountPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "node_account", + operator.address, + NODE_ID, + ])); + + ({ pda: playRecordPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "play_record", + campaignAccountPDA, + nodeAccountPDA, + CLAIM_NONCE, + ])); + + ({ pda: claimCooldownPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "cooldown", + campaignAccountPDA, + nodeAccountPDA, + ])); + + console.log("accounts:", { + publisher: publisher.address, + operator: operator.address, + publisherAccountPDA, + campaignAccountPDA, + nodeAccountPDA, + playRecordPDA, + claimCooldownPDA, + }); + + // Register publisher if not already registered (idempotent) + try { + const ix = await getRegisterPublisherInstructionAsync({ + authority: publisher, + }); + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + } catch { + // Already registered - that's fine on devnet + } + + // Snapshot current publisher state + const pub = await fetchPublisherAccount(connection.rpc, publisherAccountPDA); + initialTotalCampaigns = pub.data.totalCampaigns; + initialTotalSpent = pub.data.totalSpent; }); - test("updates node metadata", async () => { - const ix = getUpdateNodeMetadataInstruction({ - nodeAccount: nodeAccountPDA, - authority: operator, - location: null, - estimatedFootfall: 8000, - blockedTagMask: null, - status: null, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: operator, - instructions: [ix], - }); - - const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); - assert.strictEqual(node.data.estimatedFootfall, 8000); - // Unchanged fields remain the same - assert.strictEqual(node.data.screenSize, ScreenSize.Large); - assert.strictEqual(node.data.establishmentType, "retail"); - }); - }); - - describe("Play Cycle – Claim & Confirm", () => { - test("operator claims a campaign", async () => { - const campaignBefore = await fetchCampaignAccount( - connection.rpc, - campaignAccountPDA, - ); - - const ix = await getClaimCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - nodeAccount: nodeAccountPDA, - authority: operator, - cidIndex: CID_INDEX, - claimNonce: CLAIM_NONCE, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: operator, - instructions: [ix], - }); - - // Verify PlayRecord - const play = await fetchPlayRecord(connection.rpc, playRecordPDA); - assert.strictEqual(play.data.campaignAccount, campaignAccountPDA); - assert.strictEqual(play.data.nodeAccount, nodeAccountPDA); - assert.strictEqual(play.data.nonce, CLAIM_NONCE); - assert.strictEqual(play.data.cidIndex, CID_INDEX); - assert.strictEqual(play.data.status, PlayStatus.Claimed); - assert.strictEqual(play.data.paymentAmount, 0n); - - // Verify ClaimCooldown was created - const cooldown = await fetchClaimCooldown( - connection.rpc, - claimCooldownPDA, - ); - assert.strictEqual(cooldown.data.campaign, campaignAccountPDA); - assert.strictEqual(cooldown.data.node, nodeAccountPDA); - assert.ok(cooldown.data.lastClaimedAt > 0n); - - // Campaign plays_remaining decremented - const campaignAfter = await fetchCampaignAccount( - connection.rpc, - campaignAccountPDA, - ); - assert.strictEqual( - campaignAfter.data.playsRemaining, - campaignBefore.data.playsRemaining - 1n, - ); + 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); + }); }); - test("publisher confirms the play and pays the node", async () => { - const nodeBalanceBefore = await connection.getLamportBalance( - operator.address, - "confirmed", - ); - - const ix = getConfirmPlayInstruction({ - playRecord: playRecordPDA, - campaignAccount: campaignAccountPDA, - publisherAccount: publisherAccountPDA, - nodeAccount: nodeAccountPDA, - authority: operator, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - // PlayRecord transitions to Paid - const play = await fetchPlayRecord(connection.rpc, playRecordPDA); - assert.strictEqual(play.data.status, PlayStatus.Paid); - assert.strictEqual(play.data.paymentAmount, BOUNTY_PER_PLAY); - assert.ok(play.data.confirmedAt > 0n); - - // Campaign plays_completed incremented - const campaign = await fetchCampaignAccount( - connection.rpc, - campaignAccountPDA, - ); - assert.strictEqual(campaign.data.playsCompleted, 1n); - - // Node operator received the bounty - const nodeBalanceAfter = await connection.getLamportBalance( - operator.address, - "confirmed", - ); - assert.ok(nodeBalanceAfter > nodeBalanceBefore); - - // Node aggregate stats updated - const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); - assert.strictEqual(node.data.totalPlays, 1n); - assert.strictEqual(node.data.totalEarnings, BOUNTY_PER_PLAY); - - // Publisher total_spent updated - const pub = await fetchPublisherAccount( - connection.rpc, - publisherAccountPDA, - ); - assert.strictEqual(pub.data.totalSpent, BOUNTY_PER_PLAY); + describe("Campaign Lifecycle", () => { + test("creates a campaign with upfront funding", async () => { + const ix = await getCreateCampaignInstructionAsync({ + authority: publisher, + campaignId: CAMPAIGN_ID, + cids: SAMPLE_CIDS, + bountyPerPlay: BOUNTY_PER_PLAY, + totalPlays: TOTAL_PLAYS, + tagMask: TAG_MASK, + targetFilters: SAMPLE_TARGET_FILTERS, + claimCooldown: CLAIM_COOLDOWN, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); + + assert.strictEqual(campaign.data.publisherAccount, publisherAccountPDA); + assert.strictEqual(campaign.data.campaignId, CAMPAIGN_ID); + assert.strictEqual(campaign.data.bountyPerPlay, BOUNTY_PER_PLAY); + assert.strictEqual(campaign.data.playsRemaining, TOTAL_PLAYS); + assert.strictEqual(campaign.data.playsCompleted, 0n); + assert.strictEqual(campaign.data.tagMask, TAG_MASK); + assert.strictEqual(campaign.data.status, CampaignStatus.Active); + assert.strictEqual(campaign.data.claimCooldown, CLAIM_COOLDOWN); + assert.deepStrictEqual(campaign.data.cids, SAMPLE_CIDS); + + // Publisher total_campaigns incremented + const pub = await fetchPublisherAccount(connection.rpc, publisherAccountPDA); + assert.strictEqual(pub.data.totalCampaigns, initialTotalCampaigns + 1n); + }); + + test("funds the campaign with additional lamports", async () => { + const additionalFunding = 500_000n; + + const balanceBefore = await connection.getLamportBalance( + campaignAccountPDA, + "confirmed", + ); + + const ix = await getFundCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + authority: publisher, + amount: additionalFunding, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const balanceAfter = await connection.getLamportBalance( + campaignAccountPDA, + "confirmed", + ); + + assert.strictEqual(balanceAfter - balanceBefore, additionalFunding); + }); + + test("pauses the campaign", async () => { + const ix = await getUpdateCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + authority: publisher, + tagMask: null, + targetFilters: null, + status: CampaignStatus.Paused, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); + assert.strictEqual(campaign.data.status, CampaignStatus.Paused); + }); + + test("resumes the campaign", async () => { + const ix = await getUpdateCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + authority: publisher, + tagMask: null, + targetFilters: null, + status: CampaignStatus.Active, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); + assert.strictEqual(campaign.data.status, CampaignStatus.Active); + }); + + test("adds CIDs to the campaign", async () => { + const newCid = "bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq"; + + const ix = await getAddCidsToCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + authority: publisher, + newCids: [newCid], + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); + assert.strictEqual(campaign.data.cids.length, 2); + assert.strictEqual(campaign.data.cids[1], newCid); + }); }); - }); - describe("Timeout Play", () => { - const TIMEOUT_NONCE = 2n; - let timeoutPlayRecordPDA: Address; - - before(async () => { - ({ pda: timeoutPlayRecordPDA } = await getPDAAndBump( - SAMIZDAT_PROGRAM_ADDRESS, - ["play_record", campaignAccountPDA, nodeAccountPDA, TIMEOUT_NONCE], - )); - - // Claim a second play that we'll attempt to time out - const ix = await getClaimCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - nodeAccount: nodeAccountPDA, - authority: operator, - cidIndex: CID_INDEX, - claimNonce: TIMEOUT_NONCE, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: operator, - instructions: [ix], - }); + describe("Node Registration", () => { + test("registers a node", async () => { + const ix = await getRegisterNodeInstructionAsync({ + authority: operator, + nodeId: NODE_ID, + location: SAMPLE_LOCATION, + screenSize: ScreenSize.Large, + resolution: SAMPLE_RESOLUTION, + landmarks: ["Times Square"], + blockedTagMask: 0n, + estimatedFootfall: 5000, + establishmentType: "retail", + }); + + await connection.sendTransactionFromInstructions({ + feePayer: operator, + instructions: [ix], + }); + + const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); + + assert.strictEqual(node.data.authority, operator.address); + assert.strictEqual(node.data.nodeId, NODE_ID); + assert.strictEqual(node.data.screenSize, ScreenSize.Large); + assert.strictEqual(node.data.resolution.width, 1920); + assert.strictEqual(node.data.resolution.height, 1080); + assert.strictEqual(node.data.estimatedFootfall, 5000); + assert.strictEqual(node.data.establishmentType, "retail"); + assert.strictEqual(node.data.totalPlays, 0n); + assert.strictEqual(node.data.totalEarnings, 0n); + assert.deepStrictEqual(node.data.landmarks, ["Times Square"]); + }); + + test("updates node metadata", async () => { + const ix = getUpdateNodeMetadataInstruction({ + nodeAccount: nodeAccountPDA, + authority: operator, + location: null, + estimatedFootfall: 8000, + blockedTagMask: null, + status: null, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: operator, + instructions: [ix], + }); + + const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); + assert.strictEqual(node.data.estimatedFootfall, 8000); + // Unchanged fields remain the same + assert.strictEqual(node.data.screenSize, ScreenSize.Large); + assert.strictEqual(node.data.establishmentType, "retail"); + }); }); - test("verifies the claimed play exists before timeout", async () => { - const play = await fetchPlayRecord(connection.rpc, timeoutPlayRecordPDA); - assert.strictEqual(play.data.status, PlayStatus.Claimed); - assert.strictEqual(play.data.nonce, TIMEOUT_NONCE); - - // plays_remaining should have decremented again - const campaign = await fetchCampaignAccount( - connection.rpc, - campaignAccountPDA, - ); - assert.strictEqual( - campaign.data.playsRemaining, - TOTAL_PLAYS - 2n, // two claims made so far - ); + describe("Play Cycle - Claim & Confirm", () => { + test("operator claims a campaign", async () => { + const campaignBefore = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); + + const ix = await getClaimCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + nodeAccount: nodeAccountPDA, + authority: operator, + cidIndex: CID_INDEX, + claimNonce: CLAIM_NONCE, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: operator, + instructions: [ix], + }); + + // Verify PlayRecord + const play = await fetchPlayRecord(connection.rpc, playRecordPDA); + assert.strictEqual(play.data.campaignAccount, campaignAccountPDA); + assert.strictEqual(play.data.nodeAccount, nodeAccountPDA); + assert.strictEqual(play.data.nonce, CLAIM_NONCE); + assert.strictEqual(play.data.cidIndex, CID_INDEX); + assert.strictEqual(play.data.status, PlayStatus.Claimed); + assert.strictEqual(play.data.paymentAmount, 0n); + + // Verify ClaimCooldown was created + const cooldown = await fetchClaimCooldown(connection.rpc, claimCooldownPDA); + assert.strictEqual(cooldown.data.campaign, campaignAccountPDA); + assert.strictEqual(cooldown.data.node, nodeAccountPDA); + assert.ok(cooldown.data.lastClaimedAt > 0n); + + // Campaign plays_remaining decremented + const campaignAfter = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); + assert.strictEqual( + campaignAfter.data.playsRemaining, + campaignBefore.data.playsRemaining - 1n, + ); + }); + + test("publisher confirms the play and pays the node", async () => { + const nodeBalanceBefore = await connection.getLamportBalance( + operator.address, + "confirmed", + ); + + const ix = getConfirmPlayInstruction({ + playRecord: playRecordPDA, + campaignAccount: campaignAccountPDA, + publisherAccount: publisherAccountPDA, + nodeAccount: nodeAccountPDA, + authority: operator, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + // PlayRecord transitions to Paid + const play = await fetchPlayRecord(connection.rpc, playRecordPDA); + assert.strictEqual(play.data.status, PlayStatus.Paid); + assert.strictEqual(play.data.paymentAmount, BOUNTY_PER_PLAY); + assert.ok(play.data.confirmedAt > 0n); + + // Campaign plays_completed incremented + const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); + assert.strictEqual(campaign.data.playsCompleted, 1n); + + // Node operator received the bounty + const nodeBalanceAfter = await connection.getLamportBalance( + operator.address, + "confirmed", + ); + assert.ok(nodeBalanceAfter > nodeBalanceBefore); + + // Node aggregate stats updated + const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); + assert.strictEqual(node.data.totalPlays, 1n); + assert.strictEqual(node.data.totalEarnings, BOUNTY_PER_PLAY); + + // Publisher total_spent updated + const pub = await fetchPublisherAccount(connection.rpc, publisherAccountPDA); + assert.strictEqual(pub.data.totalSpent, initialTotalSpent + BOUNTY_PER_PLAY); + }); }); - // Full timeout testing requires clock warping (bankrun / solana-program-test); - // localnet doesn't support it, so we only assert the play record state here. - }); - - describe("Close Campaign", () => { - const CLOSE_CAMPAIGN_ID = 99n; - let closeCampaignPDA: Address; - - before(async () => { - ({ pda: closeCampaignPDA } = await getPDAAndBump( - SAMIZDAT_PROGRAM_ADDRESS, - ["campaign", publisherAccountPDA, CLOSE_CAMPAIGN_ID], - )); - - // Create a small campaign specifically for closing - const createIx = await getCreateCampaignInstructionAsync({ - authority: publisher, - campaignId: CLOSE_CAMPAIGN_ID, - cids: SAMPLE_CIDS, - bountyPerPlay: 1_000n, - totalPlays: 1n, - tagMask: 0n, - targetFilters: SAMPLE_TARGET_FILTERS, - claimCooldown: 0n, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [createIx], - }); + describe("Timeout Play", () => { + const TIMEOUT_NONCE = 2n; + let timeoutPlayRecordPDA: Address; + + before(async () => { + ({ pda: timeoutPlayRecordPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "play_record", + campaignAccountPDA, + nodeAccountPDA, + TIMEOUT_NONCE, + ])); + + // Claim a second play that we'll attempt to time out + const ix = await getClaimCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + nodeAccount: nodeAccountPDA, + authority: operator, + cidIndex: CID_INDEX, + claimNonce: TIMEOUT_NONCE, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: operator, + instructions: [ix], + }); + }); + + test("verifies the claimed play exists before timeout", async () => { + const play = await fetchPlayRecord(connection.rpc, timeoutPlayRecordPDA); + assert.strictEqual(play.data.status, PlayStatus.Claimed); + assert.strictEqual(play.data.nonce, TIMEOUT_NONCE); + + // plays_remaining should have decremented again + const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); + assert.strictEqual( + campaign.data.playsRemaining, + TOTAL_PLAYS - 2n, // two claims made so far + ); + }); + + // Full timeout testing requires clock warping (bankrun / solana-program-test); + // localnet doesn't support it, so we only assert the play record state here. }); - test("closes a campaign and reclaims rent + remaining funds", async () => { - const publisherBalanceBefore = await connection.getLamportBalance( - publisher.address, - ); - - const ix = await getCloseCampaignInstructionAsync({ - campaignAccount: closeCampaignPDA, - authority: publisher, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - // Campaign account should be closed (zero lamports) - const balance = await connection.getLamportBalance(closeCampaignPDA); - assert.strictEqual(balance, 0n); - - // Publisher received rent back (minus tx fee) - const publisherBalanceAfter = await connection.getLamportBalance( - publisher.address, - ); - // Net gain should be positive once we account for a small tx fee - assert.ok( - publisherBalanceAfter > publisherBalanceBefore - 100_000n, - "Publisher should have reclaimed rent minus tx fee", - ); + describe("Close Campaign", () => { + const CLOSE_CAMPAIGN_ID = RUN_SEED + 1000n; + let closeCampaignPDA: Address; + + before(async () => { + ({ pda: closeCampaignPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "campaign", + publisherAccountPDA, + CLOSE_CAMPAIGN_ID, + ])); + + // Create a small campaign specifically for closing + const createIx = await getCreateCampaignInstructionAsync({ + authority: publisher, + campaignId: CLOSE_CAMPAIGN_ID, + cids: SAMPLE_CIDS, + bountyPerPlay: 1_000n, + totalPlays: 1n, + tagMask: 0n, + targetFilters: SAMPLE_TARGET_FILTERS, + claimCooldown: 0n, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [createIx], + }); + }); + + test("closes a campaign and reclaims rent + remaining funds", async () => { + const publisherBalanceBefore = await connection.getLamportBalance(publisher.address); + + const ix = await getCloseCampaignInstructionAsync({ + campaignAccount: closeCampaignPDA, + authority: publisher, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + // Campaign account should be closed (zero lamports) + const balance = await connection.getLamportBalance(closeCampaignPDA); + assert.strictEqual(balance, 0n); + + // Publisher received rent back (minus tx fee) + const publisherBalanceAfter = await connection.getLamportBalance(publisher.address); + // Net gain should be positive once we account for a small tx fee + assert.ok( + publisherBalanceAfter > publisherBalanceBefore - 100_000n, + "Publisher should have reclaimed rent minus tx fee", + ); + }); }); - }); }); From b2d320afb2796d79d63bf4b2e515aeaac2848be3 Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 4 Mar 2026 12:45:57 +0530 Subject: [PATCH 2/9] fix: bun format Signed-off-by: Ayush --- tests/samizdat.test.ts | 997 ++++++++++++++++++++++------------------- 1 file changed, 524 insertions(+), 473 deletions(-) diff --git a/tests/samizdat.test.ts b/tests/samizdat.test.ts index 2a81a80..4c4bfaf 100644 --- a/tests/samizdat.test.ts +++ b/tests/samizdat.test.ts @@ -6,26 +6,26 @@ import { readFileSync } from "node:fs"; import { createKeyPairSignerFromBytes } from "@solana/kit"; import { - SAMIZDAT_PROGRAM_ADDRESS, - getRegisterPublisherInstructionAsync, - getCreateCampaignInstructionAsync, - getFundCampaignInstructionAsync, - getUpdateCampaignInstructionAsync, - getAddCidsToCampaignInstructionAsync, - getCloseCampaignInstructionAsync, - getRegisterNodeInstructionAsync, - getUpdateNodeMetadataInstruction, - getClaimCampaignInstructionAsync, - getConfirmPlayInstruction, - fetchPublisherAccount, - fetchCampaignAccount, - fetchNodeAccount, - fetchPlayRecord, - fetchClaimCooldown, - ScreenSize, - PlayStatus, - type TargetFiltersArgs, - CampaignStatus, + SAMIZDAT_PROGRAM_ADDRESS, + getRegisterPublisherInstructionAsync, + getCreateCampaignInstructionAsync, + getFundCampaignInstructionAsync, + getUpdateCampaignInstructionAsync, + getAddCidsToCampaignInstructionAsync, + getCloseCampaignInstructionAsync, + getRegisterNodeInstructionAsync, + getUpdateNodeMetadataInstruction, + getClaimCampaignInstructionAsync, + getConfirmPlayInstruction, + fetchPublisherAccount, + fetchCampaignAccount, + fetchNodeAccount, + fetchPlayRecord, + fetchClaimCooldown, + ScreenSize, + PlayStatus, + type TargetFiltersArgs, + CampaignStatus, } from "@client/index"; const RUN_SEED = BigInt(Date.now()); @@ -38,483 +38,534 @@ const NODE_ID = RUN_SEED; const CLAIM_NONCE = 1n; const CID_INDEX = 0; -const SAMPLE_CIDS = ["bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"]; +const SAMPLE_CIDS = [ + "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", +]; const SAMPLE_LOCATION = { - latitude: 407_128_000n, // 40.7128°N (NYC) × 1e7 - longitude: -740_060_000n, // -74.0060°W × 1e7 + latitude: 407_128_000n, // 40.7128°N (NYC) × 1e7 + longitude: -740_060_000n, // -74.0060°W × 1e7 }; const SAMPLE_RESOLUTION = { width: 1920, height: 1080 }; const SAMPLE_TARGET_FILTERS: TargetFiltersArgs = { - minFootfall: null, - maxFootfall: null, - screenSizes: [], - geoBounds: null, - establishmentTypes: [], - requiredLandmarks: [], + minFootfall: null, + maxFootfall: null, + screenSizes: [], + geoBounds: null, + establishmentTypes: [], + requiredLandmarks: [], }; describe("Samizdat Program – Happy Path", () => { - let connection: Connection; - let publisher: TransactionSigner; - let operator: TransactionSigner; + let connection: Connection; + let publisher: TransactionSigner; + let operator: TransactionSigner; + + let publisherAccountPDA: Address; + let campaignAccountPDA: Address; + let nodeAccountPDA: Address; + let playRecordPDA: Address; + let claimCooldownPDA: Address; + + // Snapshot values captured before each section + let initialTotalCampaigns: bigint; + let initialTotalSpent: bigint; + + before(async () => { + connection = connect("devnet"); + + // Load the pre-funded devnet wallet as the publisher/fee payer + const publisherKeypairBytes = new Uint8Array( + JSON.parse( + readFileSync( + `${process.env.HOME}/.config/solana/devnet-test.json`, + "utf-8", + ), + ), + ); + publisher = await createKeyPairSignerFromBytes(publisherKeypairBytes); + + // Load the pre-funded operator wallet + const operatorKeypairBytes = new Uint8Array( + JSON.parse( + readFileSync( + `${process.env.HOME}/.config/solana/devnet-operator.json`, + "utf-8", + ), + ), + ); + operator = await createKeyPairSignerFromBytes(operatorKeypairBytes); + + ({ pda: publisherAccountPDA } = await getPDAAndBump( + SAMIZDAT_PROGRAM_ADDRESS, + ["publisher", publisher.address], + )); + + ({ pda: campaignAccountPDA } = await getPDAAndBump( + SAMIZDAT_PROGRAM_ADDRESS, + ["campaign", publisherAccountPDA, CAMPAIGN_ID], + )); + + ({ pda: nodeAccountPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "node_account", + operator.address, + NODE_ID, + ])); + + ({ pda: playRecordPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "play_record", + campaignAccountPDA, + nodeAccountPDA, + CLAIM_NONCE, + ])); + + ({ pda: claimCooldownPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ + "cooldown", + campaignAccountPDA, + nodeAccountPDA, + ])); + + console.log("accounts:", { + publisher: publisher.address, + operator: operator.address, + publisherAccountPDA, + campaignAccountPDA, + nodeAccountPDA, + playRecordPDA, + claimCooldownPDA, + }); + + // Register publisher if not already registered (idempotent) + try { + const ix = await getRegisterPublisherInstructionAsync({ + authority: publisher, + }); + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + } catch { + // Already registered - that's fine on devnet + } + + // Snapshot current publisher state + const pub = await fetchPublisherAccount( + connection.rpc, + publisherAccountPDA, + ); + initialTotalCampaigns = pub.data.totalCampaigns; + initialTotalSpent = pub.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); + }); + }); + + describe("Campaign Lifecycle", () => { + test("creates a campaign with upfront funding", async () => { + const ix = await getCreateCampaignInstructionAsync({ + authority: publisher, + campaignId: CAMPAIGN_ID, + cids: SAMPLE_CIDS, + bountyPerPlay: BOUNTY_PER_PLAY, + totalPlays: TOTAL_PLAYS, + tagMask: TAG_MASK, + targetFilters: SAMPLE_TARGET_FILTERS, + claimCooldown: CLAIM_COOLDOWN, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const campaign = await fetchCampaignAccount( + connection.rpc, + campaignAccountPDA, + ); + + assert.strictEqual(campaign.data.publisherAccount, publisherAccountPDA); + assert.strictEqual(campaign.data.campaignId, CAMPAIGN_ID); + assert.strictEqual(campaign.data.bountyPerPlay, BOUNTY_PER_PLAY); + assert.strictEqual(campaign.data.playsRemaining, TOTAL_PLAYS); + assert.strictEqual(campaign.data.playsCompleted, 0n); + assert.strictEqual(campaign.data.tagMask, TAG_MASK); + assert.strictEqual(campaign.data.status, CampaignStatus.Active); + assert.strictEqual(campaign.data.claimCooldown, CLAIM_COOLDOWN); + assert.deepStrictEqual(campaign.data.cids, SAMPLE_CIDS); + + // Publisher total_campaigns incremented + const pub = await fetchPublisherAccount( + connection.rpc, + publisherAccountPDA, + ); + assert.strictEqual(pub.data.totalCampaigns, initialTotalCampaigns + 1n); + }); - let publisherAccountPDA: Address; - let campaignAccountPDA: Address; - let nodeAccountPDA: Address; - let playRecordPDA: Address; - let claimCooldownPDA: Address; + test("funds the campaign with additional lamports", async () => { + const additionalFunding = 500_000n; - // Snapshot values captured before each section - let initialTotalCampaigns: bigint; - let initialTotalSpent: bigint; + const balanceBefore = await connection.getLamportBalance( + campaignAccountPDA, + "confirmed", + ); - before(async () => { - connection = connect("devnet"); - - // Load the pre-funded devnet wallet as the publisher/fee payer - const publisherKeypairBytes = new Uint8Array( - JSON.parse( - readFileSync(`${process.env.HOME}/.config/solana/devnet-test.json`, "utf-8"), - ), - ); - publisher = await createKeyPairSignerFromBytes(publisherKeypairBytes); - - // Load the pre-funded operator wallet - const operatorKeypairBytes = new Uint8Array( - JSON.parse( - readFileSync(`${process.env.HOME}/.config/solana/devnet-operator.json`, "utf-8"), - ), - ); - operator = await createKeyPairSignerFromBytes(operatorKeypairBytes); - - ({ pda: publisherAccountPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "publisher", - publisher.address, - ])); - - ({ pda: campaignAccountPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "campaign", - publisherAccountPDA, - CAMPAIGN_ID, - ])); - - ({ pda: nodeAccountPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "node_account", - operator.address, - NODE_ID, - ])); - - ({ pda: playRecordPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "play_record", - campaignAccountPDA, - nodeAccountPDA, - CLAIM_NONCE, - ])); - - ({ pda: claimCooldownPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "cooldown", - campaignAccountPDA, - nodeAccountPDA, - ])); - - console.log("accounts:", { - publisher: publisher.address, - operator: operator.address, - publisherAccountPDA, - campaignAccountPDA, - nodeAccountPDA, - playRecordPDA, - claimCooldownPDA, - }); - - // Register publisher if not already registered (idempotent) - try { - const ix = await getRegisterPublisherInstructionAsync({ - authority: publisher, - }); - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - } catch { - // Already registered - that's fine on devnet - } - - // Snapshot current publisher state - const pub = await fetchPublisherAccount(connection.rpc, publisherAccountPDA); - initialTotalCampaigns = pub.data.totalCampaigns; - initialTotalSpent = pub.data.totalSpent; + const ix = await getFundCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + authority: publisher, + amount: additionalFunding, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const balanceAfter = await connection.getLamportBalance( + campaignAccountPDA, + "confirmed", + ); + + assert.strictEqual(balanceAfter - balanceBefore, additionalFunding); }); - 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); - }); + test("pauses the campaign", async () => { + const ix = await getUpdateCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + authority: publisher, + tagMask: null, + targetFilters: null, + status: CampaignStatus.Paused, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const campaign = await fetchCampaignAccount( + connection.rpc, + campaignAccountPDA, + ); + assert.strictEqual(campaign.data.status, CampaignStatus.Paused); }); - describe("Campaign Lifecycle", () => { - test("creates a campaign with upfront funding", async () => { - const ix = await getCreateCampaignInstructionAsync({ - authority: publisher, - campaignId: CAMPAIGN_ID, - cids: SAMPLE_CIDS, - bountyPerPlay: BOUNTY_PER_PLAY, - totalPlays: TOTAL_PLAYS, - tagMask: TAG_MASK, - targetFilters: SAMPLE_TARGET_FILTERS, - claimCooldown: CLAIM_COOLDOWN, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); - - assert.strictEqual(campaign.data.publisherAccount, publisherAccountPDA); - assert.strictEqual(campaign.data.campaignId, CAMPAIGN_ID); - assert.strictEqual(campaign.data.bountyPerPlay, BOUNTY_PER_PLAY); - assert.strictEqual(campaign.data.playsRemaining, TOTAL_PLAYS); - assert.strictEqual(campaign.data.playsCompleted, 0n); - assert.strictEqual(campaign.data.tagMask, TAG_MASK); - assert.strictEqual(campaign.data.status, CampaignStatus.Active); - assert.strictEqual(campaign.data.claimCooldown, CLAIM_COOLDOWN); - assert.deepStrictEqual(campaign.data.cids, SAMPLE_CIDS); - - // Publisher total_campaigns incremented - const pub = await fetchPublisherAccount(connection.rpc, publisherAccountPDA); - assert.strictEqual(pub.data.totalCampaigns, initialTotalCampaigns + 1n); - }); - - test("funds the campaign with additional lamports", async () => { - const additionalFunding = 500_000n; - - const balanceBefore = await connection.getLamportBalance( - campaignAccountPDA, - "confirmed", - ); - - const ix = await getFundCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - authority: publisher, - amount: additionalFunding, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const balanceAfter = await connection.getLamportBalance( - campaignAccountPDA, - "confirmed", - ); - - assert.strictEqual(balanceAfter - balanceBefore, additionalFunding); - }); - - test("pauses the campaign", async () => { - const ix = await getUpdateCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - authority: publisher, - tagMask: null, - targetFilters: null, - status: CampaignStatus.Paused, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); - assert.strictEqual(campaign.data.status, CampaignStatus.Paused); - }); - - test("resumes the campaign", async () => { - const ix = await getUpdateCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - authority: publisher, - tagMask: null, - targetFilters: null, - status: CampaignStatus.Active, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); - assert.strictEqual(campaign.data.status, CampaignStatus.Active); - }); - - test("adds CIDs to the campaign", async () => { - const newCid = "bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq"; - - const ix = await getAddCidsToCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - authority: publisher, - newCids: [newCid], - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); - assert.strictEqual(campaign.data.cids.length, 2); - assert.strictEqual(campaign.data.cids[1], newCid); - }); + test("resumes the campaign", async () => { + const ix = await getUpdateCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + authority: publisher, + tagMask: null, + targetFilters: null, + status: CampaignStatus.Active, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const campaign = await fetchCampaignAccount( + connection.rpc, + campaignAccountPDA, + ); + assert.strictEqual(campaign.data.status, CampaignStatus.Active); }); - describe("Node Registration", () => { - test("registers a node", async () => { - const ix = await getRegisterNodeInstructionAsync({ - authority: operator, - nodeId: NODE_ID, - location: SAMPLE_LOCATION, - screenSize: ScreenSize.Large, - resolution: SAMPLE_RESOLUTION, - landmarks: ["Times Square"], - blockedTagMask: 0n, - estimatedFootfall: 5000, - establishmentType: "retail", - }); - - await connection.sendTransactionFromInstructions({ - feePayer: operator, - instructions: [ix], - }); - - const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); - - assert.strictEqual(node.data.authority, operator.address); - assert.strictEqual(node.data.nodeId, NODE_ID); - assert.strictEqual(node.data.screenSize, ScreenSize.Large); - assert.strictEqual(node.data.resolution.width, 1920); - assert.strictEqual(node.data.resolution.height, 1080); - assert.strictEqual(node.data.estimatedFootfall, 5000); - assert.strictEqual(node.data.establishmentType, "retail"); - assert.strictEqual(node.data.totalPlays, 0n); - assert.strictEqual(node.data.totalEarnings, 0n); - assert.deepStrictEqual(node.data.landmarks, ["Times Square"]); - }); - - test("updates node metadata", async () => { - const ix = getUpdateNodeMetadataInstruction({ - nodeAccount: nodeAccountPDA, - authority: operator, - location: null, - estimatedFootfall: 8000, - blockedTagMask: null, - status: null, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: operator, - instructions: [ix], - }); - - const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); - assert.strictEqual(node.data.estimatedFootfall, 8000); - // Unchanged fields remain the same - assert.strictEqual(node.data.screenSize, ScreenSize.Large); - assert.strictEqual(node.data.establishmentType, "retail"); - }); + test("adds CIDs to the campaign", async () => { + const newCid = + "bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq"; + + const ix = await getAddCidsToCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + authority: publisher, + newCids: [newCid], + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + const campaign = await fetchCampaignAccount( + connection.rpc, + campaignAccountPDA, + ); + assert.strictEqual(campaign.data.cids.length, 2); + assert.strictEqual(campaign.data.cids[1], newCid); + }); + }); + + describe("Node Registration", () => { + test("registers a node", async () => { + const ix = await getRegisterNodeInstructionAsync({ + authority: operator, + nodeId: NODE_ID, + location: SAMPLE_LOCATION, + screenSize: ScreenSize.Large, + resolution: SAMPLE_RESOLUTION, + landmarks: ["Times Square"], + blockedTagMask: 0n, + estimatedFootfall: 5000, + establishmentType: "retail", + }); + + await connection.sendTransactionFromInstructions({ + feePayer: operator, + instructions: [ix], + }); + + const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); + + assert.strictEqual(node.data.authority, operator.address); + assert.strictEqual(node.data.nodeId, NODE_ID); + assert.strictEqual(node.data.screenSize, ScreenSize.Large); + assert.strictEqual(node.data.resolution.width, 1920); + assert.strictEqual(node.data.resolution.height, 1080); + assert.strictEqual(node.data.estimatedFootfall, 5000); + assert.strictEqual(node.data.establishmentType, "retail"); + assert.strictEqual(node.data.totalPlays, 0n); + assert.strictEqual(node.data.totalEarnings, 0n); + assert.deepStrictEqual(node.data.landmarks, ["Times Square"]); }); - describe("Play Cycle - Claim & Confirm", () => { - test("operator claims a campaign", async () => { - const campaignBefore = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); - - const ix = await getClaimCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - nodeAccount: nodeAccountPDA, - authority: operator, - cidIndex: CID_INDEX, - claimNonce: CLAIM_NONCE, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: operator, - instructions: [ix], - }); - - // Verify PlayRecord - const play = await fetchPlayRecord(connection.rpc, playRecordPDA); - assert.strictEqual(play.data.campaignAccount, campaignAccountPDA); - assert.strictEqual(play.data.nodeAccount, nodeAccountPDA); - assert.strictEqual(play.data.nonce, CLAIM_NONCE); - assert.strictEqual(play.data.cidIndex, CID_INDEX); - assert.strictEqual(play.data.status, PlayStatus.Claimed); - assert.strictEqual(play.data.paymentAmount, 0n); - - // Verify ClaimCooldown was created - const cooldown = await fetchClaimCooldown(connection.rpc, claimCooldownPDA); - assert.strictEqual(cooldown.data.campaign, campaignAccountPDA); - assert.strictEqual(cooldown.data.node, nodeAccountPDA); - assert.ok(cooldown.data.lastClaimedAt > 0n); - - // Campaign plays_remaining decremented - const campaignAfter = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); - assert.strictEqual( - campaignAfter.data.playsRemaining, - campaignBefore.data.playsRemaining - 1n, - ); - }); - - test("publisher confirms the play and pays the node", async () => { - const nodeBalanceBefore = await connection.getLamportBalance( - operator.address, - "confirmed", - ); - - const ix = getConfirmPlayInstruction({ - playRecord: playRecordPDA, - campaignAccount: campaignAccountPDA, - publisherAccount: publisherAccountPDA, - nodeAccount: nodeAccountPDA, - authority: operator, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - // PlayRecord transitions to Paid - const play = await fetchPlayRecord(connection.rpc, playRecordPDA); - assert.strictEqual(play.data.status, PlayStatus.Paid); - assert.strictEqual(play.data.paymentAmount, BOUNTY_PER_PLAY); - assert.ok(play.data.confirmedAt > 0n); - - // Campaign plays_completed incremented - const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); - assert.strictEqual(campaign.data.playsCompleted, 1n); - - // Node operator received the bounty - const nodeBalanceAfter = await connection.getLamportBalance( - operator.address, - "confirmed", - ); - assert.ok(nodeBalanceAfter > nodeBalanceBefore); - - // Node aggregate stats updated - const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); - assert.strictEqual(node.data.totalPlays, 1n); - assert.strictEqual(node.data.totalEarnings, BOUNTY_PER_PLAY); - - // Publisher total_spent updated - const pub = await fetchPublisherAccount(connection.rpc, publisherAccountPDA); - assert.strictEqual(pub.data.totalSpent, initialTotalSpent + BOUNTY_PER_PLAY); - }); + test("updates node metadata", async () => { + const ix = getUpdateNodeMetadataInstruction({ + nodeAccount: nodeAccountPDA, + authority: operator, + location: null, + estimatedFootfall: 8000, + blockedTagMask: null, + status: null, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: operator, + instructions: [ix], + }); + + const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); + assert.strictEqual(node.data.estimatedFootfall, 8000); + // Unchanged fields remain the same + assert.strictEqual(node.data.screenSize, ScreenSize.Large); + assert.strictEqual(node.data.establishmentType, "retail"); + }); + }); + + describe("Play Cycle - Claim & Confirm", () => { + test("operator claims a campaign", async () => { + const campaignBefore = await fetchCampaignAccount( + connection.rpc, + campaignAccountPDA, + ); + + const ix = await getClaimCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + nodeAccount: nodeAccountPDA, + authority: operator, + cidIndex: CID_INDEX, + claimNonce: CLAIM_NONCE, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: operator, + instructions: [ix], + }); + + // Verify PlayRecord + const play = await fetchPlayRecord(connection.rpc, playRecordPDA); + assert.strictEqual(play.data.campaignAccount, campaignAccountPDA); + assert.strictEqual(play.data.nodeAccount, nodeAccountPDA); + assert.strictEqual(play.data.nonce, CLAIM_NONCE); + assert.strictEqual(play.data.cidIndex, CID_INDEX); + assert.strictEqual(play.data.status, PlayStatus.Claimed); + assert.strictEqual(play.data.paymentAmount, 0n); + + // Verify ClaimCooldown was created + const cooldown = await fetchClaimCooldown( + connection.rpc, + claimCooldownPDA, + ); + assert.strictEqual(cooldown.data.campaign, campaignAccountPDA); + assert.strictEqual(cooldown.data.node, nodeAccountPDA); + assert.ok(cooldown.data.lastClaimedAt > 0n); + + // Campaign plays_remaining decremented + const campaignAfter = await fetchCampaignAccount( + connection.rpc, + campaignAccountPDA, + ); + assert.strictEqual( + campaignAfter.data.playsRemaining, + campaignBefore.data.playsRemaining - 1n, + ); }); - describe("Timeout Play", () => { - const TIMEOUT_NONCE = 2n; - let timeoutPlayRecordPDA: Address; - - before(async () => { - ({ pda: timeoutPlayRecordPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "play_record", - campaignAccountPDA, - nodeAccountPDA, - TIMEOUT_NONCE, - ])); - - // Claim a second play that we'll attempt to time out - const ix = await getClaimCampaignInstructionAsync({ - campaignAccount: campaignAccountPDA, - nodeAccount: nodeAccountPDA, - authority: operator, - cidIndex: CID_INDEX, - claimNonce: TIMEOUT_NONCE, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: operator, - instructions: [ix], - }); - }); - - test("verifies the claimed play exists before timeout", async () => { - const play = await fetchPlayRecord(connection.rpc, timeoutPlayRecordPDA); - assert.strictEqual(play.data.status, PlayStatus.Claimed); - assert.strictEqual(play.data.nonce, TIMEOUT_NONCE); - - // plays_remaining should have decremented again - const campaign = await fetchCampaignAccount(connection.rpc, campaignAccountPDA); - assert.strictEqual( - campaign.data.playsRemaining, - TOTAL_PLAYS - 2n, // two claims made so far - ); - }); - - // Full timeout testing requires clock warping (bankrun / solana-program-test); - // localnet doesn't support it, so we only assert the play record state here. + test("publisher confirms the play and pays the node", async () => { + const nodeBalanceBefore = await connection.getLamportBalance( + operator.address, + "confirmed", + ); + + const ix = getConfirmPlayInstruction({ + playRecord: playRecordPDA, + campaignAccount: campaignAccountPDA, + publisherAccount: publisherAccountPDA, + nodeAccount: nodeAccountPDA, + authority: operator, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + // PlayRecord transitions to Paid + const play = await fetchPlayRecord(connection.rpc, playRecordPDA); + assert.strictEqual(play.data.status, PlayStatus.Paid); + assert.strictEqual(play.data.paymentAmount, BOUNTY_PER_PLAY); + assert.ok(play.data.confirmedAt > 0n); + + // Campaign plays_completed incremented + const campaign = await fetchCampaignAccount( + connection.rpc, + campaignAccountPDA, + ); + assert.strictEqual(campaign.data.playsCompleted, 1n); + + // Node operator received the bounty + const nodeBalanceAfter = await connection.getLamportBalance( + operator.address, + "confirmed", + ); + assert.ok(nodeBalanceAfter > nodeBalanceBefore); + + // Node aggregate stats updated + const node = await fetchNodeAccount(connection.rpc, nodeAccountPDA); + assert.strictEqual(node.data.totalPlays, 1n); + assert.strictEqual(node.data.totalEarnings, BOUNTY_PER_PLAY); + + // Publisher total_spent updated + const pub = await fetchPublisherAccount( + connection.rpc, + publisherAccountPDA, + ); + assert.strictEqual( + pub.data.totalSpent, + initialTotalSpent + BOUNTY_PER_PLAY, + ); + }); + }); + + describe("Timeout Play", () => { + const TIMEOUT_NONCE = 2n; + let timeoutPlayRecordPDA: Address; + + before(async () => { + ({ pda: timeoutPlayRecordPDA } = await getPDAAndBump( + SAMIZDAT_PROGRAM_ADDRESS, + ["play_record", campaignAccountPDA, nodeAccountPDA, TIMEOUT_NONCE], + )); + + // Claim a second play that we'll attempt to time out + const ix = await getClaimCampaignInstructionAsync({ + campaignAccount: campaignAccountPDA, + nodeAccount: nodeAccountPDA, + authority: operator, + cidIndex: CID_INDEX, + claimNonce: TIMEOUT_NONCE, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: operator, + instructions: [ix], + }); + }); + + test("verifies the claimed play exists before timeout", async () => { + const play = await fetchPlayRecord(connection.rpc, timeoutPlayRecordPDA); + assert.strictEqual(play.data.status, PlayStatus.Claimed); + assert.strictEqual(play.data.nonce, TIMEOUT_NONCE); + + // plays_remaining should have decremented again + const campaign = await fetchCampaignAccount( + connection.rpc, + campaignAccountPDA, + ); + assert.strictEqual( + campaign.data.playsRemaining, + TOTAL_PLAYS - 2n, // two claims made so far + ); + }); + + // Full timeout testing requires clock warping (bankrun / solana-program-test); + // localnet doesn't support it, so we only assert the play record state here. + }); + + describe("Close Campaign", () => { + const CLOSE_CAMPAIGN_ID = RUN_SEED + 1000n; + let closeCampaignPDA: Address; + + before(async () => { + ({ pda: closeCampaignPDA } = await getPDAAndBump( + SAMIZDAT_PROGRAM_ADDRESS, + ["campaign", publisherAccountPDA, CLOSE_CAMPAIGN_ID], + )); + + // Create a small campaign specifically for closing + const createIx = await getCreateCampaignInstructionAsync({ + authority: publisher, + campaignId: CLOSE_CAMPAIGN_ID, + cids: SAMPLE_CIDS, + bountyPerPlay: 1_000n, + totalPlays: 1n, + tagMask: 0n, + targetFilters: SAMPLE_TARGET_FILTERS, + claimCooldown: 0n, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [createIx], + }); }); - describe("Close Campaign", () => { - const CLOSE_CAMPAIGN_ID = RUN_SEED + 1000n; - let closeCampaignPDA: Address; - - before(async () => { - ({ pda: closeCampaignPDA } = await getPDAAndBump(SAMIZDAT_PROGRAM_ADDRESS, [ - "campaign", - publisherAccountPDA, - CLOSE_CAMPAIGN_ID, - ])); - - // Create a small campaign specifically for closing - const createIx = await getCreateCampaignInstructionAsync({ - authority: publisher, - campaignId: CLOSE_CAMPAIGN_ID, - cids: SAMPLE_CIDS, - bountyPerPlay: 1_000n, - totalPlays: 1n, - tagMask: 0n, - targetFilters: SAMPLE_TARGET_FILTERS, - claimCooldown: 0n, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [createIx], - }); - }); - - test("closes a campaign and reclaims rent + remaining funds", async () => { - const publisherBalanceBefore = await connection.getLamportBalance(publisher.address); - - const ix = await getCloseCampaignInstructionAsync({ - campaignAccount: closeCampaignPDA, - authority: publisher, - }); - - await connection.sendTransactionFromInstructions({ - feePayer: publisher, - instructions: [ix], - }); - - // Campaign account should be closed (zero lamports) - const balance = await connection.getLamportBalance(closeCampaignPDA); - assert.strictEqual(balance, 0n); - - // Publisher received rent back (minus tx fee) - const publisherBalanceAfter = await connection.getLamportBalance(publisher.address); - // Net gain should be positive once we account for a small tx fee - assert.ok( - publisherBalanceAfter > publisherBalanceBefore - 100_000n, - "Publisher should have reclaimed rent minus tx fee", - ); - }); + test("closes a campaign and reclaims rent + remaining funds", async () => { + const publisherBalanceBefore = await connection.getLamportBalance( + publisher.address, + ); + + const ix = await getCloseCampaignInstructionAsync({ + campaignAccount: closeCampaignPDA, + authority: publisher, + }); + + await connection.sendTransactionFromInstructions({ + feePayer: publisher, + instructions: [ix], + }); + + // Campaign account should be closed (zero lamports) + const balance = await connection.getLamportBalance(closeCampaignPDA); + assert.strictEqual(balance, 0n); + + // Publisher received rent back (minus tx fee) + const publisherBalanceAfter = await connection.getLamportBalance( + publisher.address, + ); + // Net gain should be positive once we account for a small tx fee + assert.ok( + publisherBalanceAfter > publisherBalanceBefore - 100_000n, + "Publisher should have reclaimed rent minus tx fee", + ); }); + }); }); From 9c9d94062a1b91a1a3a4052cdb5c289386a9fc7a Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 4 Mar 2026 12:46:13 +0530 Subject: [PATCH 3/9] ci: enable preview deployments on PRs Signed-off-by: Ayush --- .github/workflows/cd.yml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ea09ed8..7bdb094 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -13,7 +13,7 @@ permissions: id-token: write concurrency: - group: pages + group: pages-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || 'production' }} cancel-in-progress: true env: @@ -42,11 +42,6 @@ jobs: - name: Install Anchor CLI run: cargo binstall anchor-cli@${{env.ANCHOR_VERSION}} --no-confirm --locked --force - # - name: Inject Program Keypair - # run: | - # mkdir -p target/deploy - # echo "${{ secrets.PROGRAM_KEYPAIR_BYTES }}" > target/deploy/cayed-keypair.json - - name: Anchor build run: anchor build shell: bash @@ -74,7 +69,8 @@ jobs: with: path: web/dist - deploy: + deploy-production: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: build permissions: pages: write @@ -87,3 +83,20 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 + + deploy-preview: + if: github.event_name == 'pull_request' + needs: build + permissions: + pages: write + id-token: write + runs-on: ubuntu-latest + environment: + name: preview-pr-${{ github.event.number }} + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy preview + id: deployment + uses: actions/deploy-pages@v4 + with: + preview: true From a043b10680f6d2fd90ebb3c6599477e5a34f0190 Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 4 Mar 2026 13:05:53 +0530 Subject: [PATCH 4/9] tests: let tests run on both devnet and localnet - to run on localnet, run: `anchor test` or `CLUSTER=localnet anchor test` - run on devnet with: `CLUSTER=devnet anchor test` Signed-off-by: Ayush --- tests/samizdat.test.ts | 56 +++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/tests/samizdat.test.ts b/tests/samizdat.test.ts index 4c4bfaf..80240a3 100644 --- a/tests/samizdat.test.ts +++ b/tests/samizdat.test.ts @@ -58,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; @@ -74,29 +76,33 @@ describe("Samizdat Program – Happy Path", () => { let initialTotalSpent: bigint; before(async () => { - connection = connect("devnet"); - - // Load the pre-funded devnet wallet as the publisher/fee payer - const publisherKeypairBytes = new Uint8Array( - JSON.parse( - readFileSync( - `${process.env.HOME}/.config/solana/devnet-test.json`, - "utf-8", + connection = connect(CLUSTER); + + if (CLUSTER === "localnet") { + const [pub, op] = await connection.createWallets(2); + publisher = pub!; + operator = op!; + } else { + const publisherKeypairBytes = new Uint8Array( + JSON.parse( + readFileSync( + `${process.env.HOME}/.config/solana/devnet-test.json`, + "utf-8", + ), ), - ), - ); - publisher = await createKeyPairSignerFromBytes(publisherKeypairBytes); - - // Load the pre-funded operator wallet - const operatorKeypairBytes = new Uint8Array( - JSON.parse( - readFileSync( - `${process.env.HOME}/.config/solana/devnet-operator.json`, - "utf-8", + ); + publisher = await createKeyPairSignerFromBytes(publisherKeypairBytes); + + const operatorKeypairBytes = new Uint8Array( + JSON.parse( + readFileSync( + `${process.env.HOME}/.config/solana/devnet-operator.json`, + "utf-8", + ), ), - ), - ); - operator = await createKeyPairSignerFromBytes(operatorKeypairBytes); + ); + operator = await createKeyPairSignerFromBytes(operatorKeypairBytes); + } ({ pda: publisherAccountPDA } = await getPDAAndBump( SAMIZDAT_PROGRAM_ADDRESS, @@ -147,16 +153,16 @@ describe("Samizdat Program – Happy Path", () => { instructions: [ix], }); } catch { - // Already registered - that's fine on devnet + // Already registered, fine on devnet } // Snapshot current publisher state - const pub = await fetchPublisherAccount( + const pubAccount = await fetchPublisherAccount( connection.rpc, publisherAccountPDA, ); - initialTotalCampaigns = pub.data.totalCampaigns; - initialTotalSpent = pub.data.totalSpent; + initialTotalCampaigns = pubAccount.data.totalCampaigns; + initialTotalSpent = pubAccount.data.totalSpent; }); describe("Publisher Registration", () => { From 7c62e6097064774b451fb17321ded1415602d6fd Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 4 Mar 2026 18:43:07 +0530 Subject: [PATCH 5/9] fix: add init_if_needed to publisher registration instruction Signed-off-by: Ayush --- .../src/instructions/register_publisher.rs | 22 +++++++++++-------- tests/samizdat.test.ts | 4 ++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/programs/samizdat/src/instructions/register_publisher.rs b/programs/samizdat/src/instructions/register_publisher.rs index 9e6ce0a..9d4fcae 100644 --- a/programs/samizdat/src/instructions/register_publisher.rs +++ b/programs/samizdat/src/instructions/register_publisher.rs @@ -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()], @@ -19,13 +19,17 @@ pub struct RegisterPublisher<'info> { } pub fn process_register_publisher(ctx: Context) -> 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(()) } diff --git a/tests/samizdat.test.ts b/tests/samizdat.test.ts index 80240a3..3a85a47 100644 --- a/tests/samizdat.test.ts +++ b/tests/samizdat.test.ts @@ -152,8 +152,8 @@ describe("Samizdat Program – Happy Path", () => { feePayer: publisher, instructions: [ix], }); - } catch { - // Already registered, fine on devnet + } catch (error) { + throw error; } // Snapshot current publisher state From 4203135eca737cf4d6f211c2ba5a0c8945b3af92 Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 4 Mar 2026 18:45:44 +0530 Subject: [PATCH 6/9] Revert "ci: enable preview deployments on PRs" This reverts commit 9c9d94062a1b91a1a3a4052cdb5c289386a9fc7a. --- .github/workflows/cd.yml | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7bdb094..ea09ed8 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -13,7 +13,7 @@ permissions: id-token: write concurrency: - group: pages-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || 'production' }} + group: pages cancel-in-progress: true env: @@ -42,6 +42,11 @@ jobs: - name: Install Anchor CLI run: cargo binstall anchor-cli@${{env.ANCHOR_VERSION}} --no-confirm --locked --force + # - name: Inject Program Keypair + # run: | + # mkdir -p target/deploy + # echo "${{ secrets.PROGRAM_KEYPAIR_BYTES }}" > target/deploy/cayed-keypair.json + - name: Anchor build run: anchor build shell: bash @@ -69,8 +74,7 @@ jobs: with: path: web/dist - deploy-production: - if: github.event_name == 'push' && github.ref == 'refs/heads/master' + deploy: needs: build permissions: pages: write @@ -83,20 +87,3 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 - - deploy-preview: - if: github.event_name == 'pull_request' - needs: build - permissions: - pages: write - id-token: write - runs-on: ubuntu-latest - environment: - name: preview-pr-${{ github.event.number }} - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy preview - id: deployment - uses: actions/deploy-pages@v4 - with: - preview: true From 70554951337c844a1d7ae383e435c3a6aed67d79 Mon Sep 17 00:00:00 2001 From: Nitish C <86357181+Nitish-bot@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:55:06 +0530 Subject: [PATCH 7/9] optioally test with env variables --- .gitignore | 3 ++- tests/samizdat.test.ts | 31 ++++++++++++------------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 27ce145..e492688 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ target node_modules test-ledger .surfpool -web/client \ No newline at end of file +web/client +.env \ No newline at end of file diff --git a/tests/samizdat.test.ts b/tests/samizdat.test.ts index 3a85a47..e3cb0dc 100644 --- a/tests/samizdat.test.ts +++ b/tests/samizdat.test.ts @@ -83,25 +83,18 @@ describe("Samizdat Program – Happy Path", () => { publisher = pub!; operator = op!; } else { - const publisherKeypairBytes = new Uint8Array( - JSON.parse( - readFileSync( - `${process.env.HOME}/.config/solana/devnet-test.json`, - "utf-8", - ), - ), - ); - publisher = await createKeyPairSignerFromBytes(publisherKeypairBytes); - - const operatorKeypairBytes = new Uint8Array( - JSON.parse( - readFileSync( - `${process.env.HOME}/.config/solana/devnet-operator.json`, - "utf-8", - ), - ), - ); - operator = await createKeyPairSignerFromBytes(operatorKeypairBytes); + 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( From 32ec56d9761cd81cc954e631ff84cda2c9ceeeb3 Mon Sep 17 00:00:00 2001 From: Nitish C <86357181+Nitish-bot@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:57:43 +0530 Subject: [PATCH 8/9] prettier --- tests/samizdat.test.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/samizdat.test.ts b/tests/samizdat.test.ts index e3cb0dc..4519dc0 100644 --- a/tests/samizdat.test.ts +++ b/tests/samizdat.test.ts @@ -84,17 +84,21 @@ describe("Samizdat Program – Happy Path", () => { operator = op!; } else { const getBytes = (envVar: string | undefined, filePath: string) => { - const raw = envVar - ? JSON.parse(envVar) + const raw = envVar + ? JSON.parse(envVar) : JSON.parse(readFileSync(filePath, "utf-8")); - return new Uint8Array(raw); + 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)); + publisher = await createKeyPairSignerFromBytes( + getBytes(process.env.KEYPAIR_ONE, publisherPath), + ); + operator = await createKeyPairSignerFromBytes( + getBytes(process.env.KEYPAIR_TWO, operatorPath), + ); } ({ pda: publisherAccountPDA } = await getPDAAndBump( From 28ee4bc501404fbca68e517e4b161776615226a1 Mon Sep 17 00:00:00 2001 From: Nitish C <86357181+Nitish-bot@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:09:00 +0530 Subject: [PATCH 9/9] all lint fmt --- web/src/pages/publisher.tsx | 6 +++++- web/src/pages/twin.tsx | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/web/src/pages/publisher.tsx b/web/src/pages/publisher.tsx index 47b5721..0224095 100644 --- a/web/src/pages/publisher.tsx +++ b/web/src/pages/publisher.tsx @@ -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; }, diff --git a/web/src/pages/twin.tsx b/web/src/pages/twin.tsx index cc086bc..e34beea 100644 --- a/web/src/pages/twin.tsx +++ b/web/src/pages/twin.tsx @@ -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 {