import * as anchor from "@coral-xyz/anchor"; import { SystemProgram, Transaction } from "@solana/web3.js"; import { getAccount } from "@solana/spl-token"; import { expect } from "chai"; import fetch from "node-fetch"; import { getTestContext, setTestContext } from "../setup"; import * as crypto from "crypto"; describe("Place and Cancel Bid (API)", () => { it("Company 1 (BMW) Places Bid for 500 EUA", async () => { const ctx = getTestContext(); const { connection, program, axumBaseUrl, company1Admin, company1Pda, company1UsdcVault, auctionPda, } = ctx; console.log("=== Company 1 Placing Bid ==="); // Derive primary market and verifying key PDAs const [primaryMarketPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("primary_market")], program.programId ); const [verifyingKeyPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("verifying_key")], program.programId ); // Step 1: Get company's USDC balance const vaultAccount = await getAccount(connection, company1UsdcVault); const companyBalance = Number(vaultAccount.amount); console.log(`Company 1 USDC balance: ${companyBalance / 1_000_000} USDC`); // Step 2: Prepare bid parameters const euaVolume = 500; // Must be divisible by 250 const bidAmount = 25_000_000; // 25 USDC (6 decimals) - PRIVATE const nonce = crypto.randomBytes(16).toString("hex"); const salt = crypto.randomBytes(32).toString("hex"); // Verify sufficient balance if (bidAmount > companyBalance) { throw new Error(`Insufficient balance: ${companyBalance} < ${bidAmount}`); } // Step 3: Get auction info for bid_id calculation const auction = await program.account.auction.fetch(auctionPda); const bidId = auction.totalBids; // Derive bid record PDA const [bidRecordPda] = anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from("bid"), auctionPda.toBuffer(), company1Pda.toBuffer(), new anchor.BN(bidId).toArrayLike(Buffer, "le", 8), ], program.programId ); console.log(`Bid Record PDA: ${bidRecordPda.toBase58()}`); console.log(`Bid ID: ${bidId}`); console.log(`EUA Volume: ${euaVolume}`); console.log(`Bid Amount: [PRIVATE - ${bidAmount / 1_000_000} USDC]`); // Step 4: Create API request payload const request = { primary_market: primaryMarketPda.toBase58(), verifying_key: verifyingKeyPda.toBase58(), auction: auctionPda.toBase58(), company: company1Pda.toBase58(), company_usdc_vault: company1UsdcVault.toBase58(), bid_record: bidRecordPda.toBase58(), company_admin: company1Admin.publicKey.toBase58(), bid_amount: bidAmount, // Backend will generate proof eua_volume: euaVolume, nonce: nonce, salt: salt, }; console.log("Calling API to create place bid transaction..."); console.log("⚠️ This will generate a ZK-SNARK proof on the backend"); // Step 5: Call Axum API to get unsigned transaction const createTxResponse = await fetch( `${axumBaseUrl}/api/primary-market/place-bid`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), } ); if (!createTxResponse.ok) { const error = await createTxResponse.text(); throw new Error(`Failed to create transaction: ${error}`); } const { transaction_base64, message, bid_hash } = await createTxResponse.json(); console.log(`API Response: ${message}`); console.log(`Bid Hash: ${bid_hash}`); // Step 6: Deserialize and sign transaction const txBuffer = Buffer.from(transaction_base64, "base64"); const transaction = Transaction.from(txBuffer); console.log("Transaction signer (expected):"); console.log(" Company 1 Admin:", company1Admin.publicKey.toBase58()); // Sign with company1Admin transaction.sign(company1Admin); console.log("Transaction signed locally"); // Step 7: Submit signed transaction to backend const signedTxBase64 = transaction.serialize().toString("base64"); const submitTxResponse = await fetch(`${axumBaseUrl}/api/submit-tx`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction_base64: signedTxBase64 }), }); if (!submitTxResponse.ok) { const error = await submitTxResponse.text(); throw new Error(`Failed to submit transaction: ${error}`); } const { signature } = await submitTxResponse.json(); console.log(`✓ Transaction signature: ${signature}`); // Step 8: Verify on-chain state - BID PLACED const bidRecord = await program.account.bidRecord.fetch(bidRecordPda); expect(bidRecord.company.equals(company1Pda)).to.be.true; expect(bidRecord.auction.equals(auctionPda)).to.be.true; expect(bidRecord.euaVolume.toNumber()).to.equal(euaVolume); expect(bidRecord.bidId.toNumber()).to.equal(bidId); expect(bidRecord.isCancelled).to.be.false; expect(bidRecord.bidHash).to.deep.equal(Buffer.from(bid_hash, "hex")); // Important: bid_amount is NOT stored on-chain (privacy!) console.log("✓ Bid record verified"); console.log(` - Bid ID: ${bidRecord.bidId.toNumber()}`); console.log(` - EUA Volume: ${bidRecord.euaVolume.toNumber()}`); console.log(` - Bid Amount: [NOT STORED ON-CHAIN - remains private]`); console.log(` - Bid Hash: ${bid_hash}`); // Step 9: Verify company state updated const company = await program.account.company.fetch(company1Pda); expect(company.placedBid).to.be.true; console.log("✓ Company 1 state verified"); console.log(` - Placed Bid: ${company.placedBid}`); // Step 10: Verify auction state updated const auctionAfter = await program.account.auction.fetch(auctionPda); expect(auctionAfter.totalBids).to.equal(bidId + 1); console.log("✓ Auction state verified"); console.log(` - Total Bids: ${auctionAfter.totalBids}`); // Step 11: Verify events/logs const txDetails = await connection.getTransaction(signature, { commitment: "confirmed", maxSupportedTransactionVersion: 0, }); if (txDetails?.meta?.logMessages) { const logs = txDetails.meta.logMessages; const hasBidLog = logs.some((log) => log.includes("Bid placed") ); if (hasBidLog) { console.log("✓ Bid placed log message found"); } const hasEventLog = logs.some((log) => log.includes("BidPlaced") ); if (hasEventLog) { console.log("✓ BidPlaced event emitted"); } } console.log("✓ Company 1 placed bid successfully via API"); // Store for cancel test setTestContext({ company1BidRecordPda: bidRecordPda, company1BidId: bidId, }); }); it("Company 2 (Total) Places Bid for 250 EUA", async () => { const ctx = getTestContext(); const { connection, program, axumBaseUrl, company2Admin, company2Pda, company2UsdcVault, auctionPda, } = ctx; console.log("=== Company 2 Placing Bid ==="); const [primaryMarketPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("primary_market")], program.programId ); const [verifyingKeyPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("verifying_key")], program.programId ); // Get balance const vaultAccount = await getAccount(connection, company2UsdcVault); const companyBalance = Number(vaultAccount.amount); console.log(`Company 2 USDC balance: ${companyBalance / 1_000_000} USDC`); // Bid parameters const euaVolume = 250; const bidAmount = 18_000_000; // 18 USDC - PRIVATE const nonce = crypto.randomBytes(16).toString("hex"); const salt = crypto.randomBytes(32).toString("hex"); // Get bid_id const auction = await program.account.auction.fetch(auctionPda); const bidId = auction.totalBids; const [bidRecordPda] = anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from("bid"), auctionPda.toBuffer(), company2Pda.toBuffer(), new anchor.BN(bidId).toArrayLike(Buffer, "le", 8), ], program.programId ); const request = { primary_market: primaryMarketPda.toBase58(), verifying_key: verifyingKeyPda.toBase58(), auction: auctionPda.toBase58(), company: company2Pda.toBase58(), company_usdc_vault: company2UsdcVault.toBase58(), bid_record: bidRecordPda.toBase58(), company_admin: company2Admin.publicKey.toBase58(), bid_amount: bidAmount, eua_volume: euaVolume, nonce: nonce, salt: salt, }; console.log("Calling API to create place bid transaction..."); const createTxResponse = await fetch( `${axumBaseUrl}/api/primary-market/place-bid`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), } ); if (!createTxResponse.ok) { const error = await createTxResponse.text(); throw new Error(`Failed to create transaction: ${error}`); } const { transaction_base64, bid_hash } = await createTxResponse.json(); const txBuffer = Buffer.from(transaction_base64, "base64"); const transaction = Transaction.from(txBuffer); transaction.sign(company2Admin); const signedTxBase64 = transaction.serialize().toString("base64"); const submitTxResponse = await fetch(`${axumBaseUrl}/api/submit-tx`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction_base64: signedTxBase64 }), }); if (!submitTxResponse.ok) { const error = await submitTxResponse.text(); throw new Error(`Failed to submit transaction: ${error}`); } const { signature } = await submitTxResponse.json(); console.log(`✓ Transaction signature: ${signature}`); // Verify bid record const bidRecord = await program.account.bidRecord.fetch(bidRecordPda); expect(bidRecord.euaVolume.toNumber()).to.equal(euaVolume); console.log("✓ Company 2 placed bid successfully"); console.log(` - EUA Volume: ${euaVolume}`); console.log(` - Bid Hash: ${bid_hash}`); // Store for cancel test setTestContext({ company2BidRecordPda: bidRecordPda, company2BidId: bidId, }); }); it("Should Reject Bid with Insufficient Balance", async () => { const ctx = getTestContext(); const { axumBaseUrl, company1Admin, company1Pda, company1UsdcVault, auctionPda, program, } = ctx; const [primaryMarketPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("primary_market")], program.programId ); const [verifyingKeyPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("verifying_key")], program.programId ); // Get current balance const vaultAccount = await getAccount(ctx.connection, company1UsdcVault); const companyBalance = Number(vaultAccount.amount); // Try to bid more than balance const excessiveBidAmount = companyBalance + 1_000_000; const auction = await program.account.auction.fetch(auctionPda); const bidId = auction.totalBids; const [bidRecordPda] = anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from("bid"), auctionPda.toBuffer(), company1Pda.toBuffer(), new anchor.BN(bidId).toArrayLike(Buffer, "le", 8), ], program.programId ); const request = { primary_market: primaryMarketPda.toBase58(), verifying_key: verifyingKeyPda.toBase58(), auction: auctionPda.toBase58(), company: company1Pda.toBase58(), company_usdc_vault: company1UsdcVault.toBase58(), bid_record: bidRecordPda.toBase58(), company_admin: company1Admin.publicKey.toBase58(), bid_amount: excessiveBidAmount, eua_volume: 250, nonce: crypto.randomBytes(16).toString("hex"), salt: crypto.randomBytes(32).toString("hex"), }; const createTxResponse = await fetch( `${axumBaseUrl}/api/primary-market/place-bid`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), } ); expect(createTxResponse.ok).to.be.false; const errorText = await createTxResponse.text(); console.log("✓ Correctly rejected insufficient balance:", errorText); }); it("Should Reject Bid with Invalid EUA Volume", async () => { const ctx = getTestContext(); const { axumBaseUrl, company1Admin, company1Pda, company1UsdcVault, auctionPda, program, } = ctx; const [primaryMarketPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("primary_market")], program.programId ); const [verifyingKeyPda] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("verifying_key")], program.programId ); const auction = await program.account.auction.fetch(auctionPda); const bidId = auction.totalBids; const [bidRecordPda] = anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from("bid"), auctionPda.toBuffer(), company1Pda.toBuffer(), new anchor.BN(bidId).toArrayLike(Buffer, "le", 8), ], program.programId ); const request = { primary_market: primaryMarketPda.toBase58(), verifying_key: verifyingKeyPda.toBase58(), auction: auctionPda.toBase58(), company: company1Pda.toBase58(), company_usdc_vault: company1UsdcVault.toBase58(), bid_record: bidRecordPda.toBase58(), company_admin: company1Admin.publicKey.toBase58(), bid_amount: 10_000_000, eua_volume: 123, // Not divisible by 250 nonce: crypto.randomBytes(16).toString("hex"), salt: crypto.randomBytes(32).toString("hex"), }; const createTxResponse = await fetch( `${axumBaseUrl}/api/primary-market/place-bid`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), } ); expect(createTxResponse.ok).to.be.false; const errorText = await createTxResponse.text(); console.log("✓ Correctly rejected invalid EUA volume:", errorText); }); it("Company 1 Cancels Their Bid", async () => { const ctx = getTestContext(); const { connection, program, axumBaseUrl, company1Admin, company1Pda, auctionPda, company1BidRecordPda, company1BidId, } = ctx; console.log("=== Company 1 Cancelling Bid ==="); // Verify bid exists and is not cancelled const bidBefore = await program.account.bidRecord.fetch(company1BidRecordPda); expect(bidBefore.isCancelled).to.be.false; const request = { auction: auctionPda.toBase58(), company: company1Pda.toBase58(), bid_record: company1BidRecordPda.toBase58(), company_admin: company1Admin.publicKey.toBase58(), }; console.log("Calling API to create cancel bid transaction..."); const createTxResponse = await fetch( `${axumBaseUrl}/api/primary-market/cancel-bid`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), } ); if (!createTxResponse.ok) { const error = await createTxResponse.text(); throw new Error(`Failed to create transaction: ${error}`); } const { transaction_base64, message } = await createTxResponse.json(); console.log(`API Response: ${message}`); const txBuffer = Buffer.from(transaction_base64, "base64"); const transaction = Transaction.from(txBuffer); transaction.sign(company1Admin); const signedTxBase64 = transaction.serialize().toString("base64"); const submitTxResponse = await fetch(`${axumBaseUrl}/api/submit-tx`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction_base64: signedTxBase64 }), }); if (!submitTxResponse.ok) { const error = await submitTxResponse.text(); throw new Error(`Failed to submit transaction: ${error}`); } const { signature } = await submitTxResponse.json(); console.log(`✓ Transaction signature: ${signature}`); // Verify bid cancelled const bidAfter = await program.account.bidRecord.fetch(company1BidRecordPda); expect(bidAfter.isCancelled).to.be.true; console.log("✓ Bid cancelled successfully"); // Verify company state const company = await program.account.company.fetch(company1Pda); expect(company.placedBid).to.be.false; console.log("✓ Company 1 state updated (placed_bid = false)"); // Verify events/logs const txDetails = await connection.getTransaction(signature, { commitment: "confirmed", maxSupportedTransactionVersion: 0, }); if (txDetails?.meta?.logMessages) { const logs = txDetails.meta.logMessages; const hasCancelLog = logs.some((log) => log.includes("Bid cancelled") ); if (hasCancelLog) { console.log("✓ Bid cancelled log message found"); } } }); it("Verify Final Auction State", async () => { const ctx = getTestContext(); const { program, auctionPda } = ctx; const auction = await program.account.auction.fetch(auctionPda); console.log("\n=== Final Auction State ==="); console.log(`Total Bids Placed: ${auction.totalBids}`); console.log(`Active Bids: ${auction.totalBids - 1}`); // One cancelled console.log(`Status: Active`); console.log(`EUA Volume: ${auction.euaVolume.toNumber()}`); // We should have 2 total bids, 1 cancelled expect(auction.totalBids).to.equal(2); expect(auction.status).to.deep.equal({ active: {} }); console.log("✓ All bid operations verified successfully"); }); });