- Executive Summary
- Solana vs EVM Context Bridge
- Smart Contract Architecture Overview
- Environment Setup
- Account Architecture & PDAs
- Function Reference
- Integration Patterns
- Error Handling
- Testing and Development
BALR Market is a sophisticated sports betting prediction market built on Solana. It allows users to create sports betting markets, place orders on event outcomes, and trade prediction shares in both primary and secondary markets.
- Markets & Events: Create football matches and betting events within them
- Order Matching: Users place YES/NO orders that get matched when prices complement to 1 SOL
- Share Tokens: Matched orders result in share tokens that can be traded
- Primary/Secondary Markets: Time-based market phases with different trading rules
- Platform Fees: Built-in fee collection system
- Account-based: All data stored in separate account structures vs single contract storage
- Rent System: Accounts must maintain rent-exempt balance vs gas payments
- PDAs: Program Derived Addresses replace mappings for user/event associations
- Cross-Program Calls: Similar to contract interactions but with account validation
- Transaction Structure: Multiple instructions per transaction vs single function calls
- Solana wallet (Phantom, Solflare)
@solana/web3.js>= 1.95.0@coral-xyz/anchor>= 0.30.0- Node.js environment with TypeScript support
EVM: All contract data stored in single contract's storage slots
mapping(address => uint256) balances;Solana: Each piece of data stored in separate accounts with unique addresses
// Each user's portfolio is a separate account
const [portfolioAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("portfolio"), userWallet.publicKey.toBuffer()],
PROGRAM_ID
);EVM: Single function call per transaction
await contract.placeBet(eventId, amount, prediction);Solana: Multiple instructions per transaction with explicit account requirements
const transaction = new Transaction().add(
await program.methods
.placeOrder(orderId, eventId, orderType, quantity, unitPrice)
.accounts({
globalState: globalStateAccount,
event: eventAccount,
order: orderAccount,
escrowAccount: escrowAccount,
buyer: wallet.publicKey,
adminWallet: adminWallet,
systemProgram: SystemProgram.programId,
})
.instruction()
);EVM: MetaMask pattern
const signer = provider.getSigner();
await contract.connect(signer).functionName();Solana: Wallet adapter pattern
import { useWallet, useConnection } from '@solana/wallet-adapter-react';
const { publicKey, signTransaction } = useWallet();
const { connection } = useConnection();- Program ID:
CYCLSN9sVJGKo4xy3daXvH3KFD66wPgDvAuFVRhc6RDq - Network: Configurable (localnet/devnet/mainnet)
- Language: Rust with Anchor framework
- GlobalState: Platform configuration and admin controls
- Market: Football match container (Team A vs Team B)
- Event: Specific betting question within a market
- Order: Individual buy orders with escrow
- MatchedPair: Record of matched YES/NO orders
- ShareToken: Ownership tokens for matched positions
- OrderBook: Trading book for each event
Market Creation → Event Creation → Order Placement → Order Matching → Share Minting → Settlement
npm install @solana/web3.js @coral-xyz/anchor
npm install @solana/wallet-adapter-base @solana/wallet-adapter-react
npm install @solana/wallet-adapter-phantom @solana/wallet-adapter-solflareimport { Connection, PublicKey, clusterApiUrl } from '@solana/web3.js';
import { AnchorProvider, Program, Idl } from '@coral-xyz/anchor';
// Program configuration
export const PROGRAM_ID = new PublicKey('CYCLSN9sVJGKo4xy3daXvH3KFD66wPgDvAuFVRhc6RDq');
export const NETWORK = 'devnet'; // or 'mainnet-beta'
export const connection = new Connection(clusterApiUrl(NETWORK));
// Initialize program
const provider = AnchorProvider.env();
const program = new Program(idl as Idl, PROGRAM_ID, provider);import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets';
const network = WalletAdapterNetwork.Devnet;
const wallets = [
new PhantomWalletAdapter(),
new SolflareWalletAdapter({ network }),
];
// Wrap your app
<ConnectionProvider endpoint={clusterApiUrl(network)}>
<WalletProvider wallets={wallets} autoConnect>
<YourApp />
</WalletProvider>
</ConnectionProvider>PDA: ["global_state"]
const [globalState] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")],
PROGRAM_ID
);Purpose: Platform configuration, admin controls, fee settings
PDA: ["market", market_id]
const [marketAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from(marketId)],
PROGRAM_ID
);Purpose: Football match information (Team A vs Team B, timestamp)
PDA: ["event", market_id, event_id]
const [eventAccount] = PublicKey.findProgramAddressSync(
[
Buffer.from("event"),
Buffer.from(marketId),
Buffer.from(eventId)
],
PROGRAM_ID
);Purpose: Specific betting question, share limits, pricing, timing
PDA: ["order", event_id, order_id_bytes]
const orderIdBuffer = Buffer.allocUnsafe(8);
orderIdBuffer.writeBigUInt64LE(BigInt(orderId));
const [orderAccount] = PublicKey.findProgramAddressSync(
[
Buffer.from("order"),
Buffer.from(eventId),
orderIdBuffer
],
PROGRAM_ID
);Purpose: Individual buy order with quantity, price, status
PDA: ["escrow", event_id, order_id_bytes]
const [escrowAccount] = PublicKey.findProgramAddressSync(
[
Buffer.from("escrow"),
Buffer.from(eventId),
orderIdBuffer
],
PROGRAM_ID
);Purpose: Holds SOL for pending orders
PDA: ["share", event_id, owner, share_type_bytes]
const shareType = "yes"; // or "no"
const [shareTokenAccount] = PublicKey.findProgramAddressSync(
[
Buffer.from("share"),
Buffer.from(eventId),
ownerPublicKey.toBuffer(),
Buffer.from(shareType)
],
PROGRAM_ID
);Purpose: User's share ownership for specific event outcome
Function: initialize_global_state
- Purpose: One-time platform setup by admin
- Frontend Usage: Admin panel initialization
- When to Call: Only once during platform deployment
Required Accounts:
globalState(init): Platform configuration accountadmin(signer): Platform administrator walletsystemProgram: Solana system program
Parameters:
- Frontend Must Provide:
admin(Pubkey),platform_fee_primary(u16),platform_fee_secondary(u16) - Auto-Generated: None
- Conditional: None
TypeScript Example:
async function initializeGlobalState(
admin: PublicKey,
platformFeePrimary: number, // basis points (e.g., 200 = 2%)
platformFeeSecondary: number
) {
const [globalState] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")],
PROGRAM_ID
);
const tx = await program.methods
.initializeGlobalState(admin, platformFeePrimary, platformFeeSecondary)
.accounts({
globalState,
admin: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
return tx;
}Error Handling:
Unauthorized: Only designated admin can initializePlatformFeeExceedsMaximum: Fee cannot exceed 5%
Function: create_market
- Purpose: Create a new football match market
- Frontend Usage: Admin creates upcoming match
- When to Call: 24+ hours before match starts
Required Accounts:
globalState(mut): Platform statemarket(init): New market accountadmin(signer, mut): Admin wallet (pays for account creation)systemProgram: Solana system program
Parameters:
- Frontend Must Provide:
market_id(String ≤50 chars),team_a(String ≤100 chars),team_b(String ≤100 chars),match_timestamp(i64) - Auto-Generated: Market account PDA
- Conditional: None
Pre-transaction Setup:
- Ensure match timestamp is at least 24 hours in future
- Verify admin has sufficient SOL for account rent
TypeScript Example:
async function createMarket(
marketId: string,
teamA: string,
teamB: string,
matchTimestamp: number // Unix timestamp
) {
// Validation
if (marketId.length > 50) throw new Error("Market ID too long");
if (teamA.length > 100 || teamB.length > 100) throw new Error("Team name too long");
const currentTime = Math.floor(Date.now() / 1000);
if (matchTimestamp <= currentTime + 86400) {
throw new Error("Match must be at least 24 hours in future");
}
// Account derivation
const [globalState] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")],
PROGRAM_ID
);
const [market] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from(marketId)],
PROGRAM_ID
);
const tx = await program.methods
.createMarket(marketId, teamA, teamB, new anchor.BN(matchTimestamp))
.accounts({
globalState,
market,
admin: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
return tx;
}State Changes: Creates market account, increments global event counter UI Considerations: Show loading state, confirm account creation cost
Function: create_event
- Purpose: Create specific betting question within a market
- Frontend Usage: Admin creates prediction opportunities
- When to Call: After market creation, before match starts
Required Accounts:
globalState(mut): Platform statemarket(mut): Parent market accountevent(init): New event accountorderBook(init): Order book for this eventadmin(signer, mut): Admin walletsystemProgram: Solana system program
Parameters:
- Frontend Must Provide:
event_id(String ≤50),question(String ≤200),max_shares(u32, even number ≤1000),opta_odds_yes(u16, basis points),match_timestamp(i64) - Auto-Generated: Event and orderbook PDAs, normalized odds, share prices
- Conditional: None
Pre-transaction Setup:
- Market must exist
- Admin needs SOL for two account creations (event + orderbook)
TypeScript Example:
async function createEvent(
marketId: string,
eventId: string,
question: string,
maxShares: number, // Must be even, ≤1000
optaOddsYes: number // Basis points (e.g., 6000 = 60% chance)
) {
// Validation
if (eventId.length > 50) throw new Error("Event ID too long");
if (question.length > 200) throw new Error("Question too long");
if (maxShares % 2 !== 0 || maxShares > 1000) throw new Error("Invalid share count");
// Account derivation
const [globalState] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")], PROGRAM_ID
);
const [market] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), Buffer.from(marketId)], PROGRAM_ID
);
const [event] = PublicKey.findProgramAddressSync(
[Buffer.from("event"), Buffer.from(marketId), Buffer.from(eventId)], PROGRAM_ID
);
const [orderBook] = PublicKey.findProgramAddressSync(
[Buffer.from("orderbook"), Buffer.from(eventId), Buffer.from("primary")], PROGRAM_ID
);
const matchTimestamp = Math.floor(Date.now() / 1000) + 86400; // 24h from now
const tx = await program.methods
.createEvent(eventId, question, maxShares, optaOddsYes, new anchor.BN(matchTimestamp))
.accounts({
globalState,
market,
event,
orderBook,
admin: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
return tx;
}State Changes: Creates event and orderbook accounts, calculates YES/NO prices from odds UI Considerations: Display calculated share prices, show account creation costs
Function: place_order
- Purpose: Place buy order for YES or NO shares
- Frontend Usage: Core user trading function
- When to Call: During primary market phase (before match starts)
Required Accounts:
globalState: Platform state (validation)event(mut): Target event accountorder(init): New order accountescrowAccount(init): Escrow for order fundsbuyer(signer, mut): User walletadminWallet(mut): Fee recipientsystemProgram: Solana system program
Parameters:
- Frontend Must Provide:
order_id(u64),event_id(String),order_type(YES/NO),quantity(u64 ≤500),unit_price(u64, lamports per share) - Auto-Generated: Order and escrow PDAs, total cost calculation
- Conditional: Platform fee calculation
Pre-transaction Setup:
- Check user has sufficient SOL balance
- Validate primary market is still open
- Calculate total cost including platform fee
TypeScript Example:
interface OrderParams {
eventId: string;
orderType: 'YES' | 'NO';
quantity: number; // Number of shares
unitPrice: number; // Price per share in lamports
}
async function placeOrder({ eventId, orderType, quantity, unitPrice }: OrderParams) {
// Generate unique order ID
const orderId = Date.now(); // Better: use proper ID generation
// Validation
if (quantity <= 0 || quantity > 500) throw new Error("Invalid quantity");
if (unitPrice <= 0 || unitPrice >= 1_000_000_000) throw new Error("Invalid price");
// Account derivation
const [globalState] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")], PROGRAM_ID
);
// Find market_id from event (you'll need this from your state/API)
const marketId = await getMarketIdForEvent(eventId);
const [event] = PublicKey.findProgramAddressSync(
[Buffer.from("event"), Buffer.from(marketId), Buffer.from(eventId)], PROGRAM_ID
);
const orderIdBuffer = Buffer.allocUnsafe(8);
orderIdBuffer.writeBigUInt64LE(BigInt(orderId));
const [order] = PublicKey.findProgramAddressSync(
[Buffer.from("order"), Buffer.from(eventId), orderIdBuffer], PROGRAM_ID
);
const [escrowAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("escrow"), Buffer.from(eventId), orderIdBuffer], PROGRAM_ID
);
// Get admin wallet from global state
const globalStateAccount = await program.account.globalState.fetch(globalState);
const adminWallet = globalStateAccount.admin;
// Calculate costs
const totalAmount = quantity * unitPrice;
const platformFee = Math.floor(totalAmount * globalStateAccount.platformFeePrimary / 10000);
const totalCost = totalAmount + platformFee;
// Check user balance
const userBalance = await connection.getBalance(wallet.publicKey);
if (userBalance < totalCost + 5000) { // Add buffer for tx fee
throw new Error("Insufficient balance");
}
const orderTypeEnum = orderType === 'YES' ? { yes: {} } : { no: {} };
const tx = await program.methods
.placeOrder(
new anchor.BN(orderId),
eventId,
orderTypeEnum,
new anchor.BN(quantity),
new anchor.BN(unitPrice)
)
.accounts({
globalState,
event,
order,
escrowAccount,
buyer: wallet.publicKey,
adminWallet,
systemProgram: SystemProgram.programId,
})
.rpc();
return { tx, orderId, totalCost, platformFee };
}State Changes: Creates order and escrow accounts, transfers SOL to escrow and fee to admin UI Considerations: Show total cost breakdown, loading states, balance validation
Function: cancel_order
- Purpose: Cancel pending order and refund escrowed SOL
- Frontend Usage: User order management
- When to Call: When order is still pending
Required Accounts:
globalState: Platform stateevent: Event account (validation)order(mut): Order to cancelescrowAccount(mut): Escrow to refund frombuyer(signer, mut): Order ownersystemProgram: Solana system program
Parameters:
- Frontend Must Provide:
order_id(u64),event_id(String) - Auto-Generated: Account PDAs
- Conditional: None
TypeScript Example:
async function cancelOrder(eventId: string, orderId: number) {
const orderIdBuffer = Buffer.allocUnsafe(8);
orderIdBuffer.writeBigUInt64LE(BigInt(orderId));
const [globalState] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")], PROGRAM_ID
);
const marketId = await getMarketIdForEvent(eventId);
const [event] = PublicKey.findProgramAddressSync(
[Buffer.from("event"), Buffer.from(marketId), Buffer.from(eventId)], PROGRAM_ID
);
const [order] = PublicKey.findProgramAddressSync(
[Buffer.from("order"), Buffer.from(eventId), orderIdBuffer], PROGRAM_ID
);
const [escrowAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("escrow"), Buffer.from(eventId), orderIdBuffer], PROGRAM_ID
);
// Verify order exists and is owned by user
const orderAccount = await program.account.order.fetch(order);
if (!orderAccount.buyer.equals(wallet.publicKey)) {
throw new Error("Not your order");
}
if (orderAccount.status !== "Pending") {
throw new Error("Order cannot be cancelled");
}
const tx = await program.methods
.cancelOrder(new anchor.BN(orderId), eventId)
.accounts({
globalState,
event,
order,
escrowAccount,
buyer: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
return tx;
}Error Handling:
OrderNotFound: Invalid order IDUnauthorized: Not order ownerOrderNotPending: Already matched or cancelled
Function: match_orders
- Purpose: Match complementary YES/NO orders
- Frontend Usage: Market maker or automated matching
- When to Call: When compatible orders exist (prices sum to ~1 SOL)
Required Accounts:
globalState: Platform stateevent(mut): Event accountyesOrder(mut): YES order to matchnoOrder(mut): NO order to matchyesEscrow(mut): YES order escrownoEscrow(mut): NO order escrowmatchedPair(init): Record of the matchyesBuyer(mut): YES order ownernoBuyer(mut): NO order ownerauthority(signer, mut): Matching authoritysystemProgram: Solana system program
Parameters:
- Frontend Must Provide:
event_id(String),yes_order_id(u64),no_order_id(u64) - Auto-Generated: Matched pair PDA, match quantity calculation
- Conditional: Price sum validation (must equal ~1 SOL)
Pre-transaction Setup:
- Identify compatible orders (YES price + NO price ≈ 1 SOL)
- Verify both orders are pending
- Calculate match quantity (min of both order quantities)
TypeScript Example:
async function matchOrders(
eventId: string,
yesOrderId: number,
noOrderId: number
) {
// Account derivation helpers
const getOrderPDA = (orderId: number) => {
const buffer = Buffer.allocUnsafe(8);
buffer.writeBigUInt64LE(BigInt(orderId));
return PublicKey.findProgramAddressSync(
[Buffer.from("order"), Buffer.from(eventId), buffer], PROGRAM_ID
)[0];
};
const getEscrowPDA = (orderId: number) => {
const buffer = Buffer.allocUnsafe(8);
buffer.writeBigUInt64LE(BigInt(orderId));
return PublicKey.findProgramAddressSync(
[Buffer.from("escrow"), Buffer.from(eventId), buffer], PROGRAM_ID
)[0];
};
// Fetch order details for validation
const yesOrder = await program.account.order.fetch(getOrderPDA(yesOrderId));
const noOrder = await program.account.order.fetch(getOrderPDA(noOrderId));
// Validate price compatibility (sum should be ~1 SOL)
const priceSum = yesOrder.unitPrice.toNumber() + noOrder.unitPrice.toNumber();
const oneSol = 1_000_000_000;
const tolerance = oneSol / 100; // 1% tolerance
if (Math.abs(priceSum - oneSol) > tolerance) {
throw new Error("Orders not price compatible");
}
// Verify orders are pending
if (yesOrder.status !== "Pending" || noOrder.status !== "Pending") {
throw new Error("Orders must be pending");
}
// Account setup
const marketId = await getMarketIdForEvent(eventId);
const [globalState] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")], PROGRAM_ID
);
const [event] = PublicKey.findProgramAddressSync(
[Buffer.from("event"), Buffer.from(marketId), Buffer.from(eventId)], PROGRAM_ID
);
const eventAccount = await program.account.event.fetch(event);
const matchIdBuffer = Buffer.allocUnsafe(8);
matchIdBuffer.writeBigUInt64LE(BigInt(eventAccount.totalMatches));
const [matchedPair] = PublicKey.findProgramAddressSync(
[Buffer.from("match"), Buffer.from(eventId), matchIdBuffer], PROGRAM_ID
);
const tx = await program.methods
.matchOrders(eventId, new anchor.BN(yesOrderId), new anchor.BN(noOrderId))
.accounts({
globalState,
event,
yesOrder: getOrderPDA(yesOrderId),
noOrder: getOrderPDA(noOrderId),
yesEscrow: getEscrowPDA(yesOrderId),
noEscrow: getEscrowPDA(noOrderId),
matchedPair,
yesBuyer: yesOrder.buyer,
noBuyer: noOrder.buyer,
authority: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
return tx;
}State Changes: Updates order quantities, creates matched pair, transfers escrowed funds between buyers UI Considerations: Show match details, quantity matched, remaining order amounts
Function: mint_shares
- Purpose: Create share tokens for matched order pair
- Frontend Usage: After orders are matched, mint ownership tokens
- When to Call: Immediately after successful order matching
Required Accounts:
globalState: Platform stateevent(mut): Event accountmatchedPair: Matched order recordyesShareToken(init): YES share token for buyernoShareToken(init): NO share token for buyermintAuthority(signer, mut): Share minting authoritysystemProgram: Solana system program
Parameters:
- Frontend Must Provide:
event_id(String),matched_pair_id(u64) - Auto-Generated: Share token PDAs for both buyers
- Conditional: None
TypeScript Example:
async function mintShares(eventId: string, matchedPairId: number) {
const matchIdBuffer = Buffer.allocUnsafe(8);
matchIdBuffer.writeBigUInt64LE(BigInt(matchedPairId));
const [matchedPair] = PublicKey.findProgramAddressSync(
[Buffer.from("match"), Buffer.from(eventId), matchIdBuffer], PROGRAM_ID
);
// Fetch matched pair to get buyer addresses
const matchedPairAccount = await program.account.matchedPair.fetch(matchedPair);
// Create share token PDAs
const [yesShareToken] = PublicKey.findProgramAddressSync(
[
Buffer.from("share"),
Buffer.from(eventId),
matchedPairAccount.yesBuyer.toBuffer(),
Buffer.from("yes")
],
PROGRAM_ID
);
const [noShareToken] = PublicKey.findProgramAddressSync(
[
Buffer.from("share"),
Buffer.from(eventId),
matchedPairAccount.noBuyer.toBuffer(),
Buffer.from("no")
],
PROGRAM_ID
);
const marketId = await getMarketIdForEvent(eventId);
const [event] = PublicKey.findProgramAddressSync(
[Buffer.from("event"), Buffer.from(marketId), Buffer.from(eventId)], PROGRAM_ID
);
const [globalState] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")], PROGRAM_ID
);
const tx = await program.methods
.mintShares(eventId, new anchor.BN(matchedPairId))
.accounts({
globalState,
event,
matchedPair,
yesShareToken,
noShareToken,
mintAuthority: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
return { tx, yesShareToken, noShareToken };
}State Changes: Creates share token accounts for both buyers UI Considerations: Display share token addresses, confirm minting success
The contract includes several other functions for complete market management:
end_primary_market: Close primary trading phase when event startsrefund_unmatched_orders: Refund unmatched orders after primary market closescollect_platform_fees: Admin function to collect accumulated feesprocess_match: Post-match settlement processing
Each follows similar patterns for account derivation, parameter validation, and state management.
import { useWallet, useConnection } from '@solana/wallet-adapter-react';
function TradingInterface() {
const { publicKey, connect, disconnect, signTransaction } = useWallet();
const { connection } = useConnection();
const handleConnect = async () => {
try {
await connect();
// Initialize user state, fetch portfolio, etc.
} catch (error) {
console.error('Wallet connection failed:', error);
}
};
if (!publicKey) {
return <button onClick={handleConnect}>Connect Wallet</button>;
}
return <TradingApp userWallet={publicKey} />;
}function useEventUpdates(eventId: string) {
const [eventData, setEventData] = useState(null);
const { connection } = useConnection();
useEffect(() => {
const marketId = getMarketIdForEvent(eventId);
const [eventAccount] = PublicKey.findProgramAddressSync(
[Buffer.from("event"), Buffer.from(marketId), Buffer.from(eventId)],
PROGRAM_ID
);
const subscription = connection.onAccountChange(
eventAccount,
(accountInfo) => {
const eventData = program.account.event.coder.accounts.decode(
"Event",
accountInfo.data
);
setEventData(eventData);
}
);
return () => {
connection.removeAccountChangeListener(subscription);
};
}, [eventId]);
return eventData;
}async function placeMultipleOrders(orders: OrderParams[]) {
const transaction = new Transaction();
for (const order of orders) {
const instruction = await program.methods
.placeOrder(/* params */)
.accounts(/* accounts */)
.instruction();
transaction.add(instruction);
}
// Sign and send batch transaction
const signature = await program.provider.sendAndConfirm(transaction);
return signature;
}function TradingErrorBoundary({ children }: { children: React.ReactNode }) {
const handleError = (error: Error) => {
if (error.message.includes('6015')) {
return 'Insufficient funds for this order';
}
if (error.message.includes('6021')) {
return 'Primary market has closed for this event';
}
return 'Transaction failed. Please try again.';
};
return (
<ErrorBoundary
fallback={({ error }) => (
<div className="error-message">
{handleError(error)}
</div>
)}
>
{children}
</ErrorBoundary>
);
}function useTransaction() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const executeTransaction = async (transactionFn: () => Promise<string>) => {
setLoading(true);
setError(null);
try {
const signature = await transactionFn();
// Wait for confirmation
const confirmation = await connection.confirmTransaction(
signature,
'confirmed'
);
if (confirmation.value.err) {
throw new Error('Transaction failed');
}
return signature;
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
throw err;
} finally {
setLoading(false);
}
};
return { executeTransaction, loading, error };
}| Code | Error | Frontend Solution |
|---|---|---|
| 6000 | Unauthorized | Check wallet connection and admin rights |
| 6015 | InsufficientFunds | Validate user balance before transaction |
| 6021 | PrimaryMarketClosed | Update UI to disable order placement |
| 6023 | InvalidOrderQuantity | Validate quantity input (1-500 shares) |
| 6024 | InvalidOrderPrice | Validate price input (0 < price < 1 SOL) |
| 6029 | CannotTradeWithSelf | Prevent users from matching their own orders |
| 6031 | InvalidPriceSum | Ensure order prices sum to ~1 SOL for matching |
export function parseAnchorError(error: any): string {
// Extract Anchor error code
const errorCode = error.error?.errorCode?.number;
switch (errorCode) {
case 6015:
return "Insufficient funds. Please add SOL to your wallet.";
case 6021:
return "Primary market has closed. Trading is no longer available.";
case 6023:
return "Invalid quantity. Please enter 1-500 shares.";
case 6024:
return "Invalid price. Price must be between 0 and 1 SOL.";
case 6029:
return "Cannot trade with yourself.";
default:
return error.message || "Transaction failed. Please try again.";
}
}
export function handleTransactionError(error: any) {
console.error('Transaction error:', error);
if (error.code === 4001) {
return "Transaction cancelled by user.";
}
if (error.message.includes('insufficient funds')) {
return "Insufficient SOL for transaction fees.";
}
return parseAnchorError(error);
}# Start local validator
solana-test-validator
# Deploy program locally
anchor build
anchor deploy
# Run tests
anchor testdescribe('BALR Market Integration', () => {
let program: Program<BalrMarket>;
let provider: AnchorProvider;
beforeEach(async () => {
provider = AnchorProvider.env();
program = new Program(idl, PROGRAM_ID, provider);
});
it('should place and match orders', async () => {
// Setup market and event
await createMarket("TEST_MARKET", "Team A", "Team B", futureTimestamp);
await createEvent("TEST_EVENT", "Will Team A win?", 100, 6000);
// Place complementary orders
const yesOrderId = Date.now();
const noOrderId = Date.now() + 1;
await placeOrder({
eventId: "TEST_EVENT",
orderType: "YES",
quantity: 10,
unitPrice: 600_000_000 // 0.6 SOL
});
await placeOrder({
eventId: "TEST_EVENT",
orderType: "NO",
quantity: 10,
unitPrice: 400_000_000 // 0.4 SOL
});
// Match orders
await matchOrders("TEST_EVENT", yesOrderId, noOrderId);
// Verify share tokens created
const yesShares = await program.account.shareToken.fetch(yesShareTokenPDA);
expect(yesShares.quantity.toNumber()).toBe(10);
});
});- Account Derivation: Verify PDA seeds match contract expectations
- Balance Checks: Ensure accounts have sufficient rent-exempt balance
- Transaction Simulation: Use
simulatebeforesendfor debugging - Event Logs: Monitor emitted events for state changes
- Account Data: Fetch account data to verify state updates
- Devnet: Free SOL from faucet, faster iteration, test environment
- Mainnet: Real SOL required, higher stakes, production environment
- Program IDs: May differ between networks
- RPC Endpoints: Different endpoints for different clusters
// Global state
["global_state"] → Platform configuration
// Market
["market", market_id] → Match information
// Event
["event", market_id, event_id] → Betting question
// Order & Escrow
["order", event_id, order_id_bytes] → User order
["escrow", event_id, order_id_bytes] → Order funds
// Shares
["share", event_id, owner, share_type] → User ownership- market_id: String ≤50 chars, unique identifier
- event_id: String ≤50 chars, unique within market
- order_id: u64, timestamp or sequential
- quantity: u64, 1-500 shares per order
- unit_price: u64, lamports per share (0 < price < 1 SOL)
- order_type: "YES" or "NO" enum
- Setup → Connect wallet, derive accounts
- Validate → Check balances, market status
- Execute → Send transaction with proper accounts
- Confirm → Wait for confirmation, handle errors
- Update → Refresh UI state, notify user