Compare commits

..

28 Commits

Author SHA1 Message Date
0105ba063a reward dist signer fix 2025-07-31 00:13:30 +05:30
e409d45e61 reward dist fix 2025-07-30 23:39:21 +05:30
3a9fa10f43 reward dist 2025-07-30 22:24:45 +05:30
364b01c2d3 ticket count and attempts 2025-07-30 12:52:14 +05:30
20184fdc48 seeds constants 2025-07-29 16:23:59 +05:30
230f98975a uid for receipt 2025-07-29 15:28:51 +05:30
9c17c33e35 uid for receipt 2025-07-29 15:27:08 +05:30
1939acaf45 leaderboard complete 2025-07-29 08:36:45 +00:00
60b0ed6641 space issues fixed 2025-07-29 03:07:12 +00:00
5681511a0d leaderboard update 2025-07-29 08:16:00 +05:30
db24f57690 ticket leaderboard 2025-07-28 11:15:18 +05:30
87f612c901 sync 2025-07-25 07:42:45 +05:30
a39517f801 deduct fees native fixed 2025-06-28 04:32:02 +05:30
d3b618fe5a back to no init 2025-06-28 01:31:02 +05:30
00da84183e init if needed, not for vault 2025-06-28 01:20:08 +05:30
3b6421a3f5 back to no init 2025-06-28 01:11:04 +05:30
87e72eb655 init_if_needed 2025-06-28 00:54:28 +05:30
a8cdcb54f3 close bet token, set correct balance 2025-06-20 17:48:58 +05:30
682a927049 account resolution error fix#1 2025-06-20 17:29:52 +05:30
23a98aea9c Merge remote-tracking branch 'origin/prod' into tokens 2025-06-18 20:18:58 +05:30
efedc6ab71 fixed warning 2025-06-18 20:12:17 +05:30
8dedb47f8e token sup 2025-06-18 20:09:02 +05:30
c84d8a4a6d multi currency support v1: 2025-06-15 12:39:43 +05:30
cc73ac1feb clear func 2025-05-21 11:00:43 +00:00
d8db404453 fixes on close 2025-05-21 10:57:01 +00:00
b32b041a14 fees and pay seperated 2025-05-21 09:45:01 +05:30
fee9ac44f4 init 2025-05-21 04:06:05 +00:00
cf8ef365b2 prod at token launch 2025-05-20 15:47:01 +00:00
39 changed files with 4575 additions and 962 deletions

View File

@ -4,15 +4,19 @@
resolution = true resolution = true
skip-lint = false skip-lint = false
[programs.localnet] [programs.devnet]
bets = "Haj94DF925qNRgcoRwQfNsVLKgSmFhG4bjgtvusMkkpD" bets = "Haj94DF925qNRgcoRwQfNsVLKgSmFhG4bjgtvusMkkpD"
[registry] [registry]
url = "https://api.apr.dev" url = "https://api.apr.dev"
[provider] [provider]
cluster = "Localnet" cluster = "Devnet"
wallet = "~/.config/solana/id.json" wallet = "~/.config/solana/id.json"
[scripts] [scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
[test]
startup_wait = 10000
validator = { url = "http://127.0.0.1:8899" }

100
Cargo.lock generated
View File

@ -203,7 +203,7 @@ dependencies = [
"borsh 0.10.4", "borsh 0.10.4",
"bytemuck", "bytemuck",
"getrandom 0.2.15", "getrandom 0.2.15",
"solana-program", "solana-program 1.18.26",
"thiserror", "thiserror",
] ]
@ -436,12 +436,19 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bets" name = "bets"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anchor-lang", "anchor-lang",
"anchor-spl", "anchor-spl",
"solana-program 2.0.3",
] ]
[[package]] [[package]]
@ -1925,13 +1932,59 @@ dependencies = [
"sha3 0.10.8", "sha3 0.10.8",
"solana-frozen-abi", "solana-frozen-abi",
"solana-frozen-abi-macro", "solana-frozen-abi-macro",
"solana-sdk-macro", "solana-sdk-macro 1.18.26",
"thiserror", "thiserror",
"tiny-bip39", "tiny-bip39",
"wasm-bindgen", "wasm-bindgen",
"zeroize", "zeroize",
] ]
[[package]]
name = "solana-program"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70306519f79aa7699264d76d7f4fe252ab22fef3a85404a748a42f8dd750653e"
dependencies = [
"ark-bn254",
"ark-ec",
"ark-ff",
"ark-serialize",
"base64 0.22.1",
"bincode",
"bitflags",
"blake3",
"borsh 0.10.4",
"borsh 1.5.7",
"bs58 0.5.1",
"bv",
"bytemuck",
"bytemuck_derive",
"console_error_panic_hook",
"console_log",
"curve25519-dalek",
"getrandom 0.2.15",
"js-sys",
"lazy_static",
"libsecp256k1",
"log",
"memoffset",
"num-bigint",
"num-derive",
"num-traits",
"parking_lot",
"rand 0.8.5",
"rustc_version",
"rustversion",
"serde",
"serde_bytes",
"serde_derive",
"sha2 0.10.8",
"sha3 0.10.8",
"solana-sdk-macro 2.0.3",
"thiserror",
"wasm-bindgen",
]
[[package]] [[package]]
name = "solana-sdk" name = "solana-sdk"
version = "1.18.26" version = "1.18.26"
@ -1980,8 +2033,8 @@ dependencies = [
"solana-frozen-abi", "solana-frozen-abi",
"solana-frozen-abi-macro", "solana-frozen-abi-macro",
"solana-logger", "solana-logger",
"solana-program", "solana-program 1.18.26",
"solana-sdk-macro", "solana-sdk-macro 1.18.26",
"thiserror", "thiserror",
"uriparse", "uriparse",
"wasm-bindgen", "wasm-bindgen",
@ -2000,6 +2053,19 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "solana-sdk-macro"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44bfdd94b479f125a64f028c31ca6b018cf7ab1a5ebc974f175c54dd56ad58b1"
dependencies = [
"bs58 0.5.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.100",
]
[[package]] [[package]]
name = "solana-security-txt" name = "solana-security-txt"
version = "1.1.1" version = "1.1.1"
@ -2028,7 +2094,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha3 0.9.1", "sha3 0.9.1",
"solana-program", "solana-program 1.18.26",
"solana-sdk", "solana-sdk",
"subtle", "subtle",
"thiserror", "thiserror",
@ -2045,7 +2111,7 @@ dependencies = [
"borsh 1.5.7", "borsh 1.5.7",
"num-derive", "num-derive",
"num-traits", "num-traits",
"solana-program", "solana-program 1.18.26",
"spl-token", "spl-token",
"spl-token-2022", "spl-token-2022",
"thiserror", "thiserror",
@ -2058,7 +2124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "210101376962bb22bb13be6daea34656ea1cbc248fce2164b146e39203b55e03" checksum = "210101376962bb22bb13be6daea34656ea1cbc248fce2164b146e39203b55e03"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"solana-program", "solana-program 1.18.26",
"spl-discriminator-derive", "spl-discriminator-derive",
] ]
@ -2092,7 +2158,7 @@ version = "4.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a49f49f95f2d02111ded31696ab38a081fab623d4c76bd4cb074286db4560836" checksum = "a49f49f95f2d02111ded31696ab38a081fab623d4c76bd4cb074286db4560836"
dependencies = [ dependencies = [
"solana-program", "solana-program 1.18.26",
] ]
[[package]] [[package]]
@ -2103,7 +2169,7 @@ checksum = "c52d84c55efeef8edcc226743dc089d7e3888b8e3474569aa3eff152b37b9996"
dependencies = [ dependencies = [
"borsh 1.5.7", "borsh 1.5.7",
"bytemuck", "bytemuck",
"solana-program", "solana-program 1.18.26",
"solana-zk-token-sdk", "solana-zk-token-sdk",
"spl-program-error", "spl-program-error",
] ]
@ -2116,7 +2182,7 @@ checksum = "e45a49acb925db68aa501b926096b2164adbdcade7a0c24152af9f0742d0a602"
dependencies = [ dependencies = [
"num-derive", "num-derive",
"num-traits", "num-traits",
"solana-program", "solana-program 1.18.26",
"spl-program-error-derive", "spl-program-error-derive",
"thiserror", "thiserror",
] ]
@ -2140,7 +2206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fab8edfd37be5fa17c9e42c1bff86abbbaf0494b031b37957f2728ad2ff842ba" checksum = "fab8edfd37be5fa17c9e42c1bff86abbbaf0494b031b37957f2728ad2ff842ba"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"solana-program", "solana-program 1.18.26",
"spl-discriminator", "spl-discriminator",
"spl-pod", "spl-pod",
"spl-program-error", "spl-program-error",
@ -2158,7 +2224,7 @@ dependencies = [
"num-derive", "num-derive",
"num-traits", "num-traits",
"num_enum", "num_enum",
"solana-program", "solana-program 1.18.26",
"thiserror", "thiserror",
] ]
@ -2173,7 +2239,7 @@ dependencies = [
"num-derive", "num-derive",
"num-traits", "num-traits",
"num_enum", "num_enum",
"solana-program", "solana-program 1.18.26",
"solana-security-txt", "solana-security-txt",
"solana-zk-token-sdk", "solana-zk-token-sdk",
"spl-memo", "spl-memo",
@ -2193,7 +2259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "014817d6324b1e20c4bbc883e8ee30a5faa13e59d91d1b2b95df98b920150c17" checksum = "014817d6324b1e20c4bbc883e8ee30a5faa13e59d91d1b2b95df98b920150c17"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"solana-program", "solana-program 1.18.26",
"spl-discriminator", "spl-discriminator",
"spl-pod", "spl-pod",
"spl-program-error", "spl-program-error",
@ -2206,7 +2272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3da00495b602ebcf5d8ba8b3ecff1ee454ce4c125c9077747be49c2d62335ba" checksum = "f3da00495b602ebcf5d8ba8b3ecff1ee454ce4c125c9077747be49c2d62335ba"
dependencies = [ dependencies = [
"borsh 1.5.7", "borsh 1.5.7",
"solana-program", "solana-program 1.18.26",
"spl-discriminator", "spl-discriminator",
"spl-pod", "spl-pod",
"spl-program-error", "spl-program-error",
@ -2221,7 +2287,7 @@ checksum = "a9b5c08a89838e5a2931f79b17f611857f281a14a2100968a3ccef352cb7414b"
dependencies = [ dependencies = [
"arrayref", "arrayref",
"bytemuck", "bytemuck",
"solana-program", "solana-program 1.18.26",
"spl-discriminator", "spl-discriminator",
"spl-pod", "spl-pod",
"spl-program-error", "spl-program-error",
@ -2236,7 +2302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c872f93d0600e743116501eba2d53460e73a12c9a496875a42a7d70e034fe06d" checksum = "c872f93d0600e743116501eba2d53460e73a12c9a496875a42a7d70e034fe06d"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"solana-program", "solana-program 1.18.26",
"spl-discriminator", "spl-discriminator",
"spl-pod", "spl-pod",
"spl-program-error", "spl-program-error",

1
bfk3.json Normal file
View File

@ -0,0 +1 @@
[35,174,214,54,242,1,199,216,163,198,236,156,136,174,255,23,227,139,41,35,8,67,70,64,116,179,59,110,68,243,2,102,222,40,2,247,155,101,212,33,251,2,171,60,245,235,194,209,166,57,123,68,77,134,242,10,253,213,189,51,50,119,177,7]

1
bk2.json Normal file
View File

@ -0,0 +1 @@
[148,41,155,174,198,60,158,184,194,95,32,154,56,15,105,221,184,104,78,95,198,191,73,206,7,204,237,221,77,118,10,185,141,91,28,223,24,106,252,190,71,7,81,159,21,139,95,162,137,133,175,102,147,41,2,59,136,124,237,109,240,37,214,136]

1
bk5.json Normal file
View File

@ -0,0 +1 @@
[175,127,97,183,211,241,79,174,178,61,65,104,189,207,155,197,146,250,192,93,8,137,68,245,101,185,15,191,159,76,243,215,221,10,222,76,239,168,2,136,183,93,220,122,236,171,209,22,233,114,77,52,207,194,223,80,251,189,50,24,185,22,89,66]

2606
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,13 +8,16 @@
"@coral-xyz/anchor": "^0.30.1" "@coral-xyz/anchor": "^0.30.1"
}, },
"devDependencies": { "devDependencies": {
"@solana/spl-token": "^0.4.13",
"@types/bn.js": "^5.1.0", "@types/bn.js": "^5.1.0",
"@types/chai": "^5.2.1", "@types/chai": "^5.2.1",
"@types/mocha": "^10.0.10", "@types/mocha": "^10.0.10",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"anchor-bankrun": "^0.5.0",
"chai": "^4.3.4", "chai": "^4.3.4",
"mocha": "^9.0.3", "mocha": "^9.0.3",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"solana-bankrun": "^0.4.0",
"ts-mocha": "^10.0.0", "ts-mocha": "^10.0.0",
"typescript": "^4.3.5" "typescript": "^4.3.5"
} }

View File

@ -14,8 +14,12 @@ cpi = ["no-entrypoint"]
no-entrypoint = [] no-entrypoint = []
no-idl = [] no-idl = []
no-log-ix-name = [] no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"] idl-build = [
"anchor-lang/idl-build",
"anchor-spl/idl-build"
]
[dependencies] [dependencies]
anchor-lang = "0.30.1" anchor-lang = {version="0.30.1", features=["init-if-needed"]}
anchor-spl = "0.30.1" anchor-spl="0.30.1"
solana-program= "=2.0.3"

View File

@ -5,3 +5,18 @@ pub const SEED: &str = "anchor";
#[constant] #[constant]
pub const FEE_COLLECTOR: &str= "9esrj2X33pr5og6fdkDMjaW6fdnnb9hT1cWshamxTdL4"; pub const FEE_COLLECTOR: &str= "9esrj2X33pr5og6fdkDMjaW6fdnnb9hT1cWshamxTdL4";
#[constant]
pub const TICKET_SALE_VAULT_ADDRESS: &str= "9esrj2X33pr5og6fdkDMjaW6fdnnb9hT1cWshamxTdL4";
#[constant]
pub const RECEIPT_ACCOUNT_SEED: &[u8] = b"receipt_account";
#[constant]
pub const TICKET_LEADERBOARD_LIST_SEED: &[u8] = b"ticket_leaderboards_list";
#[constant]
pub const TICKET_LEADERBOARD_SEED: &[u8] = b"ticket_leaderboard";
#[constant]
pub const LEADERBOARD_ENTRY_SEED: &[u8] = b"leaderboard_entry";

View File

@ -17,6 +17,26 @@ pub enum BettingError {
InvalidWinner, InvalidWinner,
#[msg("Please use the correct fee collector address")] #[msg("Please use the correct fee collector address")]
InvalidFeeCollector, InvalidFeeCollector,
#[msg("")] #[msg("Joiner account not provided")]
JoinerAccountNotProvided JoinerAccountNotProvided,
#[msg("Insufficient funds")]
InsufficientFunds,
#[msg("Fee calculation error")]
FeeCalculationError,
#[msg("Invalid ticket sale vault address")]
InvalidTicketSaleVault,
}
#[error_code]
pub enum LeaderboardError {
#[msg("Invalid id")]
InvalidId,
#[msg("Not active")]
NotActive,
}
#[error_code]
pub enum ArithmeticError {
#[msg("Arithmetic error")]
ArithmeticError,
} }

View File

@ -0,0 +1,81 @@
use anchor_lang::prelude::*;
use anchor_spl::{associated_token::AssociatedToken, token::{transfer_checked, TransferChecked}, token_interface::{TokenAccount, TokenInterface}};
use crate::*;
pub fn buy(ctx: Context<BuyTickets>, amount: u64) -> Result<()> {
let transfer_amount = 100000000; // 0.1 SOL in lamports
let ix = anchor_lang::solana_program::system_instruction::transfer(
&ctx.accounts.payer.key(),
&ctx.accounts.ticket_leaderboard_list.key(),
transfer_amount
);
anchor_lang::solana_program::program::invoke(
&ix,
&[
ctx.accounts.payer.to_account_info(),
ctx.accounts.ticket_leaderboard_list.to_account_info(),
],
)?;
// Transfer tokens from leaderboard vault to user vault
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_accounts = TransferChecked {
from: ctx.accounts.ticket_leaderboard_list_vault.to_account_info(),
to: ctx.accounts.user_token_vault.to_account_info(),
authority: ctx.accounts.ticket_leaderboard_list.to_account_info(),
mint: ctx.accounts.token_mint.to_account_info(),
};
let seeds = &[
TICKET_LEADERBOARD_LIST_SEED.as_ref(),
&[ctx.bumps.ticket_leaderboard_list][..]
];
let signer_seeds = &[&seeds[..]];
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
transfer_checked(cpi_ctx, amount, 9)?;
Ok(())
}
#[derive(Accounts)]
pub struct BuyTickets<'info>{
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [TICKET_LEADERBOARD_LIST_SEED],
bump
)]
pub ticket_leaderboard_list: Account<'info, TicketLeaderboardList>,
#[account(
init_if_needed,
payer=payer,
associated_token::mint = token_mint,
associated_token::authority = ticket_leaderboard_list,
associated_token::token_program = token_program
)]
pub ticket_leaderboard_list_vault: InterfaceAccount<'info, TokenAccount>,
#[account(
init_if_needed,
payer = payer,
associated_token::mint = token_mint,
associated_token::authority = payer,
associated_token::token_program = token_program
)]
pub user_token_vault: InterfaceAccount<'info, TokenAccount>,
pub token_mint: InterfaceAccount<'info, anchor_spl::token_interface::Mint>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

View File

@ -0,0 +1,22 @@
use anchor_lang::prelude::*;
use crate::*;
pub fn clear(ctx: Context<ClearBetsList>) -> Result<()> {
let bets_list = &mut ctx.accounts.bets_list;
bets_list.bets.clear();
msg!("Cleared all bets from the list!");
Ok(())
}
#[derive(Accounts)]
pub struct ClearBetsList<'info> {
#[account(
mut,
seeds = [b"bets_list"],
bump
)]
pub bets_list: Account<'info, BetsList>,
#[account(mut)]
pub payer: Signer<'info>,
}

View File

@ -1,145 +1,34 @@
use std::str::FromStr;
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
use crate::{error::BettingError, *}; use crate::{error::BettingError, *};
pub fn close(ctx: Context<CloseBet>, winner: Pubkey, userid: String) -> Result<()> { pub fn close(ctx: Context<CloseBet>, winner:Pubkey, userid:String)->Result<()>{
let bet_vault = &mut ctx.accounts.bet_vault; let bet_vault = &mut ctx.accounts.bet_vault;
require!( require!(
bet_vault.owner == winner || bet_vault.joiner == winner || bet_vault.owner_id == userid || bet_vault.joiner_id == userid, bet_vault.owner == winner || bet_vault.joiner == winner || bet_vault.owner_id == userid || bet_vault.joiner_id == userid,
BettingError::InvalidWinner BettingError::InvalidWinner
); );
let fee_collector_pubkey = Pubkey::from_str(FEE_COLLECTOR).map_err(|_| BettingError::InvalidFeeCollector)?;
require_keys_eq!(
ctx.accounts.fee_wallet.key(),
fee_collector_pubkey,
BettingError::InvalidFeeCollector
);
let is_native_sol = bet_vault.is_native_sol;
let bet_vault_key = bet_vault.key();
if is_native_sol {
// Calculate the 5% fee
let total_lamports = **bet_vault.to_account_info().lamports.borrow();
let mut fee = total_lamports / 20; // 5%
let referrer_fee = fee / 4; //50% for each referrer
// Handle referrer fees
if let Some(owner_ref) = &ctx.accounts.owner_referrer {
fee -= referrer_fee;
**bet_vault.to_account_info().try_borrow_mut_lamports()? -= referrer_fee;
**owner_ref.try_borrow_mut_lamports()? += referrer_fee;
}
if let Some(joiner_ref) = &ctx.accounts.joiner_referrer {
fee -= referrer_fee;
**bet_vault.to_account_info().try_borrow_mut_lamports()? -= referrer_fee;
**joiner_ref.try_borrow_mut_lamports()? += referrer_fee;
}
**bet_vault.to_account_info().try_borrow_mut_lamports()? -= fee;
**ctx.accounts.fee_wallet.to_account_info().try_borrow_mut_lamports()? += fee;
} else {
// Handle SPL token fees
let bet_vault_token_account = ctx.accounts.bet_vault_token_account.as_ref().ok_or(BettingError::BetNotFilled)?;
let fee_wallet_token_account = ctx.accounts.fee_wallet_token_account.as_ref().ok_or(BettingError::BetNotFilled)?;
let token_account = TokenAccount::try_deserialize(&mut &bet_vault_token_account.data.borrow()[..])?;
let total_tokens = token_account.amount;
let mut fee = total_tokens / 20; // 5%
let referrer_fee = fee / 4; // 50% for each referrer
// Handle referrer fees
if let Some(owner_ref) = &ctx.accounts.owner_referrer_token_account {
fee -= referrer_fee;
let cpi_accounts = Transfer {
from: bet_vault_token_account.to_account_info(),
to: owner_ref.to_account_info(),
authority: ctx.accounts.bet_vault.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
token::transfer(cpi_ctx, referrer_fee)?;
}
if let Some(joiner_ref) = &ctx.accounts.joiner_referrer_token_account {
fee -= referrer_fee;
let cpi_accounts = Transfer {
from: bet_vault_token_account.to_account_info(),
to: joiner_ref.to_account_info(),
authority: ctx.accounts.bet_vault.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
token::transfer(cpi_ctx, referrer_fee)?;
}
// Transfer fee to fee wallet
let cpi_accounts = Transfer {
from: bet_vault_token_account.to_account_info(),
to: fee_wallet_token_account.to_account_info(),
authority: ctx.accounts.bet_vault.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
token::transfer(cpi_ctx, fee)?;
}
let bets_list = &mut ctx.accounts.bets_list; let bets_list = &mut ctx.accounts.bets_list;
// Remove the bet_vault public key from the list // Remove the bet_vault public key from the list
bets_list.bets.retain(|&bet| bet != bet_vault_key); bets_list.bets.retain(|&bet| bet != bet_vault.key());
msg!("Bet {} closed by {}", bet_vault_key, winner);
Ok(()) Ok(())
} }
#[derive(Accounts)] #[derive(Accounts)]
pub struct CloseBet<'info> { pub struct CloseBet<'info>{
#[account(mut)] #[account(mut)]
pub bets_list: Account<'info, BetsList>, pub bets_list: Account<'info, BetsList>,
#[account(mut, close = winner)] #[account(mut, close=winner)]
pub bet_vault: Account<'info, BetVault>, pub bet_vault: Account<'info, BetVault>,
/// CHECK: This is validated against the FEE_COLLECTOR constant.
#[account(mut)]
pub fee_wallet: AccountInfo<'info>,
#[account(mut)] #[account(mut)]
pub winner: SystemAccount<'info>, pub winner: SystemAccount<'info>,
#[account(mut)] #[account(mut)]
pub payer: Signer<'info>, pub payer: Signer<'info>,
#[account(mut)] pub system_program: Program<'info, System>
pub owner_referrer: Option<AccountInfo<'info>>,
#[account(mut)]
pub joiner_referrer: Option<AccountInfo<'info>>,
pub system_program: Program<'info, System>,
// SPL Token accounts
#[account(mut)]
pub bet_vault_token_account: Option<AccountInfo<'info>>,
#[account(mut)]
pub fee_wallet_token_account: Option<AccountInfo<'info>>,
#[account(mut)]
pub owner_referrer_token_account: Option<AccountInfo<'info>>,
#[account(mut)]
pub joiner_referrer_token_account: Option<AccountInfo<'info>>,
pub token_program: Program<'info, Token>,
} }

View File

@ -0,0 +1,86 @@
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{TokenAccount, TokenInterface},
};
use crate::{error::BettingError, shared::transfer_tokens_pda, state::{BetVault, BetsList}};
pub fn close_token_bet(ctx: Context<CloseBetToken>, winner: Pubkey, userid: String, owner:Pubkey, game_id:String, nonce:u64) -> Result<()> {
let bet_vault = &mut ctx.accounts.bet_vault;
require!(
bet_vault.owner == winner || bet_vault.joiner == winner || bet_vault.owner_id == userid || bet_vault.joiner_id == userid,
BettingError::InvalidWinner
);
let seeds = &[
b"bet_vault",
bet_vault.owner.as_ref(),
bet_vault.game_id.as_bytes(),
&bet_vault.nonce.to_le_bytes(),
];
// Get the current token balance of the bet vault
let token_balance = ctx.accounts.token_vault.amount;
// Transfer all tokens from vault to winner
transfer_tokens_pda(
&ctx.accounts.token_vault,
&ctx.accounts.winner_token_account,
token_balance,
&ctx.accounts.token_mint,
&bet_vault,
&ctx.accounts.token_program,
seeds,
ctx.bumps.bet_vault,
)?;
// Remove bet from global list
let bets_list = &mut ctx.accounts.bets_list;
if let Some(pos) = bets_list.bets.iter().position(|x| *x == bet_vault.key()) {
bets_list.bets.remove(pos);
}
msg!("Bet closed and {} tokens transferred to winner {}", token_balance, winner);
Ok(())
}
#[derive(Accounts)]
#[instruction( winner: Pubkey, userid: String,owner:Pubkey, game_id:String, nonce:u64)]
pub struct CloseBetToken<'info> {
#[account(mut)]
pub bets_list: Account<'info, BetsList>,
#[account(
mut,
seeds = [b"bet_vault", owner.as_ref(), game_id.as_bytes(), &nonce.to_le_bytes()],
bump
)]
pub bet_vault: Account<'info, BetVault>,
#[account(mut)]
pub winner: SystemAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_mint: InterfaceAccount<'info, anchor_spl::token_interface::Mint>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = bet_vault,
associated_token::token_program = token_program
)]
pub token_vault: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = winner,
associated_token::token_program = token_program
)]
pub winner_token_account: InterfaceAccount<'info, TokenAccount>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

View File

@ -1,17 +1,7 @@
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer}; use crate::*;
use crate::{error::BettingError, *};
pub fn create( pub fn create(ctx: Context<CreateBet>, wager: u64, user_id:String, game_id:String, _nonce:u64) -> Result<()> {
ctx: Context<CreateBet>,
wager: u64,
user_id: String,
game_id: String,
_nonce: u64,
is_native_sol: bool,
token_mint: Option<Pubkey>,
token_decimals: u8,
) -> Result<()> {
let bets_list = &mut ctx.accounts.bets_list; let bets_list = &mut ctx.accounts.bets_list;
let bet_vault = &mut ctx.accounts.bet_vault; let bet_vault = &mut ctx.accounts.bet_vault;
let payer = &ctx.accounts.payer; let payer = &ctx.accounts.payer;
@ -20,54 +10,26 @@ pub fn create(
// Store bet details // Store bet details
bet_vault.game_id = game_id; bet_vault.game_id = game_id;
bet_vault.owner = payer.key(); bet_vault.owner = payer.key();
bet_vault.owner_id = user_id; bet_vault.owner_id= user_id;
bet_vault.wager = wager; bet_vault.wager = wager;
bet_vault.is_native_sol = is_native_sol; bet_vault.nonce = _nonce;
bet_vault.token_mint = token_mint; // Transfer SOL from the payer to the bet vault
bet_vault.token_decimals = token_decimals; let cpi_accounts = anchor_lang::system_program::Transfer {
from: payer.to_account_info(),
if is_native_sol { to: bet_vault.to_account_info(),
// Transfer SOL from the payer to the bet vault };
let cpi_accounts = anchor_lang::system_program::Transfer { let cpi_ctx = CpiContext::new(system_program.to_account_info(), cpi_accounts);
from: payer.to_account_info(), anchor_lang::system_program::transfer(cpi_ctx, wager)?;
to: bet_vault.to_account_info(),
};
let cpi_ctx = CpiContext::new(system_program.to_account_info(), cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, wager)?;
} else {
// Transfer SPL tokens
let payer_token_account = ctx.accounts.payer_token_account.as_ref().ok_or(BettingError::BetNotFilled)?;
let bet_vault_token_account = ctx.accounts.bet_vault_token_account.as_ref().ok_or(BettingError::BetNotFilled)?;
let cpi_accounts = Transfer {
from: payer_token_account.to_account_info(),
to: bet_vault_token_account.to_account_info(),
authority: payer.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
token::transfer(cpi_ctx, wager)?;
}
// Add this bet to the global list // Add this bet to the global list
bets_list.bets.push(bet_vault.key()); bets_list.bets.push(bet_vault.key());
msg!("New bet {} created with {} tokens!", bet_vault.key(), wager); msg!("New bet {} created with {} lamports!", bet_vault.key(), wager);
Ok(()) Ok(())
} }
#[derive(Accounts)] #[derive(Accounts)]
#[instruction( #[instruction(wager: u64,user_id:String, game_id: String, _nonce:u64)]
wager: u64,
user_id: String,
game_id: String,
_nonce: u64,
is_native_sol: bool,
token_mint: Option<Pubkey>,
token_decimals: u8
)]
pub struct CreateBet<'info> { pub struct CreateBet<'info> {
#[account(mut)] #[account(mut)]
pub payer: Signer<'info>, pub payer: Signer<'info>,
@ -78,20 +40,11 @@ pub struct CreateBet<'info> {
#[account( #[account(
init, init,
payer = payer, payer = payer,
space = 8 + BetVault::INIT_SPACE, space = 8 + BetVault::INIT_SPACE, // Owner (Pubkey) + Wager (u64)
seeds = [b"bet_vault", payer.key().as_ref(), &game_id.as_bytes(), &_nonce.to_le_bytes()], seeds = [b"bet_vault", payer.key().as_ref(), &game_id.as_bytes(), &_nonce.to_le_bytes()],
bump bump
)] )]
pub bet_vault: Account<'info, BetVault>, pub bet_vault: Account<'info, BetVault>,
pub system_program: Program<'info, System>, pub system_program: Program<'info, System>,
// SPL Token accounts
#[account(mut)]
pub payer_token_account: Option<AccountInfo<'info>>,
#[account(mut)]
pub bet_vault_token_account: Option<AccountInfo<'info>>,
pub token_program: Program<'info, Token>,
} }

View File

@ -0,0 +1,78 @@
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{Mint, TokenAccount, TokenInterface},
};
use crate::{shared::transfer_tokens, *};
pub fn create_token_bet(ctx: Context<CreateBetToken>, wager: u64, user_id:String, game_id:String, _nonce:u64) -> Result<()> {
let bets_list = &mut ctx.accounts.bets_list;
let bet_vault = &mut ctx.accounts.bet_vault;
let payer = &ctx.accounts.payer;
// Store bet details
bet_vault.game_id = game_id;
bet_vault.owner = payer.key();
bet_vault.owner_id= user_id;
bet_vault.wager = wager;
bet_vault.token_mint = ctx.accounts.token_mint.key();
bet_vault.nonce = _nonce;
transfer_tokens(
&ctx.accounts.payer_token_account,
&ctx.accounts.token_vault,
&wager, &ctx.accounts.token_mint,
&ctx.accounts.payer,
&ctx.accounts.token_program
)?;
// Add this bet to the global list
bets_list.bets.push(bet_vault.key());
msg!("New bet {} created with {} lamports!", bet_vault.key(), wager);
Ok(())
}
#[derive(Accounts)]
#[instruction(wager: u64, user_id: String, game_id: String, _nonce: u64)]
pub struct CreateBetToken<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mint::token_program= token_program)]
pub token_mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub bets_list: Account<'info, BetsList>,
#[account(
init,
payer = payer,
space = 8 + BetVault::INIT_SPACE, // Owner (Pubkey) + Wager (u64)
seeds = [b"bet_vault", payer.key().as_ref(), &game_id.as_bytes(), &_nonce.to_le_bytes()],
bump
)]
pub bet_vault: Account<'info, BetVault>,
#[account(
mut,
associated_token::mint= token_mint,
associated_token::authority= payer,
associated_token::token_program= token_program
)]
pub payer_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
init,
payer= payer,
associated_token::mint= token_mint,
associated_token::authority= bet_vault,
associated_token::token_program= token_program
)]
pub token_vault: InterfaceAccount<'info, TokenAccount>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

View File

@ -0,0 +1,58 @@
use anchor_lang::prelude::*;
use anchor_spl::{associated_token::AssociatedToken, token_interface::{TokenAccount, TokenInterface}};
use crate::*;
pub fn create(ctx: Context<CreateLeaderboard>, id:u64, game_id:String, entry_ticket_count:u64, max_attempt_count:u64) -> Result<()> {
let ticket_leaderboard_list = &mut ctx.accounts.ticket_leaderboard_list;
let ticket_leaderboard = &mut ctx.accounts.ticket_leaderboard;
ticket_leaderboard.id = id;
ticket_leaderboard.players = vec![];
ticket_leaderboard.game_id = game_id;
ticket_leaderboard.is_over = false;
ticket_leaderboard.entry_ticket_count = entry_ticket_count;
ticket_leaderboard.max_attempts = max_attempt_count;
ticket_leaderboard_list.leaderboards.push(ticket_leaderboard.key());
msg!("Leaderboard created!");
Ok(())
}
#[derive(Accounts)]
#[instruction(id:u64)]
pub struct CreateLeaderboard<'info>{
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [TICKET_LEADERBOARD_LIST_SEED],
bump
)]
pub ticket_leaderboard_list: Account<'info, TicketLeaderboardList>,
#[account(
init,
payer = payer,
space = 8 + TicketLeaderboard::INIT_SPACE,
seeds = [TICKET_LEADERBOARD_SEED, id.to_le_bytes().as_ref()],
bump
)]
pub ticket_leaderboard: Account<'info, TicketLeaderboard>,
#[account(
init,
payer = payer,
associated_token::mint = token_mint,
associated_token::authority = ticket_leaderboard,
associated_token::token_program = token_program
)]
pub token_vault: InterfaceAccount<'info, TokenAccount>,
pub token_mint: InterfaceAccount<'info, anchor_spl::token_interface::Mint>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

View File

@ -0,0 +1,101 @@
use std::str::FromStr;
use anchor_lang::prelude::*;
use crate::{error::BettingError, *};
pub fn deduct(ctx: Context<DeductFees>, winner:Pubkey, userid:String)->Result<()>{
let bet_vault = &mut ctx.accounts.bet_vault;
require!(
bet_vault.owner == winner || bet_vault.joiner == winner || bet_vault.owner_id == userid || bet_vault.joiner_id == userid,
BettingError::InvalidWinner
);
let fee_collector_pubkey = Pubkey::from_str(FEE_COLLECTOR).map_err(|_| BettingError::InvalidFeeCollector)?;
require_keys_eq!(
ctx.accounts.fee_wallet.key(),
fee_collector_pubkey,
BettingError::InvalidFeeCollector
);
// Calculate the 5% fee
let total_lamports = **bet_vault.to_account_info().lamports.borrow();
msg!("Total lamports: {}", total_lamports);
let fee = total_lamports / 40; // 2.5%
// Calculate rent-exempt minimum balance
let rent_exempt_minimum = Rent::get()?.minimum_balance(bet_vault.to_account_info().data_len());
msg!("Rent exempt minimum: {}", rent_exempt_minimum);
let referrer_fee = total_lamports / 80; //1.25% for each referrer
msg!("Fee: {}, Referrer fee: {}", fee, referrer_fee);
msg!("Total to be sent: {}", fee + referrer_fee + referrer_fee);
// Calculate rent-exempt minimum for referrer accounts
let referrer_rent_exempt = Rent::get()?.minimum_balance(0); // 0 bytes for basic account
msg!("Referrer rent exempt minimum: {}", referrer_rent_exempt);
// Verify we have enough funds for all transfers
let total_transfer = fee.checked_add(referrer_fee)
.and_then(|sum| sum.checked_add(referrer_fee))
.ok_or(BettingError::InsufficientFunds)?;
require!(
total_transfer <= total_lamports,
BettingError::InsufficientFunds
);
// Check if referrer accounts have enough SOL for rent
let owner_referrer_balance = **ctx.accounts.owner_referrer.lamports.borrow();
let joiner_referrer_balance = **ctx.accounts.joiner_referrer.lamports.borrow();
require!(
owner_referrer_balance + referrer_fee >= referrer_rent_exempt,
BettingError::InsufficientFunds
);
require!(
joiner_referrer_balance + referrer_fee >= referrer_rent_exempt,
BettingError::InsufficientFunds
);
// Transfer referrer fees
**bet_vault.to_account_info().try_borrow_mut_lamports()? -= referrer_fee;
**ctx.accounts.owner_referrer.try_borrow_mut_lamports()? += referrer_fee;
msg!("After owner referrer transfer - Vault balance: {}", **bet_vault.to_account_info().lamports.borrow());
**bet_vault.to_account_info().try_borrow_mut_lamports()? -= referrer_fee;
**ctx.accounts.joiner_referrer.try_borrow_mut_lamports()? += referrer_fee;
msg!("After joiner referrer transfer - Vault balance: {}", **bet_vault.to_account_info().lamports.borrow());
**bet_vault.to_account_info().try_borrow_mut_lamports()? -= fee;
**ctx.accounts.fee_wallet.to_account_info().try_borrow_mut_lamports()? += fee;
msg!("After fee transfer - Vault balance: {}", **bet_vault.to_account_info().lamports.borrow());
Ok(())
}
#[derive(Accounts)]
pub struct DeductFees<'info>{
#[account(mut)]
pub bets_list: Account<'info, BetsList>,
#[account(mut)]
pub bet_vault: Account<'info, BetVault>,
/// CHECK: This is validated against the FEE_COLLECTOR constant.
#[account(mut)]
pub fee_wallet: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: No constraints are applied to this account.
#[account(mut)]
pub owner_referrer: AccountInfo<'info>,
/// CHECK: No constraints are applied to this account.
#[account(mut)]
pub joiner_referrer: AccountInfo<'info>,
pub system_program: Program<'info, System>
}

View File

@ -0,0 +1,157 @@
use std::str::FromStr;
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{TokenAccount, TokenInterface},
};
use crate::{error::BettingError, shared::transfer_tokens_pda, *};
pub fn handle_deduct_fees_token(ctx: Context<DeductFeesToken>, winner: Pubkey, userid: String, owner:Pubkey, game_id:String, nonce:u64) -> Result<()> {
let bet_vault = &mut ctx.accounts.bet_vault;
require!(
bet_vault.owner == winner || bet_vault.joiner == winner || bet_vault.owner_id == userid || bet_vault.joiner_id == userid,
BettingError::InvalidWinner
);
let fee_collector_pubkey = Pubkey::from_str(FEE_COLLECTOR).map_err(|_| BettingError::InvalidFeeCollector)?;
require_keys_eq!(
ctx.accounts.fee_wallet.key(),
fee_collector_pubkey,
BettingError::InvalidFeeCollector
);
// Calculate the fees (2.5% platform fee, 1.25% for each referrer)
let total_tokens = bet_vault.wager;
let fee = total_tokens.checked_div(40).ok_or(BettingError::InsufficientFunds)?; // 2.5%
let referrer_fee = total_tokens.checked_div(80).ok_or(BettingError::InsufficientFunds)?; // 1.25% for each referrer
msg!("Total tokens: {}", total_tokens);
msg!("Fee: {}, Referrer fee: {}", fee, referrer_fee);
msg!("Total to be sent: {}", fee + referrer_fee + referrer_fee);
// Verify we have enough funds for all transfers
let total_transfer = fee.checked_add(referrer_fee)
.and_then(|sum| sum.checked_add(referrer_fee))
.ok_or(BettingError::InsufficientFunds)?;
require!(
total_transfer <= total_tokens,
BettingError::InsufficientFunds
);
// Get PDA seeds and bump
let seeds = &[
b"bet_vault",
bet_vault.owner.as_ref(),
bet_vault.game_id.as_bytes(),
&bet_vault.nonce.to_le_bytes(),
];
// Transfer referrer fees
transfer_tokens_pda(
&ctx.accounts.token_vault,
&ctx.accounts.owner_referrer_token_account,
referrer_fee,
&ctx.accounts.token_mint,
&bet_vault,
&ctx.accounts.token_program,
seeds,
ctx.bumps.bet_vault,
)?;
msg!("Transferred {} tokens to owner referrer", referrer_fee);
transfer_tokens_pda(
&ctx.accounts.token_vault,
&ctx.accounts.joiner_referrer_token_account,
referrer_fee,
&ctx.accounts.token_mint,
&bet_vault,
&ctx.accounts.token_program,
seeds,
ctx.bumps.bet_vault,
)?;
msg!("Transferred {} tokens to joiner referrer", referrer_fee);
// Transfer platform fee
transfer_tokens_pda(
&ctx.accounts.token_vault,
&ctx.accounts.fee_wallet_token_account,
fee,
&ctx.accounts.token_mint,
&bet_vault,
&ctx.accounts.token_program,
seeds,
ctx.bumps.bet_vault,
)?;
msg!("Transferred {} tokens to fee wallet", fee);
Ok(())
}
#[derive(Accounts)]
#[instruction(winner: Pubkey, userid: String,owner:Pubkey, game_id:String, nonce:u64)]
pub struct DeductFeesToken<'info> {
#[account(mut)]
pub bets_list: Account<'info, BetsList>,
#[account(
mut,
seeds = [b"bet_vault", owner.as_ref(), game_id.as_bytes(), &nonce.to_le_bytes()],
bump
)]
pub bet_vault: Account<'info, BetVault>,
/// CHECK: This is validated against the FEE_COLLECTOR constant.
#[account(mut)]
pub fee_wallet: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: No constraints are applied to this account.
#[account(mut)]
pub owner_referrer: AccountInfo<'info>,
/// CHECK: No constraints are applied to this account.
#[account(mut)]
pub joiner_referrer: AccountInfo<'info>,
pub token_mint: InterfaceAccount<'info, anchor_spl::token_interface::Mint>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = bet_vault,
associated_token::token_program = token_program
)]
pub token_vault: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = fee_wallet,
associated_token::token_program = token_program
)]
pub fee_wallet_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = owner_referrer,
associated_token::token_program = token_program
)]
pub owner_referrer_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = joiner_referrer,
associated_token::token_program = token_program
)]
pub joiner_referrer_token_account: InterfaceAccount<'info, TokenAccount>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

View File

@ -0,0 +1,75 @@
use anchor_lang::prelude::*;
use anchor_spl::{associated_token::AssociatedToken, token_interface::{TokenAccount, TokenInterface}};
use crate::*;
pub fn enter(ctx: Context<EnterLeaderboard>, id:u64, uid:String) -> Result<()> {
let payer = &ctx.accounts.payer;
let receipt = &mut ctx.accounts.receipt_account;
let leaderboard = &ctx.accounts.ticket_leaderboard;
// Transfer 1 token from user's vault to leaderboard vault
let transfer_amount = 1u64 * leaderboard.entry_ticket_count;
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_accounts = anchor_spl::token_interface::TransferChecked {
from: ctx.accounts.user_token_vault.to_account_info(),
to: ctx.accounts.token_vault.to_account_info(),
authority: payer.to_account_info(),
mint: ctx.accounts.token_mint.to_account_info(),
};
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
anchor_spl::token_interface::transfer_checked(cpi_ctx, transfer_amount, ctx.accounts.token_mint.decimals)?;
receipt.active = true;
receipt.id= id;
receipt.uid = uid;
msg!("Successfully entered leaderboard");
Ok(())
}
#[derive(Accounts)]
#[instruction(id:u64)]
pub struct EnterLeaderboard<'info>{
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [TICKET_LEADERBOARD_SEED, id.to_le_bytes().as_ref()],
bump
)]
pub ticket_leaderboard: Account<'info, TicketLeaderboard>,
#[account(
init_if_needed,
payer= payer,
space = 8 + TicketReceiptVault::INIT_SPACE,
seeds = [RECEIPT_ACCOUNT_SEED, payer.key().as_ref()],
bump,
)]
pub receipt_account: Account<'info, TicketReceiptVault>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = ticket_leaderboard,
associated_token::token_program = token_program
)]
pub token_vault: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = payer,
associated_token::token_program = token_program
)]
pub user_token_vault: InterfaceAccount<'info, TokenAccount>,
pub token_mint: InterfaceAccount<'info, anchor_spl::token_interface::Mint>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

View File

@ -0,0 +1,26 @@
use anchor_lang::prelude::*;
use crate::*;
pub fn init(ctx: Context<InitLeaderboardList>) -> Result<()> {
ctx.accounts.ticket_leaderboard_list.leaderboards = vec![];
msg!("Initialized Leaderboard List!");
Ok(())
}
#[derive(Accounts)]
pub struct InitLeaderboardList<'info>{
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + TicketLeaderboardList::INIT_SPACE,
seeds = [TICKET_LEADERBOARD_LIST_SEED],
bump
)]
pub ticket_leaderboard_list: Account<'info, TicketLeaderboardList>,
pub system_program: Program<'info, System>,
}

View File

@ -1,70 +1,45 @@
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
use crate::{error::BettingError, *}; use crate::{error::BettingError, *};
pub fn join(ctx: Context<JoinBet>, user_id: String, _game_id: String) -> Result<()> { pub fn join(ctx: Context<JoinBet>,user_id:String, _game_id:String) ->Result<()>{
let bet_vault = &mut ctx.accounts.bet_vault; let bet_vault = &mut ctx.accounts.bet_vault;
require!( require!(
bet_vault.joiner == Pubkey::default(), bet_vault.joiner == Pubkey::default(),
BettingError::BetAlreadyFilled BettingError::BetAlreadyFilled
); );
let payer = &ctx.accounts.payer; let payer= &ctx.accounts.payer;
if bet_vault.is_native_sol { let ix = anchor_lang::solana_program::system_instruction::transfer(
let ix = anchor_lang::solana_program::system_instruction::transfer( &payer.key(),
&payer.key(), &bet_vault.key(),
&bet_vault.key(), bet_vault.wager,
bet_vault.wager, );
);
anchor_lang::solana_program::program::invoke( anchor_lang::solana_program::program::invoke(
&ix, &ix,
&[ &[
payer.to_account_info(), payer.to_account_info(),
bet_vault.to_account_info(), bet_vault.to_account_info(),
], ],
)?; )?;
} else {
// Transfer SPL tokens
let payer_token_account = ctx.accounts.payer_token_account.as_ref().ok_or(BettingError::BetNotFilled)?;
let bet_vault_token_account = ctx.accounts.bet_vault_token_account.as_ref().ok_or(BettingError::BetNotFilled)?;
let cpi_accounts = Transfer {
from: payer_token_account.to_account_info(),
to: bet_vault_token_account.to_account_info(),
authority: payer.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
token::transfer(cpi_ctx, bet_vault.wager)?;
}
bet_vault.joiner = payer.key(); bet_vault.joiner = payer.key();
bet_vault.joiner_id = user_id; bet_vault.joiner_id = user_id;
msg!("Joined bet {}!", bet_vault.key()); msg!("Joined bet {}!", bet_vault.key());
Ok(()) Ok(())
} }
#[derive(Accounts)] #[derive(Accounts)]
#[instruction(_game_id: String)] #[instruction(_game_id:String)]
pub struct JoinBet<'info> { pub struct JoinBet<'info>{
#[account(mut)] #[account(
mut
)]
pub bet_vault: Account<'info, BetVault>, pub bet_vault: Account<'info, BetVault>,
#[account(mut)] #[account(mut)]
pub payer: Signer<'info>, pub payer: Signer<'info>,
pub system_program: Program<'info, System>, pub system_program: Program<'info, System>,
// SPL Token accounts
#[account(mut)]
pub payer_token_account: Option<AccountInfo<'info>>,
#[account(mut)]
pub bet_vault_token_account: Option<AccountInfo<'info>>,
pub token_program: Program<'info, Token>,
} }

View File

@ -0,0 +1,63 @@
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{Mint, TokenAccount, TokenInterface},
};
use crate::{error::BettingError, shared::transfer_tokens, *};
pub fn join_token_bet(ctx: Context<JoinBetToken>, user_id: String, _game_id: String) -> Result<()> {
let bet_vault = &mut ctx.accounts.bet_vault;
require!(
bet_vault.joiner == Pubkey::default(),
BettingError::BetAlreadyFilled
);
let payer = &ctx.accounts.payer;
// Transfer tokens from payer to bet vault
transfer_tokens(
&ctx.accounts.payer_token_account,
&ctx.accounts.token_vault,
&bet_vault.wager,
&ctx.accounts.token_mint,
&ctx.accounts.payer,
&ctx.accounts.token_program
)?;
bet_vault.joiner = payer.key();
bet_vault.joiner_id = user_id;
msg!("Joined bet {} with {} tokens!", bet_vault.key(), bet_vault.wager);
Ok(())
}
#[derive(Accounts)]
#[instruction(_game_id: String)]
pub struct JoinBetToken<'info> {
#[account(mut)]
pub bet_vault: Account<'info, BetVault>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_mint: InterfaceAccount<'info, Mint>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = payer,
associated_token::token_program = token_program
)]
pub payer_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = bet_vault,
associated_token::token_program = token_program
)]
pub token_vault: InterfaceAccount<'info, TokenAccount>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

View File

@ -4,11 +4,53 @@ pub use initialize_bets_list::*;
pub mod create_bet; pub mod create_bet;
pub use create_bet::*; pub use create_bet::*;
pub mod create_bet_token;
pub use create_bet_token::*;
pub mod join_bet; pub mod join_bet;
pub use join_bet::*; pub use join_bet::*;
pub mod join_bet_token;
pub use join_bet_token::*;
pub mod close_bet; pub mod close_bet;
pub use close_bet::*; pub use close_bet::*;
pub mod close_bet_token;
pub use close_bet_token::*;
pub mod refund_bet; pub mod refund_bet;
pub use refund_bet::*; pub use refund_bet::*;
pub mod deduct_fees;
pub use deduct_fees::*;
pub mod deduct_fees_token;
pub use deduct_fees_token::*;
pub mod shared;
pub use shared::*;
pub mod clear_bets_list;
pub use clear_bets_list::*;
pub mod init_leaderboard_list;
pub use init_leaderboard_list::*;
pub mod create_leaderboard;
pub use create_leaderboard::*;
pub mod update_leaderboard;
pub use update_leaderboard::*;
pub mod buy_tickets;
pub use buy_tickets::*;
pub mod enter_leaderboard;
pub use enter_leaderboard::*;
pub mod remove_leaderboard;
pub use remove_leaderboard::*;
pub mod reward_leaderboard;
pub use reward_leaderboard::*;

View File

@ -1,5 +1,4 @@
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
use crate::{error::BettingError, *}; use crate::{error::BettingError, *};
pub fn refund(ctx: Context<RefundBet>, owner: Pubkey) -> Result<()> { pub fn refund(ctx: Context<RefundBet>, owner: Pubkey) -> Result<()> {
@ -10,71 +9,44 @@ pub fn refund(ctx: Context<RefundBet>, owner: Pubkey) -> Result<()> {
BettingError::InvalidWinner BettingError::InvalidWinner
); );
let is_native_sol = bet_vault.is_native_sol;
let joiner = bet_vault.joiner;
let system_program_key = ctx.accounts.system_program.key();
let bet_vault_key = bet_vault.key();
// Check if there's a joiner and it's a valid account (not the system program or default) // Check if there's a joiner and it's a valid account (not the system program or default)
if joiner != Pubkey::default() && joiner != system_program_key { if bet_vault.joiner != Pubkey::default() && bet_vault.joiner != ctx.accounts.system_program.key() {
if is_native_sol { // Calculate the 50% that goes to the joiner
// Calculate the 50% that goes to the joiner let total_lamports = **bet_vault.to_account_info().lamports.borrow();
let total_lamports = **bet_vault.to_account_info().lamports.borrow(); let joiner_share = total_lamports / 2;
let joiner_share = total_lamports / 2;
// Transfer joiner's share // Transfer joiner's share
**bet_vault.to_account_info().try_borrow_mut_lamports()? -= joiner_share; **bet_vault.to_account_info().try_borrow_mut_lamports()? -= joiner_share;
// We need to find the joiner account in the remaining accounts // We need to find the joiner account in the remaining accounts
let joiner_account = ctx.remaining_accounts let joiner_account = ctx.remaining_accounts
.iter() .iter()
.find(|account| account.key() == joiner) .find(|account| account.key() == bet_vault.joiner)
.ok_or(BettingError::JoinerAccountNotProvided)?; .ok_or(BettingError::JoinerAccountNotProvided)?;
**joiner_account.try_borrow_mut_lamports()? += joiner_share; **joiner_account.try_borrow_mut_lamports()? += joiner_share;
msg!("Sent {} lamports to joiner {}", joiner_share, joiner); msg!("Sent {} lamports to joiner {}", joiner_share, bet_vault.joiner);
} else {
// Handle SPL token refund
let bet_vault_token_account = ctx.accounts.bet_vault_token_account.as_ref().ok_or(BettingError::BetNotFilled)?;
let joiner_token_account = ctx.accounts.joiner_token_account.as_ref().ok_or(BettingError::JoinerAccountNotProvided)?;
let token_account = TokenAccount::try_deserialize(&mut &bet_vault_token_account.data.borrow()[..])?;
let total_tokens = token_account.amount;
let joiner_share = total_tokens / 2;
let cpi_accounts = Transfer {
from: bet_vault_token_account.to_account_info(),
to: joiner_token_account.to_account_info(),
authority: ctx.accounts.bet_vault.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
token::transfer(cpi_ctx, joiner_share)?;
msg!("Sent {} tokens to joiner {}", joiner_share, joiner);
}
} }
// The remaining balance will go to the owner through the close=owner directive // The remaining balance will go to the owner through the close=owner directive
let bets_list = &mut ctx.accounts.bets_list; let bets_list = &mut ctx.accounts.bets_list;
// Remove the bet_vault public key from the list // Remove the bet_vault public key from the list
bets_list.bets.retain(|&bet| bet != bet_vault_key); bets_list.bets.retain(|&bet| bet != bet_vault.key());
msg!("Bet {} Refunded by {}", bet_vault_key, owner); msg!("Bet {} Refunded by {}", bet_vault.key(), owner);
Ok(()) Ok(())
} }
#[derive(Accounts)] #[derive(Accounts)]
pub struct RefundBet<'info> { pub struct RefundBet<'info> {
#[account(mut)] #[account(mut)]
pub bets_list: Account<'info, BetsList>, pub bets_list: Account<'info, BetsList>,
#[account(mut, close = owner)] #[account(mut, close=owner)]
pub bet_vault: Account<'info, BetVault>, pub bet_vault: Account<'info, BetVault>,
#[account(mut)] #[account(mut)]
@ -82,15 +54,5 @@ pub struct RefundBet<'info> {
#[account(mut)] #[account(mut)]
pub payer: Signer<'info>, pub payer: Signer<'info>,
pub system_program: Program<'info, System>
pub system_program: Program<'info, System>,
// SPL Token accounts
#[account(mut)]
pub bet_vault_token_account: Option<AccountInfo<'info>>,
#[account(mut)]
pub joiner_token_account: Option<AccountInfo<'info>>,
pub token_program: Program<'info, Token>,
} }

View File

@ -0,0 +1,38 @@
use anchor_lang::prelude::*;
use crate::*;
pub fn remove(ctx: Context<RemoveLeaderboard>, id:u64) -> Result<()> {
let ticket_leaderboard_list = &mut ctx.accounts.ticket_leaderboard_list;
let ticket_leaderboard = &mut ctx.accounts.ticket_leaderboard;
ticket_leaderboard_list.leaderboards.retain(|&leaderboard| leaderboard != ticket_leaderboard.key());
msg!("Successfully removed leaderboard with id: {}", id);
Ok(())
}
#[derive(Accounts)]
#[instruction(id:u64)]
pub struct RemoveLeaderboard<'info>{
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [TICKET_LEADERBOARD_LIST_SEED],
bump
)]
pub ticket_leaderboard_list: Account<'info, TicketLeaderboardList>,
#[account(
mut,
seeds = [TICKET_LEADERBOARD_SEED, id.to_le_bytes().as_ref()],
bump
)]
pub ticket_leaderboard: Account<'info, TicketLeaderboard>,
pub system_program: Program<'info, System>,
}

View File

@ -0,0 +1,116 @@
use anchor_lang::prelude::*;
use anchor_spl::{associated_token::AssociatedToken, token::{transfer_checked, TransferChecked}, token_interface::{TokenAccount, TokenInterface}};
use crate::{error::ArithmeticError, *};
pub fn reward(ctx:Context<RewardLeaderboard>, id:u64)->Result<()>{
let leaderboard = &mut ctx.accounts.ticket_leaderboard;
let ticket_leaderboard_vault = &ctx.accounts.ticket_leaderboard_vault;
let ticket_leaderboard_list_vault = &ctx.accounts.ticket_leaderboard_list_vault;
let winner = &ctx.accounts.winner;
// Get the token balance in the ticket_leaderboard vault
let token_balance = ticket_leaderboard_vault.amount;
// Calculate SOL reward: each token is worth 0.1 SOL (100,000,000 lamports)
let sol_reward = match token_balance.checked_mul(100_000_000) {
Some(reward) => reward,
None => return Err(Error::from(ArithmeticError::ArithmeticError)),
};
// Transfer SOL from ticket_leaderboard_list to winner
let ix = anchor_lang::solana_program::system_instruction::transfer(
&ctx.accounts.ticket_leaderboard_list.key(),
&winner.key(),
sol_reward
);
let seeds = &[
TICKET_LEADERBOARD_LIST_SEED.as_ref(),
&[ctx.bumps.ticket_leaderboard_list][..]
];
let signer_seeds = &[&seeds[..]];
anchor_lang::solana_program::program::invoke_signed(
&ix,
&[
ctx.accounts.ticket_leaderboard_list.to_account_info(),
winner.to_account_info(),
],
signer_seeds,
)?;
// Transfer all tokens from ticket_leaderboard vault to ticket_leaderboard_list vault
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_accounts = TransferChecked {
from: ticket_leaderboard_vault.to_account_info(),
to: ticket_leaderboard_list_vault.to_account_info(),
authority: leaderboard.to_account_info(),
mint: ctx.accounts.token_mint.to_account_info(),
};
let seeds = &[
TICKET_LEADERBOARD_SEED,
&id.to_le_bytes()[..],
&[ctx.bumps.ticket_leaderboard][..]
];
let signer_seeds = &[&seeds[..]];
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);
transfer_checked(cpi_ctx, token_balance, ctx.accounts.token_mint.decimals)?;
leaderboard.is_over = true;
msg!("Successfully rewarded winner with {} SOL and transferred {} tokens to leaderboard list",
sol_reward as f64 / 1_000_000_000.0, token_balance);
Ok(())
}
#[derive(Accounts)]
#[instruction(id:u64)]
pub struct RewardLeaderboard<'info>{
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: The winner account
#[account(mut)]
pub winner: SystemAccount<'info>,
#[account(
mut,
seeds = [TICKET_LEADERBOARD_LIST_SEED],
bump
)]
pub ticket_leaderboard_list: Account<'info, TicketLeaderboardList>,
#[account(
mut,
seeds = [TICKET_LEADERBOARD_SEED, id.to_le_bytes().as_ref()],
bump
)]
pub ticket_leaderboard: Account<'info, TicketLeaderboard>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = ticket_leaderboard,
associated_token::token_program = token_program
)]
pub ticket_leaderboard_vault: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = ticket_leaderboard_list,
associated_token::token_program = token_program
)]
pub ticket_leaderboard_list_vault: InterfaceAccount<'info, TokenAccount>,
pub token_mint: InterfaceAccount<'info, anchor_spl::token_interface::Mint>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

View File

@ -0,0 +1,56 @@
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{
transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked,
};
use crate::*;
pub fn transfer_tokens<'info>(
from: &InterfaceAccount<'info, TokenAccount>,
to: &InterfaceAccount<'info, TokenAccount>,
amount: &u64,
mint: &InterfaceAccount<'info, Mint>,
authority: &Signer<'info>,
token_program: &Interface<'info, TokenInterface>,
) -> Result<()> {
let transfer_accounts_options = TransferChecked {
from: from.to_account_info(),
mint: mint.to_account_info(),
to: to.to_account_info(),
authority: authority.to_account_info(),
};
let cpi_context = CpiContext::new(token_program.to_account_info(), transfer_accounts_options);
transfer_checked(cpi_context, *amount, mint.decimals)
}
pub fn transfer_tokens_pda<'info>(
from: &InterfaceAccount<'info, TokenAccount>,
to: &InterfaceAccount<'info, TokenAccount>,
amount: u64,
mint: &InterfaceAccount<'info, Mint>,
pda_account: &Account<'info, BetVault>,
token_program: &Interface<'info, TokenInterface>,
seeds: &[&[u8]],
bump: u8,
) -> Result<()> {
let transfer_accounts_options = TransferChecked {
from: from.to_account_info(),
mint: mint.to_account_info(),
to: to.to_account_info(),
authority: pda_account.to_account_info(),
};
let bump_seed = [bump];
let mut all_seeds = seeds.to_vec();
all_seeds.push(&bump_seed);
let signer_seeds: &[&[&[u8]]] = &[&all_seeds];
let cpi_context = CpiContext::new_with_signer(
token_program.to_account_info(),
transfer_accounts_options,
signer_seeds,
);
transfer_checked(cpi_context, amount, mint.decimals)
}

View File

@ -0,0 +1,68 @@
use anchor_lang::prelude::*;
use crate::{error::LeaderboardError, *};
pub fn update(ctx: Context<UpdateLeaderboard>, id:u64, player:Pubkey, new_score:u64) -> Result<()> {
let leaderboard_entry: &mut Account<LeaderboardEntry> = &mut ctx.accounts.leaderboard_entry;
let leaderboard: &mut Account<TicketLeaderboard> = &mut ctx.accounts.ticket_leaderboard;
let receipt = &mut ctx.accounts.receipt_account;
if receipt.id != id {
return Err(LeaderboardError::InvalidId.into());
}
if !receipt.active {
return Err(LeaderboardError::NotActive.into());
}
// Find existing entry for this payer
if leaderboard_entry.highscore < new_score {
leaderboard_entry.highscore = new_score;
}
leaderboard_entry.total_tickets += 1;
if !leaderboard.players.contains(&player) {
leaderboard.players.push(player);
}
leaderboard_entry.uid = receipt.uid.clone();
receipt.active = false;
msg!("Successfully updated leaderboard with score: {}", new_score);
Ok(())
}
#[derive(Accounts)]
#[instruction(id:u64, player:Pubkey, new_score:u64)]
pub struct UpdateLeaderboard<'info>{
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [TICKET_LEADERBOARD_SEED, id.to_le_bytes().as_ref()],
bump
)]
pub ticket_leaderboard: Account<'info, TicketLeaderboard>,
#[account(
init_if_needed,
payer= payer,
space = 8 + LeaderboardEntry::INIT_SPACE,
seeds = [LEADERBOARD_ENTRY_SEED, ticket_leaderboard.key().as_ref(), player.as_ref()],
bump
)]
pub leaderboard_entry: Account<'info, LeaderboardEntry>,
#[account(
mut,
seeds = [RECEIPT_ACCOUNT_SEED, player.as_ref()],
bump
)]
pub receipt_account: Account<'info, TicketReceiptVault>,
pub system_program: Program<'info, System>,
}

View File

@ -19,37 +19,73 @@ pub mod bets {
initialize_bets_list::init(ctx) initialize_bets_list::init(ctx)
} }
pub fn create_bet( pub fn create_bet(ctx: Context<CreateBet>, wager:u64,user_id:String, game_id:String, nonce:u64)-> Result<()>{
ctx: Context<CreateBet>, create_bet::create(ctx, wager, user_id, game_id,nonce)
wager: u64,
user_id: String,
game_id: String,
nonce: u64,
is_native_sol: bool,
token_mint: Option<Pubkey>,
token_decimals: u8,
) -> Result<()> {
create_bet::create(
ctx,
wager,
user_id,
game_id,
nonce,
is_native_sol,
token_mint,
token_decimals
)
} }
pub fn join_bet(ctx: Context<JoinBet>, user_id: String, game_id: String) -> Result<()> { pub fn join_bet(ctx: Context<JoinBet>,user_id:String, game_id:String) -> Result<()>{
join_bet::join(ctx, user_id, game_id) join_bet::join(ctx,user_id, game_id)
} }
pub fn close_bet(ctx: Context<CloseBet>, winner: Pubkey, userid: String) -> Result<()> { pub fn close_bet(ctx:Context<CloseBet>, winner:Pubkey, userid:String)->Result<()> {
close_bet::close(ctx, winner, userid) close_bet::close(ctx, winner, userid)
} }
pub fn refund_bet(ctx: Context<RefundBet>, owner: Pubkey) -> Result<()> { pub fn refund_bet(ctx:Context<RefundBet>, owner:Pubkey)->Result<()>{
refund_bet::refund(ctx, owner) refund_bet::refund(ctx, owner)
} }
pub fn deduct_fees(ctx:Context<DeductFees>, winner:Pubkey, userid:String)->Result<()>{
deduct_fees::deduct(ctx, winner, userid)
}
pub fn create_bet_token(ctx:Context<CreateBetToken>, wager:u64,user_id:String, game_id:String, nonce:u64)->Result<()>{
create_bet_token::create_token_bet(ctx, wager, user_id, game_id, nonce)
}
pub fn join_bet_token(ctx:Context<JoinBetToken>, user_id:String, game_id:String)->Result<()>{
join_bet_token::join_token_bet(ctx, user_id, game_id)
}
pub fn deduct_fees_token(ctx:Context<DeductFeesToken>, winner:Pubkey, userid:String, owner:Pubkey, game_id:String, nonce:u64)->Result<()>{
deduct_fees_token::handle_deduct_fees_token(ctx, winner, userid, owner, game_id, nonce)
}
pub fn close_bet_token(ctx:Context<CloseBetToken>, winner:Pubkey, userid:String, owner:Pubkey, game_id:String, nonce:u64)->Result<()>{
close_bet_token::close_token_bet(ctx, winner, userid, owner, game_id, nonce)
}
pub fn clear_bets_list(ctx: Context<ClearBetsList>) -> Result<()> {
clear_bets_list::clear(ctx)
}
pub fn init_leaderboard_list(ctx:Context<InitLeaderboardList>) -> Result<()>{
init_leaderboard_list::init(ctx)
}
pub fn create_leaderboard(ctx:Context<CreateLeaderboard>, id:u64, game_id:String, entry_ticket_count:u64, max_attempt_count:u64)->Result<()>{
create_leaderboard::create(ctx, id, game_id, entry_ticket_count, max_attempt_count)
}
pub fn update_leaderboard(ctx:Context<UpdateLeaderboard>, id:u64, player:Pubkey, new_score:u64)->Result<()>{
update_leaderboard::update(ctx, id, player, new_score)
}
pub fn buy_tickets(ctx:Context<BuyTickets>, amount:u64)->Result<()>{
buy_tickets::buy(ctx,amount)
}
pub fn enter_leaderboard(ctx:Context<EnterLeaderboard>, id:u64, uid:String)->Result<()>{
enter_leaderboard::enter(ctx, id, uid)
}
pub fn remove_leaderboard(ctx:Context<RemoveLeaderboard>, id:u64)->Result<()>{
remove_leaderboard::remove(ctx, id)
}
pub fn reward_leaderboard(ctx:Context<RewardLeaderboard>, id:u64)->Result<()>{
reward_leaderboard::reward(ctx, id)
}
} }

View File

@ -4,6 +4,8 @@ use crate::*;
#[account] #[account]
#[derive(InitSpace)] #[derive(InitSpace)]
pub struct BetVault { pub struct BetVault {
#[max_len(10)]
pub nonce: u64,
#[max_len(10)] #[max_len(10)]
pub game_id: String, pub game_id: String,
pub owner: Pubkey, pub owner: Pubkey,
@ -12,8 +14,6 @@ pub struct BetVault {
pub joiner: Pubkey, pub joiner: Pubkey,
#[max_len(40)] #[max_len(40)]
pub joiner_id: String, pub joiner_id: String,
pub wager: u64, pub token_mint:Pubkey,
pub is_native_sol: bool, pub wager:u64
pub token_mint: Option<Pubkey>,
pub token_decimals: u8
} }

View File

@ -3,3 +3,15 @@ pub use bets_list::*;
pub mod bet_vault; pub mod bet_vault;
pub use bet_vault::*; pub use bet_vault::*;
pub mod ticket_leaderboard;
pub use ticket_leaderboard::*;
pub mod ticket_leaderboard_account;
pub use ticket_leaderboard_account::*;
pub mod ticket_leaderboard_list;
pub use ticket_leaderboard_list::*;
pub mod ticket_receipt_vault;
pub use ticket_receipt_vault::*;

View File

@ -0,0 +1,15 @@
use anchor_lang::*;
use crate::*;
#[account]
#[derive(InitSpace)]
pub struct TicketLeaderboard{
pub id: u64,
#[max_len(16)]
pub game_id: String,
#[max_len(150)]
pub players: Vec<Pubkey>,
pub entry_ticket_count: u64,
pub max_attempts: u64,
pub is_over: bool,
}

View File

@ -0,0 +1,11 @@
use anchor_lang::*;
use crate::*;
#[account]
#[derive(InitSpace)]
pub struct LeaderboardEntry{
#[max_len(50)]
pub uid: String,
pub highscore: u64,
pub total_tickets: u64
}

View File

@ -0,0 +1,9 @@
use anchor_lang::*;
use crate::*;
#[account]
#[derive(InitSpace)]
pub struct TicketLeaderboardList{
#[max_len(300)]
pub leaderboards: Vec<Pubkey>
}

View File

@ -0,0 +1,11 @@
use anchor_lang::*;
use crate::*;
#[account]
#[derive(InitSpace)]
pub struct TicketReceiptVault{
pub id: u64,
#[max_len(50)]
pub uid: String,
pub active: bool
}

View File

@ -1,416 +1,233 @@
import * as anchor from "@coral-xyz/anchor"; import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor";
import { Bets } from "../target/types/bets"; import { Bets } from "../target/types/bets";
import { PublicKey, SystemProgram, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js"; import { Keypair, PublicKey, LAMPORTS_PER_SOL, Transaction, SystemProgram } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID, createMint, createAccount, mintTo, getAccount } from "@solana/spl-token"; import { ProgramTestContext, startAnchor } from "solana-bankrun";
import { assert } from "chai"; import { BankrunProvider } from "anchor-bankrun";
import { expect } from "chai";
import { BN } from "@coral-xyz/anchor";
const IDL = require('../target/idl/bets.json');
const betsAddress = new PublicKey("Haj94DF925qNRgcoRwQfNsVLKgSmFhG4bjgtvusMkkpD");
describe("bets", () => { describe("bets", () => {
// Configure the client to use the local cluster let context: ProgramTestContext;
const provider = anchor.AnchorProvider.env(); let provider: BankrunProvider;
anchor.setProvider(provider); let betsProgram: Program<Bets>;
let user1: Keypair;
const program = anchor.workspace.Bets as Program<Bets>; let user2: Keypair;
let gameId = "game123";
// Test accounts let userId1 = "user123";
const owner = Keypair.generate(); let userId2 = "user456";
const joiner = Keypair.generate(); let wager = new BN(LAMPORTS_PER_SOL); // 1 SOL
const feeWallet = Keypair.generate();
const ownerReferrer = Keypair.generate();
const joinerReferrer = Keypair.generate();
// SPL Token accounts
let tokenMint: PublicKey;
let ownerTokenAccount: PublicKey;
let joinerTokenAccount: PublicKey;
let betVaultTokenAccount: PublicKey;
let feeWalletTokenAccount: PublicKey;
let ownerReferrerTokenAccount: PublicKey;
let joinerReferrerTokenAccount: PublicKey;
// Program state accounts
let betsList: PublicKey;
let betVault: PublicKey;
const gameId = "test-game-1";
const ownerId = "owner123";
const joinerId = "joiner456";
const wager = new anchor.BN(LAMPORTS_PER_SOL); // 1 SOL or 1 token
const tokenDecimals = 9;
before(async () => { before(async () => {
// Airdrop SOL to test accounts context = await startAnchor("", [{ name: "bets", programId: betsAddress }], []);
await provider.connection.requestAirdrop(owner.publicKey, 10 * LAMPORTS_PER_SOL); provider = new BankrunProvider(context);
await provider.connection.requestAirdrop(joiner.publicKey, 10 * LAMPORTS_PER_SOL); betsProgram = new Program<Bets>(IDL, provider);
await provider.connection.requestAirdrop(feeWallet.publicKey, 10 * LAMPORTS_PER_SOL);
await provider.connection.requestAirdrop(ownerReferrer.publicKey, 10 * LAMPORTS_PER_SOL);
await provider.connection.requestAirdrop(joinerReferrer.publicKey, 10 * LAMPORTS_PER_SOL);
// Create SPL token mint // Create test users
tokenMint = await createMint( user1 = Keypair.generate();
provider.connection, user2 = Keypair.generate();
owner,
owner.publicKey, // Fund accounts using bankrun
null, const fundAmount = 10 * LAMPORTS_PER_SOL;
tokenDecimals
// Create accounts first by sending a transaction
const tx = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: context.payer.publicKey,
newAccountPubkey: user1.publicKey,
lamports: fundAmount,
space: 0,
programId: SystemProgram.programId,
}),
SystemProgram.createAccount({
fromPubkey: context.payer.publicKey,
newAccountPubkey: user2.publicKey,
lamports: fundAmount,
space: 0,
programId: SystemProgram.programId,
})
); );
// Create token accounts // Get recent blockhash and set it on the transaction
ownerTokenAccount = await createAccount( const [blockhash] = await context.banksClient.getLatestBlockhash();
provider.connection, tx.recentBlockhash = blockhash;
owner, tx.feePayer = context.payer.publicKey;
tokenMint,
owner.publicKey
);
joinerTokenAccount = await createAccount( // Sign the transaction with all required signers
provider.connection, tx.sign(context.payer, user1, user2);
joiner, await context.banksClient.processTransaction(tx);
tokenMint,
joiner.publicKey
);
betVaultTokenAccount = await createAccount(
provider.connection,
owner,
tokenMint,
owner.publicKey
);
feeWalletTokenAccount = await createAccount(
provider.connection,
feeWallet,
tokenMint,
feeWallet.publicKey
);
ownerReferrerTokenAccount = await createAccount(
provider.connection,
ownerReferrer,
tokenMint,
ownerReferrer.publicKey
);
joinerReferrerTokenAccount = await createAccount(
provider.connection,
joinerReferrer,
tokenMint,
joinerReferrer.publicKey
);
// Mint tokens to owner and joiner
await mintTo(
provider.connection,
owner,
tokenMint,
ownerTokenAccount,
owner,
100 * LAMPORTS_PER_SOL
);
await mintTo(
provider.connection,
owner,
tokenMint,
joinerTokenAccount,
owner,
100 * LAMPORTS_PER_SOL
);
}); });
it("Initializes the bets list", async () => { it("Initializes the bets list", async () => {
[betsList] = PublicKey.findProgramAddressSync( await betsProgram.methods.initialize().rpc();
[Buffer.from("bets_list")],
program.programId
);
await program.methods const [betsListAddress] = PublicKey.findProgramAddressSync(
.initialize() [Buffer.from("bets_list")],
betsAddress
);
const betsList = await betsProgram.account.betsList.fetch(betsListAddress);
expect(betsList.bets).to.be.an('array').that.is.empty;
});
it("Creates a new bet", async () => {
const nonce = new BN(Date.now());
await betsProgram.methods
.createBet(wager, userId1, gameId, nonce)
.accounts({ .accounts({
betsList, payer: user1.publicKey,
payer: provider.wallet.publicKey, betsList: (await PublicKey.findProgramAddressSync(
systemProgram: SystemProgram.programId, [Buffer.from("bets_list")],
betsAddress
))[0],
}) })
.signers([user1])
.rpc(); .rpc();
const betsListAccount = await program.account.betsList.fetch(betsList); const [betVaultAddress] = PublicKey.findProgramAddressSync(
assert.ok(betsListAccount.bets.length === 0); [
Buffer.from("bet_vault"),
user1.publicKey.toBuffer(),
Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
betsAddress
);
const betVault = await betsProgram.account.betVault.fetch(betVaultAddress);
expect(betVault.gameId).to.equal(gameId);
expect(betVault.owner).to.eql(user1.publicKey);
// expect(betVault.ownerId).to.equal(userId1);
// expect(betVault.wager).to.equal(wager);
}); });
describe("Native SOL bets", () => { it("Joins an existing bet", async () => {
it("Creates a bet with native SOL", async () => { const nonce = new BN(Date.now());
const nonce = new anchor.BN(Date.now()); // First create a bet
[betVault] = PublicKey.findProgramAddressSync( await betsProgram.methods
[ .createBet(wager, userId1, gameId, nonce)
Buffer.from("bet_vault"), .accounts({
owner.publicKey.toBuffer(), payer: user1.publicKey,
Buffer.from(gameId), betsList: (await PublicKey.findProgramAddressSync(
nonce.toArrayLike(Buffer, "le", 8), [Buffer.from("bets_list")],
], betsAddress
program.programId ))[0],
); })
.signers([user1])
.rpc();
await program.methods // Then join the bet
.createBet( await betsProgram.methods
wager, .joinBet(userId2, gameId)
ownerId, .accounts({
gameId, betVault: (await PublicKey.findProgramAddressSync(
nonce, [
true, // isNativeSol Buffer.from("bet_vault"),
null, // tokenMint user1.publicKey.toBuffer(),
0 // tokenDecimals Buffer.from(gameId),
) nonce.toArrayLike(Buffer, "le", 8),
.accounts({ ],
payer: owner.publicKey, betsAddress
betsList, ))[0],
betVault, payer: user2.publicKey
systemProgram: SystemProgram.programId, })
}) .signers([user2])
.signers([owner]) .rpc();
.rpc();
const betVaultAccount = await program.account.betVault.fetch(betVault); const [betVaultAddress] = PublicKey.findProgramAddressSync(
assert.ok(betVaultAccount.owner.equals(owner.publicKey)); [
assert.ok(betVaultAccount.ownerId === ownerId); Buffer.from("bet_vault"),
assert.ok(betVaultAccount.gameId === gameId); user1.publicKey.toBuffer(),
assert.ok(betVaultAccount.wager.eq(wager)); Buffer.from(gameId),
assert.ok(betVaultAccount.isNativeSol === true); nonce.toArrayLike(Buffer, "le", 8),
}); ],
betsAddress
);
it("Joins a bet with native SOL", async () => { const betVault = await betsProgram.account.betVault.fetch(betVaultAddress);
await program.methods
.joinBet(joinerId, gameId)
.accounts({
betVault,
payer: joiner.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([joiner])
.rpc();
const betVaultAccount = await program.account.betVault.fetch(betVault); expect(betVault.joiner).to.eql(user2.publicKey);
assert.ok(betVaultAccount.joiner.equals(joiner.publicKey)); expect(betVault.joinerId).to.equal(userId2);
assert.ok(betVaultAccount.joinerId === joinerId);
});
it("Closes a bet with native SOL", async () => {
await program.methods
.closeBet(owner.publicKey, ownerId)
.accounts({
betsList,
betVault,
feeWallet: feeWallet.publicKey,
winner: owner.publicKey,
payer: owner.publicKey,
ownerReferrer: ownerReferrer.publicKey,
joinerReferrer: joinerReferrer.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([owner])
.rpc();
// Verify the bet was removed from the list
const betsListAccount = await program.account.betsList.fetch(betsList);
assert.ok(!betsListAccount.bets.some(bet => bet.equals(betVault)));
});
}); });
describe("SPL Token bets", () => { it("Closes a bet with a winner", async () => {
it("Creates a bet with SPL tokens", async () => { const nonce = new BN(Date.now());
const nonce = new anchor.BN(Date.now()); // First create a bet
[betVault] = PublicKey.findProgramAddressSync( await betsProgram.methods
[ .createBet(wager, userId1, gameId, nonce)
Buffer.from("bet_vault"), .accounts({
owner.publicKey.toBuffer(), payer: user1.publicKey,
Buffer.from(gameId), betsList: (await PublicKey.findProgramAddressSync(
nonce.toArrayLike(Buffer, "le", 8), [Buffer.from("bets_list")],
], betsAddress
program.programId ))[0],
);
await program.methods })
.createBet( .signers([user1])
wager, .rpc();
ownerId,
gameId,
nonce,
false, // isNativeSol
tokenMint,
tokenDecimals
)
.accounts({
payer: owner.publicKey,
betsList,
betVault,
systemProgram: SystemProgram.programId,
payerTokenAccount: ownerTokenAccount,
betVaultTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
})
.signers([owner])
.rpc();
const betVaultAccount = await program.account.betVault.fetch(betVault); // Join the bet
assert.ok(betVaultAccount.owner.equals(owner.publicKey)); await betsProgram.methods
assert.ok(betVaultAccount.ownerId === ownerId); .joinBet(userId2, gameId)
assert.ok(betVaultAccount.gameId === gameId); .accounts({
assert.ok(betVaultAccount.wager.eq(wager)); betVault: (await PublicKey.findProgramAddressSync(
assert.ok(betVaultAccount.isNativeSol === false); [
assert.ok(betVaultAccount.tokenMint.equals(tokenMint)); Buffer.from("bet_vault"),
assert.ok(betVaultAccount.tokenDecimals === tokenDecimals); user1.publicKey.toBuffer(),
}); Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
betsAddress
))[0],
payer: user2.publicKey
})
.signers([user2])
.rpc();
it("Joins a bet with SPL tokens", async () => { // Close the bet with user1 as winner
await program.methods await betsProgram.methods
.joinBet(joinerId, gameId) .closeBet(user1.publicKey, userId1)
.accounts({ .accounts({
betVault, betsList: (await PublicKey.findProgramAddressSync(
payer: joiner.publicKey, [Buffer.from("bets_list")],
systemProgram: SystemProgram.programId, betsAddress
payerTokenAccount: joinerTokenAccount, ))[0],
betVaultTokenAccount, betVault: (await PublicKey.findProgramAddressSync(
tokenProgram: TOKEN_PROGRAM_ID, [
}) Buffer.from("bet_vault"),
.signers([joiner]) user1.publicKey.toBuffer(),
.rpc(); Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
betsAddress
))[0],
winner: user1.publicKey,
payer: user1.publicKey,
const betVaultAccount = await program.account.betVault.fetch(betVault); })
assert.ok(betVaultAccount.joiner.equals(joiner.publicKey)); .signers([user1])
assert.ok(betVaultAccount.joinerId === joinerId); .rpc();
});
it("Closes a bet with SPL tokens", async () => { // Verify the bet is removed from the bets list
await program.methods const [betsListAddress] = PublicKey.findProgramAddressSync(
.closeBet(owner.publicKey, ownerId) [Buffer.from("bets_list")],
.accounts({ betsAddress
betsList, );
betVault, const betsList = await betsProgram.account.betsList.fetch(betsListAddress);
feeWallet: feeWallet.publicKey, const [betVaultAddress] = PublicKey.findProgramAddressSync(
winner: owner.publicKey, [
payer: owner.publicKey, Buffer.from("bet_vault"),
ownerReferrer: ownerReferrer.publicKey, user1.publicKey.toBuffer(),
joinerReferrer: joinerReferrer.publicKey, Buffer.from(gameId),
systemProgram: SystemProgram.programId, nonce.toArrayLike(Buffer, "le", 8),
betVaultTokenAccount, ],
feeWalletTokenAccount, betsAddress
ownerReferrerTokenAccount, );
joinerReferrerTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
})
.signers([owner])
.rpc();
// Verify the bet was removed from the list expect(betsList.bets).to.not.include(betVaultAddress);
const betsListAccount = await program.account.betsList.fetch(betsList);
assert.ok(!betsListAccount.bets.some(bet => bet.equals(betVault)));
});
});
describe("Refund functionality", () => {
it("Refunds a native SOL bet", async () => {
// Create a new bet
const nonce = new anchor.BN(Date.now());
[betVault] = PublicKey.findProgramAddressSync(
[
Buffer.from("bet_vault"),
owner.publicKey.toBuffer(),
Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
program.programId
);
await program.methods
.createBet(
wager,
ownerId,
gameId,
nonce,
true, // isNativeSol
null, // tokenMint
0 // tokenDecimals
)
.accounts({
payer: owner.publicKey,
betsList,
betVault,
systemProgram: SystemProgram.programId,
})
.signers([owner])
.rpc();
// Refund the bet
await program.methods
.refundBet(owner.publicKey)
.accounts({
betsList,
betVault,
owner: owner.publicKey,
payer: owner.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([owner])
.rpc();
// Verify the bet was removed from the list
const betsListAccount = await program.account.betsList.fetch(betsList);
assert.ok(!betsListAccount.bets.some(bet => bet.equals(betVault)));
});
it("Refunds an SPL token bet", async () => {
// Create a new bet
const nonce = new anchor.BN(Date.now());
[betVault] = PublicKey.findProgramAddressSync(
[
Buffer.from("bet_vault"),
owner.publicKey.toBuffer(),
Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
program.programId
);
await program.methods
.createBet(
wager,
ownerId,
gameId,
nonce,
false, // isNativeSol
tokenMint,
tokenDecimals
)
.accounts({
payer: owner.publicKey,
betsList,
betVault,
systemProgram: SystemProgram.programId,
payerTokenAccount: ownerTokenAccount,
betVaultTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
})
.signers([owner])
.rpc();
// Refund the bet
await program.methods
.refundBet(owner.publicKey)
.accounts({
betsList,
betVault,
owner: owner.publicKey,
payer: owner.publicKey,
systemProgram: SystemProgram.programId,
betVaultTokenAccount,
joinerTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
})
.signers([owner])
.rpc();
// Verify the bet was removed from the list
const betsListAccount = await program.account.betsList.fetch(betsList);
assert.ok(!betsListAccount.bets.some(bet => bet.equals(betVault)));
});
}); });
}); });

619
yarn.lock

File diff suppressed because it is too large Load Diff