Skip to content

Commit f513cd2

Browse files
authored
Merge pull request #187 from quicknode/claude/vibrant-davinci-v85r1n
finance/betting-market: drop the per-wallet User index and its 32-bet cap
2 parents cf27125 + 6e7b551 commit f513cd2

38 files changed

Lines changed: 291 additions & 699 deletions

‎finance/betting-market/anchor-v1/CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [2026-10-03]
4+
5+
### Removed
6+
7+
- The per-wallet `User` index account (`seeds = [b"user", wallet]`), its
8+
`MAX_BETS_PER_USER` cap of 32 open positions, and the `TooManyBets` and
9+
`BetNotInUserIndex` errors. `place_bet`, `claim_winnings`, `claim_refund` and
10+
`close_losing_bet` no longer take a `user` account. A client lists a wallet's
11+
open bets with `getProgramAccounts` and a `memcmp` filter on `Bet.bettor` at
12+
offset 8, so a wallet can hold any number of open positions
13+
(`test_no_cap_on_open_bets_per_wallet`). Error codes after `ZeroAmount` move down by two.
14+
- Outcome labels in the tests are invented film titles.
15+
316
## [2026-09-23]
417

518
### Changed

‎finance/betting-market/anchor-v1/README.md‎

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,9 @@ cancelling).
4444
the same outcome adds to the existing Bet, so there is exactly one per (outcome, bettor). The
4545
account exists only while the position is open: it closes (rent back to the bettor) via
4646
`claim_winnings`, `claim_refund`, or `close_losing_bet`, which is also what makes a second claim
47-
impossible.
48-
- **User** (`seeds = [b"user", wallet]`) - a per-wallet index listing the bettor's open Bet
49-
addresses, so a client can find someone's positions without scanning every Bet on the program.
50-
`place_bet` adds an entry and every instruction that closes a Bet removes it, so the cap (see
51-
`MAX_BETS_PER_USER`) limits concurrent open positions, not lifetime bets. The fixed cap keeps the
52-
account a constant size; the Bet accounts are the authoritative stake record.
47+
impossible. `bettor` is the first field after the 8-byte discriminator, so a client lists a
48+
wallet's open positions with `getProgramAccounts` and a `memcmp` filter on the wallet's address at
49+
offset 8. The program keeps no per-wallet index, so a wallet can hold any number of open bets.
5350

5451
### The vault
5552

@@ -91,17 +88,14 @@ division floors each share, leaving at most a few minor units of dust in the vau
9188
- `open_betting` - admin. Moves a `Draft` with at least two outcomes to `Open`, which fixes the
9289
outcome list.
9390
- `place_bet` - bettor. Stakes tokens on one outcome of an `Open` event, before
94-
`betting_closes_at`; updates the pools and adds the Bet to the user's index (rejected with
95-
`TooManyBets` if all `MAX_BETS_PER_USER` slots hold open positions).
91+
`betting_closes_at`; creates or tops up the Bet and updates the pools.
9692
- `settle_event` - admin. Once `betting_closes_at` has passed, resolves to a winning outcome, takes
9793
the fee, records the payout figures.
9894
- `claim_winnings` - winning bettor. Withdraws stake plus pro-rata share of the losing pool, then
99-
closes the Bet account and removes it from the user's index.
100-
- `close_losing_bet` - losing bettor. After settlement, closes a worthless Bet to reclaim its rent
101-
and free the slot in the user's index.
95+
closes the Bet account.
96+
- `close_losing_bet` - losing bettor. After settlement, closes a worthless Bet to reclaim its rent.
10297
- `cancel_event` - admin. Voids a draft or unresolved market.
103-
- `claim_refund` - bettor. After a cancellation, reclaims the exact stake; the Bet account closes
104-
and leaves the user's index.
98+
- `claim_refund` - bettor. After a cancellation, reclaims the exact stake; the Bet account closes.
10599

106100
### Lifecycle
107101

@@ -142,8 +136,8 @@ Tests are Rust integration tests running against
142136
settle → claim with exact payout and fee assertions), admin authorization, the bet-after-settle and
143137
double-claim guards, the outcome list locking when betting opens, the two-outcome minimum, both
144138
edges of the betting close time, settling an outcome with no bets, the cancel/refund path, the
145-
`close_losing_bet` guards, and the User index: claims, refunds, and losing-bet closes remove the
146-
Bet's entry, and a wallet whose index is full can bet again after closing a position.
139+
`close_losing_bet` guards, and a wallet holding forty open bets at once, which shows there is no
140+
per-wallet cap.
147141

148142
```sh
149143
anchor test

‎finance/betting-market/anchor-v1/programs/betting-market/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ custom-heap = []
2020
custom-panic = []
2121

2222
[dependencies]
23-
# init-if-needed: place_bet and the lazy User index create accounts only on first use.
23+
# init-if-needed: place_bet creates the Bet account only on a bettor's first bet on an outcome.
2424
anchor-lang = { version = "1.2.0", features = ["init-if-needed"] }
2525
anchor-spl = "1.2.0"
2626

‎finance/betting-market/anchor-v1/programs/betting-market/src/error.rs‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ pub enum BettingError {
2222
BetWon,
2323
#[msg("The bet amount must be greater than zero")]
2424
ZeroAmount,
25-
#[msg("This bettor already holds the maximum number of open positions")]
26-
TooManyBets,
27-
#[msg("This bet is not in the bettor's User index")]
28-
BetNotInUserIndex,
2925
#[msg("Arithmetic overflow")]
3026
MathOverflow,
3127
#[msg("Outcomes can only be added, and betting opened, while the event is a draft")]

‎finance/betting-market/anchor-v1/programs/betting-market/src/instructions/claim_refund.rs‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anchor_lang::prelude::*;
22
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
33

4-
use crate::{error::BettingError, Bet, Event, EventStatus, User};
4+
use crate::{error::BettingError, Bet, Event, EventStatus};
55

66
use super::transfer_tokens_from_vault;
77

@@ -31,13 +31,6 @@ pub struct ClaimRefundAccountConstraints<'info> {
3131
)]
3232
pub bet: Account<'info, Bet>,
3333

34-
#[account(
35-
mut,
36-
seeds = [b"user", bettor.key().as_ref()],
37-
bump = user.bump,
38-
)]
39-
pub user: Account<'info, User>,
40-
4134
#[account(
4235
mut,
4336
associated_token::mint = token_mint,
@@ -65,12 +58,6 @@ pub fn handle_claim_refund(context: Context<ClaimRefundAccountConstraints>) -> R
6558

6659
let stake = context.accounts.bet.amount;
6760

68-
// The position is over, so drop the Bet from the bettor's index before the
69-
// transfer (effects before interactions); the Bet account itself closes
70-
// when the instruction finishes.
71-
let bet_key = context.accounts.bet.key();
72-
context.accounts.user.remove_bet(&bet_key)?;
73-
7461
let event_id = context.accounts.event.event_id;
7562
let event_bump = context.accounts.event.bump;
7663
transfer_tokens_from_vault(

‎finance/betting-market/anchor-v1/programs/betting-market/src/instructions/claim_winnings.rs‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anchor_lang::prelude::*;
22
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
33

4-
use crate::{error::BettingError, Bet, Event, EventStatus, User};
4+
use crate::{error::BettingError, Bet, Event, EventStatus};
55

66
use super::transfer_tokens_from_vault;
77

@@ -31,13 +31,6 @@ pub struct ClaimWinningsAccountConstraints<'info> {
3131
)]
3232
pub bet: Account<'info, Bet>,
3333

34-
#[account(
35-
mut,
36-
seeds = [b"user", bettor.key().as_ref()],
37-
bump = user.bump,
38-
)]
39-
pub user: Account<'info, User>,
40-
4134
#[account(
4235
mut,
4336
associated_token::mint = token_mint,
@@ -90,12 +83,6 @@ pub fn handle_claim_winnings(context: Context<ClaimWinningsAccountConstraints>)
9083
.checked_add(winnings)
9184
.ok_or(BettingError::MathOverflow)?;
9285

93-
// The position is over, so drop the Bet from the bettor's index before the
94-
// transfer (effects before interactions); the Bet account itself closes
95-
// when the instruction finishes.
96-
let bet_key = context.accounts.bet.key();
97-
context.accounts.user.remove_bet(&bet_key)?;
98-
9986
let event_id = context.accounts.event.event_id;
10087
let event_bump = context.accounts.event.bump;
10188
transfer_tokens_from_vault(

‎finance/betting-market/anchor-v1/programs/betting-market/src/instructions/close_losing_bet.rs‎

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
use anchor_lang::prelude::*;
22

3-
use crate::{error::BettingError, Bet, Event, EventStatus, User};
3+
use crate::{error::BettingError, Bet, Event, EventStatus};
44

5-
// A losing bet pays nothing, but it still occupies a slot in the bettor's
6-
// User index and holds rent. Closing it frees the slot (so the bettor can
7-
// open a new position) and returns the rent. Winning bets must go through
8-
// claim_winnings instead, which also pays out the stake and winnings.
5+
// A losing bet pays nothing, but its account still holds rent. Closing it
6+
// returns the rent to the bettor. Winning bets must go through claim_winnings
7+
// instead, which also pays out the stake and winnings.
98
#[derive(Accounts)]
109
pub struct CloseLosingBetAccountConstraints<'info> {
1110
#[account(mut)]
@@ -26,13 +25,6 @@ pub struct CloseLosingBetAccountConstraints<'info> {
2625
bump = bet.bump,
2726
)]
2827
pub bet: Account<'info, Bet>,
29-
30-
#[account(
31-
mut,
32-
seeds = [b"user", bettor.key().as_ref()],
33-
bump = user.bump,
34-
)]
35-
pub user: Account<'info, User>,
3628
}
3729

3830
pub fn handle_close_losing_bet(context: Context<CloseLosingBetAccountConstraints>) -> Result<()> {
@@ -45,7 +37,5 @@ pub fn handle_close_losing_bet(context: Context<CloseLosingBetAccountConstraints
4537
BettingError::BetWon
4638
);
4739

48-
let bet_key = context.accounts.bet.key();
49-
context.accounts.user.remove_bet(&bet_key)?;
5040
Ok(())
5141
}

‎finance/betting-market/anchor-v1/programs/betting-market/src/instructions/place_bet.rs‎

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ use anchor_spl::{
44
token_interface::{Mint, TokenAccount, TokenInterface},
55
};
66

7-
use crate::{
8-
betting_is_open, error::BettingError, Bet, Config, Event, EventStatus, Outcome, User,
9-
MAX_BETS_PER_USER,
10-
};
7+
use crate::{betting_is_open, error::BettingError, Bet, Config, Event, EventStatus, Outcome};
118

129
use super::transfer_tokens_to_vault;
1310

@@ -66,15 +63,6 @@ pub struct PlaceBetAccountConstraints<'info> {
6663
)]
6764
pub bet: Box<Account<'info, Bet>>,
6865

69-
#[account(
70-
init_if_needed,
71-
payer = bettor,
72-
space = User::DISCRIMINATOR.len() + User::INIT_SPACE,
73-
seeds = [b"user", bettor.key().as_ref()],
74-
bump
75-
)]
76-
pub user: Box<Account<'info, User>>,
77-
7866
pub associated_token_program: Program<'info, AssociatedToken>,
7967
pub token_program: Interface<'info, TokenInterface>,
8068
pub system_program: Program<'info, System>,
@@ -105,9 +93,7 @@ pub fn handle_place_bet(context: Context<PlaceBetAccountConstraints>, amount: u6
10593
let event_key = context.accounts.event.key();
10694
let outcome_key = context.accounts.outcome.key();
10795
let outcome_index = context.accounts.outcome.index;
108-
let bet_key = context.accounts.bet.key();
10996
let bet_bump = context.bumps.bet;
110-
let user_bump = context.bumps.user;
11197

11298
let bet = &mut context.accounts.bet;
11399
// A fresh init_if_needed Bet has amount 0; that is how we tell a first bet
@@ -143,18 +129,5 @@ pub fn handle_place_bet(context: Context<PlaceBetAccountConstraints>, amount: u6
143129
.checked_add(amount)
144130
.ok_or(BettingError::MathOverflow)?;
145131

146-
let user = &mut context.accounts.user;
147-
if user.authority == Pubkey::default() {
148-
user.authority = bettor_key;
149-
user.bump = user_bump;
150-
}
151-
if is_new_bet {
152-
require!(
153-
user.bets.len() < MAX_BETS_PER_USER,
154-
BettingError::TooManyBets
155-
);
156-
user.bets.push(bet_key);
157-
}
158-
159132
Ok(())
160133
}

‎finance/betting-market/anchor-v1/programs/betting-market/src/lib.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,13 @@ pub mod betting_market {
7474
}
7575

7676
// A winner withdraws their stake plus their pro-rata share of the losing
77-
// pool. The Bet account closes and leaves the bettor's User index.
77+
// pool. The Bet account closes.
7878
pub fn claim_winnings(context: Context<ClaimWinningsAccountConstraints>) -> Result<()> {
7979
instructions::claim_winnings::handle_claim_winnings(context)
8080
}
8181

8282
// A loser closes their worthless bet after settlement, reclaiming the
83-
// Bet account's rent and freeing the slot in their User index.
83+
// Bet account's rent.
8484
pub fn close_losing_bet(context: Context<CloseLosingBetAccountConstraints>) -> Result<()> {
8585
instructions::close_losing_bet::handle_close_losing_bet(context)
8686
}
@@ -91,7 +91,7 @@ pub mod betting_market {
9191
}
9292

9393
// After a cancellation, a bettor reclaims their exact stake. The Bet
94-
// account closes and leaves the bettor's User index.
94+
// account closes.
9595
pub fn claim_refund(context: Context<ClaimRefundAccountConstraints>) -> Result<()> {
9696
instructions::claim_refund::handle_claim_refund(context)
9797
}

‎finance/betting-market/anchor-v1/programs/betting-market/src/state/bet.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ use anchor_lang::prelude::*;
55
// one Bet per (outcome, bettor). The account lives only while the position is
66
// open: it closes (rent back to the bettor) on claim_winnings, claim_refund,
77
// or close_losing_bet, which is also what prevents double claims.
8+
//
9+
// `bettor` is the first field, right after the 8-byte discriminator, so a
10+
// client lists a wallet's open positions with getProgramAccounts and a memcmp
11+
// filter on the wallet's address at offset 8. The program keeps no per-wallet
12+
// index, so there is no limit on how many positions a wallet holds.
813
#[account]
914
#[derive(InitSpace)]
1015
pub struct Bet {

0 commit comments

Comments
 (0)