multi currency support v1:

This commit is contained in:
Sewmina Dilshan 2025-06-15 12:39:43 +05:30
parent b32b041a14
commit c84d8a4a6d
17 changed files with 4439 additions and 263 deletions

View File

@ -16,3 +16,7 @@ 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" }

821
Cargo.lock generated

File diff suppressed because it is too large Load Diff

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,7 +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"
solana-program= "=2.0.3"

View File

@ -1,5 +1,3 @@
use std::str::FromStr;
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use crate::{error::BettingError, *}; use crate::{error::BettingError, *};

View File

@ -0,0 +1,70 @@
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{TokenAccount, TokenInterface},
};
use crate::{error::BettingError, shared::transfer_tokens, *};
pub fn close_token_bet(ctx: Context<CloseBetToken>, 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
);
// Transfer tokens from vault to winner
transfer_tokens(
&ctx.accounts.token_vault,
&ctx.accounts.winner_token_account,
&bet_vault.wager,
&ctx.accounts.token_mint,
&bet_vault.to_account_info(),
&ctx.accounts.token_program
)?;
// 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 {}", bet_vault.wager, winner);
Ok(())
}
#[derive(Accounts)]
pub struct CloseBetToken<'info> {
#[account(mut)]
pub bets_list: Account<'info, BetsList>,
#[account(mut)]
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

@ -0,0 +1,77 @@
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();
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,138 @@
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, *};
pub fn handle_deduct_fees_token(ctx: Context<DeductFeesToken>, 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 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
);
// Transfer referrer fees
transfer_tokens(
&ctx.accounts.token_vault,
&ctx.accounts.owner_referrer_token_account,
&referrer_fee,
&ctx.accounts.token_mint,
&ctx.accounts.bet_vault.to_account_info(),
&ctx.accounts.token_program
)?;
msg!("Transferred {} tokens to owner referrer", referrer_fee);
transfer_tokens(
&ctx.accounts.token_vault,
&ctx.accounts.joiner_referrer_token_account,
&referrer_fee,
&ctx.accounts.token_mint,
&ctx.accounts.bet_vault.to_account_info(),
&ctx.accounts.token_program
)?;
msg!("Transferred {} tokens to joiner referrer", referrer_fee);
// Transfer platform fee
transfer_tokens(
&ctx.accounts.token_vault,
&ctx.accounts.fee_wallet_token_account,
&fee,
&ctx.accounts.token_mint,
&ctx.accounts.bet_vault.to_account_info(),
&ctx.accounts.token_program
)?;
msg!("Transferred {} tokens to fee wallet", fee);
Ok(())
}
#[derive(Accounts)]
pub struct DeductFeesToken<'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 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,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,14 +4,29 @@ 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 mod deduct_fees;
pub use deduct_fees::*; pub use deduct_fees::*;
pub mod deduct_fees_token;
pub use deduct_fees_token::*;
pub mod shared;
pub use shared::*;

View File

@ -0,0 +1,24 @@
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{
transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked,
};
pub fn transfer_tokens<'info>(
from: &InterfaceAccount<'info, TokenAccount>,
to: &InterfaceAccount<'info, TokenAccount>,
amount: &u64,
mint: &InterfaceAccount<'info, Mint>,
authority: &AccountInfo<'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)
}

View File

@ -38,4 +38,21 @@ pub mod bets {
pub fn deduct_fees(ctx:Context<DeductFees>, winner:Pubkey, userid:String)->Result<()>{ pub fn deduct_fees(ctx:Context<DeductFees>, winner:Pubkey, userid:String)->Result<()>{
deduct_fees::deduct(ctx, winner, userid) 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)->Result<()>{
deduct_fees_token::handle_deduct_fees_token(ctx, winner, userid)
}
pub fn close_bet_token(ctx:Context<CloseBetToken>, winner:Pubkey, userid:String)->Result<()>{
close_bet_token::close_token_bet(ctx, winner, userid)
}
} }

View File

@ -12,5 +12,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 token_mint:Pubkey,
pub wager:u64 pub wager:u64
} }

View File

@ -1,16 +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 { Keypair, PublicKey, LAMPORTS_PER_SOL, Transaction, SystemProgram } from "@solana/web3.js";
import { ProgramTestContext, startAnchor } from "solana-bankrun";
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;
anchor.setProvider(anchor.AnchorProvider.env()); let provider: BankrunProvider;
let betsProgram: Program<Bets>;
let user1: Keypair;
let user2: Keypair;
let gameId = "game123";
let userId1 = "user123";
let userId2 = "user456";
let wager = new BN(LAMPORTS_PER_SOL); // 1 SOL
const program = anchor.workspace.Bets as Program<Bets>; before(async () => {
context = await startAnchor("", [{ name: "bets", programId: betsAddress }], []);
provider = new BankrunProvider(context);
betsProgram = new Program<Bets>(IDL, provider);
it("Is initialized!", async () => { // Create test users
// Add your test here. user1 = Keypair.generate();
const tx = await program.methods.initialize().rpc(); user2 = Keypair.generate();
console.log("Your transaction signature", tx);
// Fund accounts using bankrun
const fundAmount = 10 * LAMPORTS_PER_SOL;
// 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,
})
);
// Get recent blockhash and set it on the transaction
const [blockhash] = await context.banksClient.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = context.payer.publicKey;
// Sign the transaction with all required signers
tx.sign(context.payer, user1, user2);
await context.banksClient.processTransaction(tx);
});
it("Initializes the bets list", async () => {
await betsProgram.methods.initialize().rpc();
const [betsListAddress] = PublicKey.findProgramAddressSync(
[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({
payer: user1.publicKey,
betsList: (await PublicKey.findProgramAddressSync(
[Buffer.from("bets_list")],
betsAddress
))[0],
})
.signers([user1])
.rpc();
const [betVaultAddress] = PublicKey.findProgramAddressSync(
[
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);
});
it("Joins an existing bet", async () => {
const nonce = new BN(Date.now());
// First create a bet
await betsProgram.methods
.createBet(wager, userId1, gameId, nonce)
.accounts({
payer: user1.publicKey,
betsList: (await PublicKey.findProgramAddressSync(
[Buffer.from("bets_list")],
betsAddress
))[0],
})
.signers([user1])
.rpc();
// Then join the bet
await betsProgram.methods
.joinBet(userId2, gameId)
.accounts({
betVault: (await PublicKey.findProgramAddressSync(
[
Buffer.from("bet_vault"),
user1.publicKey.toBuffer(),
Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
betsAddress
))[0],
payer: user2.publicKey
})
.signers([user2])
.rpc();
const [betVaultAddress] = PublicKey.findProgramAddressSync(
[
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.joiner).to.eql(user2.publicKey);
expect(betVault.joinerId).to.equal(userId2);
});
it("Closes a bet with a winner", async () => {
const nonce = new BN(Date.now());
// First create a bet
await betsProgram.methods
.createBet(wager, userId1, gameId, nonce)
.accounts({
payer: user1.publicKey,
betsList: (await PublicKey.findProgramAddressSync(
[Buffer.from("bets_list")],
betsAddress
))[0],
})
.signers([user1])
.rpc();
// Join the bet
await betsProgram.methods
.joinBet(userId2, gameId)
.accounts({
betVault: (await PublicKey.findProgramAddressSync(
[
Buffer.from("bet_vault"),
user1.publicKey.toBuffer(),
Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
betsAddress
))[0],
payer: user2.publicKey
})
.signers([user2])
.rpc();
// Close the bet with user1 as winner
await betsProgram.methods
.closeBet(user1.publicKey, userId1)
.accounts({
betsList: (await PublicKey.findProgramAddressSync(
[Buffer.from("bets_list")],
betsAddress
))[0],
betVault: (await PublicKey.findProgramAddressSync(
[
Buffer.from("bet_vault"),
user1.publicKey.toBuffer(),
Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
betsAddress
))[0],
winner: user1.publicKey,
payer: user1.publicKey,
})
.signers([user1])
.rpc();
// Verify the bet is removed from the bets list
const [betsListAddress] = PublicKey.findProgramAddressSync(
[Buffer.from("bets_list")],
betsAddress
);
const betsList = await betsProgram.account.betsList.fetch(betsListAddress);
const [betVaultAddress] = PublicKey.findProgramAddressSync(
[
Buffer.from("bet_vault"),
user1.publicKey.toBuffer(),
Buffer.from(gameId),
nonce.toArrayLike(Buffer, "le", 8),
],
betsAddress
);
expect(betsList.bets).to.not.include(betVaultAddress);
}); });
}); });

619
yarn.lock

File diff suppressed because it is too large Load Diff