Skip to main content

streak_api/
sdk.rs

1//! Off-chain instruction builders (Ore **`api::sdk`** style): fixed account order mirrors on-chain **`process_*`** comment headers.
2//!
3//! For **`ClaimPositionFee`**, pass the **`cp\_amm`** account tail (13 metas, Anchor `ClaimPositionFeeCtx` order).
4
5use solana_program::pubkey::Pubkey;
6use spl_associated_token_account::{
7    get_associated_token_address,
8    get_associated_token_address_with_program_id,
9};
10use steel::*;
11
12use crate::{
13    consts::{
14        EXECUTOR_ADDRESS, EXECUTOR_PAY_DAILY_JACKPOT, EXECUTOR_PAY_WEEKLY_JACKPOT, FEE_COLLECTOR,
15        USDC_MAINNET_MINT,
16    },
17    instruction::*,
18    state::{Ledger, Market, Position, Treasury},
19};
20
21#[inline(always)]
22pub fn program_id() -> Pubkey {
23    crate::ID
24}
25
26#[inline(always)]
27fn executor_treasury_ix_bytes(kind: u8, pool: u8, series_id: u16, amount: u64) -> Vec<u8> {
28    ExecutorTreasury {
29        kind,
30        pool,
31        series_id: series_id.to_le_bytes(),
32        _pad_ix: [0; 4],
33        amount: amount.to_le_bytes(),
34    }
35    .to_bytes()
36}
37
38/// SPL USDC ATA (**classic** SPL Token program). For Token-2022 mints use [`treasury_usdc_ata_with_token_program`] / [`associated_usdc_ata`] with the mint owner's program id (for example **`spl_token_2022`**'s **`id()`**).
39#[inline(always)]
40pub fn treasury_usdc_ata() -> Pubkey {
41    get_associated_token_address(&Treasury::pda().0, &USDC_MAINNET_MINT)
42}
43
44#[inline(always)]
45pub fn treasury_usdc_ata_with_token_program(token_program: &Pubkey) -> Pubkey {
46    get_associated_token_address_with_program_id(&Treasury::pda().0, &USDC_MAINNET_MINT, token_program)
47}
48
49#[inline(always)]
50pub fn associated_usdc_ata(owner: Pubkey, token_program: &Pubkey) -> Pubkey {
51    get_associated_token_address_with_program_id(&owner, &USDC_MAINNET_MINT, token_program)
52}
53
54/// SPL USDC ATA for **`owner`** (classic Token program — mainnet Circle USDC).
55#[inline(always)]
56pub fn usdc_ata(owner: Pubkey) -> Pubkey {
57    get_associated_token_address(&owner, &USDC_MAINNET_MINT)
58}
59
60/// Team-fee SPL destination used by **`BuyTickets`** / **`ClaimPositionFee`** (classic Token mint).
61#[inline(always)]
62pub fn fee_collector_usdc_ata() -> Pubkey {
63    get_associated_token_address(&FEE_COLLECTOR, &USDC_MAINNET_MINT)
64}
65
66#[inline(always)]
67pub fn fee_collector_usdc_ata_with_token_program(token_program: &Pubkey) -> Pubkey {
68    get_associated_token_address_with_program_id(&FEE_COLLECTOR, &USDC_MAINNET_MINT, token_program)
69}
70
71/// **`previous_market`** / **`previous_position`** (**`series_id`**, **`period - 1`**). For **`period == 0`** use placeholders (**no signer**).
72#[inline(always)]
73pub fn buy_tickets_or_place_bet_previous_accounts(
74    user: Pubkey,
75    series_id: u16,
76    period: u64,
77) -> (Pubkey, Pubkey) {
78    match period.checked_sub(1) {
79        Some(p) => (
80            Market::pda(series_id, p).0,
81            Position::pda(series_id, p, user).0,
82        ),
83        None => (system_program::ID, system_program::ID),
84    }
85}
86
87// let [payer_info, treasury_info, mint_info, treasury_ata, system_program, token_program, ata_program]
88
89/// One-time **`Initialize`**. **`payer`** must be [`ADMIN_ADDRESS`](crate::consts::ADMIN_ADDRESS).
90///
91/// **`token_program`** must be the mint owner's program id (`spl_token` or `spl_token_2022` for the configured USDC mint).
92pub fn initialize(payer: Pubkey, token_program: Pubkey) -> Instruction {
93    let treasury_address = Treasury::pda().0;
94    let mint_address = USDC_MAINNET_MINT;
95    let treasury_ata = treasury_usdc_ata_with_token_program(&token_program);
96    Instruction {
97        program_id: crate::ID,
98        accounts: vec![
99            AccountMeta::new(payer, true),
100            AccountMeta::new(treasury_address, false),
101            AccountMeta::new_readonly(mint_address, false),
102            AccountMeta::new(treasury_ata, false),
103            AccountMeta::new_readonly(system_program::ID, false),
104            AccountMeta::new_readonly(token_program, false),
105            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
106        ],
107        data: Initialize {}.to_bytes(),
108    }
109}
110
111// let [user, treasury_info, ledger_info, mint_info, user_ata, treasury_ata, team_ata, system_program, token_program, ata_program, previous_market_info, previous_position_info]
112
113/// Builds **`BuyTickets`**.
114pub fn buy_tickets(
115    user: Pubkey,
116    series_id: u16,
117    period: u64,
118    amount: u64,
119    token_program: Pubkey,
120) -> Instruction {
121    let treasury_address = Treasury::pda().0;
122    let ledger_address = Ledger::pda(user).0;
123    let mint_address = USDC_MAINNET_MINT;
124    let user_ata = associated_usdc_ata(user, &token_program);
125    let treasury_ata_addr = treasury_usdc_ata_with_token_program(&token_program);
126    let team_ata_addr = fee_collector_usdc_ata_with_token_program(&token_program);
127    let (prev_mkt, prev_pos) =
128        buy_tickets_or_place_bet_previous_accounts(user, series_id, period);
129    Instruction {
130        program_id: crate::ID,
131        accounts: vec![
132            AccountMeta::new(user, true),
133            AccountMeta::new(treasury_address, false),
134            AccountMeta::new(ledger_address, false),
135            AccountMeta::new_readonly(mint_address, false),
136            AccountMeta::new(user_ata, false),
137            AccountMeta::new(treasury_ata_addr, false),
138            AccountMeta::new(team_ata_addr, false),
139            AccountMeta::new_readonly(system_program::ID, false),
140            AccountMeta::new_readonly(token_program, false),
141            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
142            AccountMeta::new_readonly(prev_mkt, false),
143            AccountMeta::new_readonly(prev_pos, false),
144        ],
145        data: BuyTickets {
146            series_id: series_id.to_le_bytes(),
147            _pad_ix: [0; 6],
148            period: period.to_le_bytes(),
149            amount: amount.to_le_bytes(),
150        }
151        .to_bytes(),
152    }
153}
154
155// let [user, ledger_info, market_info, position_info, previous_market_info, previous_position_info, system_program]
156
157/// Builds **`PlaceBet`**.
158///
159/// **`ticket_units`** may be **0** in the live window to activate a **`Position::STATE_COMMITTED_PREOPEN`** stake (**merge into `Market::total_*`** without debiting **`Ledger`**). See **`PlaceBet`** ix docs on-chain.
160pub fn place_bet(
161    user: Pubkey,
162    series_id: u16,
163    period: u64,
164    ticket_units: u64,
165    side: u8,
166) -> Instruction {
167    let ledger_address = Ledger::pda(user).0;
168    let treasury_address = Treasury::pda().0;
169    let market_address = Market::pda(series_id, period).0;
170    let position_address = Position::pda(series_id, period, user).0;
171    let (prev_mkt, prev_pos) =
172        buy_tickets_or_place_bet_previous_accounts(user, series_id, period);
173    Instruction {
174        program_id: crate::ID,
175        accounts: vec![
176            AccountMeta::new(user, true),
177            AccountMeta::new(ledger_address, false),
178            AccountMeta::new_readonly(treasury_address, false),
179            AccountMeta::new(market_address, false),
180            AccountMeta::new(position_address, false),
181            AccountMeta::new_readonly(prev_mkt, false),
182            AccountMeta::new_readonly(prev_pos, false),
183            AccountMeta::new_readonly(system_program::ID, false),
184        ],
185        data: PlaceBet {
186            series_id: series_id.to_le_bytes(),
187            _pad_ix: [0; 6],
188            period: period.to_le_bytes(),
189            ticket_units: ticket_units.to_le_bytes(),
190            side,
191            _pad: [0; 7],
192        }
193        .to_bytes(),
194    }
195}
196
197// let [executor, treasury_readonly, market, position]
198
199/// **`ExecutorTreasury`** **`EXECUTOR_KIND_MERGE_COMMITTED_POSITION`**: merges **[`crate::state::Position::STATE_COMMITTED_PREOPEN`]** → **`STATE_PENDING`** + **`Market::total_*`**. **`pool`** **`0`**, **`amount`** **`0`**, **`series_id`** in payload.
200pub fn executor_treasury_merge_committed(
201    executor: Pubkey,
202    series_id: u16,
203    period: u64,
204    position_owner: Pubkey,
205) -> Instruction {
206    let treasury_address = Treasury::pda().0;
207    let market_address = Market::pda(series_id, period).0;
208    let position_address = Position::pda(series_id, period, position_owner).0;
209    Instruction {
210        program_id: crate::ID,
211        accounts: vec![
212            AccountMeta::new(executor, true),
213            AccountMeta::new_readonly(treasury_address, false),
214            AccountMeta::new(market_address, false),
215            AccountMeta::new(position_address, false),
216        ],
217        data: executor_treasury_ix_bytes(
218            EXECUTOR_KIND_MERGE_COMMITTED_POSITION,
219            0,
220            series_id,
221            0,
222        ),
223    }
224}
225
226/// Builds **`AdminInstantSettlement`**. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`].
227///
228/// **`price_update_account`**: address of the Hermes `PriceUpdateV2` account posted just before
229/// this instruction by the executor via the Pyth Receiver program. Subsidy is computed on-chain.
230pub fn settle_market(
231    authority: Pubkey,
232    series_id: u16,
233    period: u64,
234    price_update_account: Pubkey,
235) -> Instruction {
236    let treasury_address = Treasury::pda().0;
237    let market_address = Market::pda(series_id, period).0;
238    Instruction {
239        program_id: crate::ID,
240        accounts: vec![
241            AccountMeta::new(authority, true),
242            AccountMeta::new(treasury_address, false),
243            AccountMeta::new(market_address, false),
244            AccountMeta::new_readonly(price_update_account, false),
245        ],
246        data: AdminInstantSettlement {
247            series_id: series_id.to_le_bytes(),
248            _pad_ix: [0; 6],
249            period: period.to_le_bytes(),
250        }
251        .to_bytes(),
252    }
253}
254
255/// Builds **`InitMarket`**. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`].
256///
257/// **`feed_id`**: Pyth Hermes price feed identifier (32 bytes) stored in `Market::pyth_price_feed`.
258/// Use [`crate::consts::PYTH_BTC_USD_FEED_ID`] for BTC/USD.
259///
260/// **`price_update_account`**: address of a current Hermes `PriceUpdateV2` account for the
261/// open-anchor snapshot. Pass `system_program::ID` when creating the market ahead of `open_ts`
262/// and crank separately once inside the anchor window.
263pub fn init_market(
264    authority: Pubkey,
265    series_id: u16,
266    period: u64,
267    open_ts: i64,
268    close_ts: i64,
269    feed_id: [u8; 32],
270    price_update_account: Pubkey,
271    rules_hash: [u8; 32],
272) -> Instruction {
273    let market_address = Market::pda(series_id, period).0;
274    let treasury_address = Treasury::pda().0;
275    Instruction {
276        program_id: crate::ID,
277        accounts: vec![
278            AccountMeta::new(authority, true),
279            AccountMeta::new(market_address, false),
280            AccountMeta::new_readonly(treasury_address, false),
281            AccountMeta::new_readonly(system_program::ID, false),
282            AccountMeta::new_readonly(price_update_account, false),
283        ],
284        data: InitMarket {
285            series_id: series_id.to_le_bytes(),
286            _pad_ix: [0; 6],
287            period: period.to_le_bytes(),
288            open_ts: open_ts.to_le_bytes(),
289            close_ts: close_ts.to_le_bytes(),
290            pyth_price_feed: feed_id,
291            rules_hash,
292        }
293        .to_bytes(),
294    }
295}
296
297/// Builds **`AdminVoidMarket`**. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`] or
298/// [`crate::consts::ADMIN_ADDRESS`].
299///
300/// Call when BTC price movement is < 0.01% (`BTC_VOID_THRESHOLD_BPS`) or on oracle failure.
301/// All `STATE_PENDING` positions must subsequently be refunded via [`refund_void_position`].
302pub fn void_market(authority: Pubkey, series_id: u16, period: u64) -> Instruction {
303    let treasury_address = Treasury::pda().0;
304    let market_address = Market::pda(series_id, period).0;
305    Instruction {
306        program_id: crate::ID,
307        accounts: vec![
308            AccountMeta::new(authority, true),
309            AccountMeta::new_readonly(treasury_address, false),
310            AccountMeta::new(market_address, false),
311        ],
312        data: AdminVoidMarket {
313            series_id: series_id.to_le_bytes(),
314            _pad_ix: [0; 6],
315            period: period.to_le_bytes(),
316        }
317        .to_bytes(),
318    }
319}
320
321/// Builds **`AdminRefundVoidPosition`**. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`].
322///
323/// Refunds `position_owner`'s `STATE_PENDING` position in a voided market back to their `Ledger`.
324/// Iterate all stakers and call this once per position.
325pub fn refund_void_position(
326    authority: Pubkey,
327    series_id: u16,
328    period: u64,
329    position_owner: Pubkey,
330) -> Instruction {
331    let market_address = Market::pda(series_id, period).0;
332    let position_address = Position::pda(series_id, period, position_owner).0;
333    let ledger_address = Ledger::pda(position_owner).0;
334    Instruction {
335        program_id: crate::ID,
336        accounts: vec![
337            AccountMeta::new(authority, true),
338            AccountMeta::new_readonly(market_address, false),
339            AccountMeta::new(position_address, false),
340            AccountMeta::new(ledger_address, false),
341        ],
342        data: AdminRefundVoidPosition {
343            series_id: series_id.to_le_bytes(),
344            _pad_ix: [0; 6],
345            period: period.to_le_bytes(),
346        }
347        .to_bytes(),
348    }
349}
350
351// let [executor, treasury_info, treasury_ata, recipient_ata, mint_info, token_program, market_info, position_info, ledger_info]
352
353/// **`recipient_ata`** must be **[`associated_usdc_ata`](associated_usdc_ata)(winning_authority, &token_program)**.
354pub fn distribute_market_reward(
355    executor: Pubkey,
356    winning_authority: Pubkey,
357    series_id: u16,
358    market_period: u64,
359    token_program: Pubkey,
360) -> Instruction {
361    let treasury_address = Treasury::pda().0;
362    let treasury_ata_addr = treasury_usdc_ata_with_token_program(&token_program);
363    let recipient_ata = associated_usdc_ata(winning_authority, &token_program);
364    let mint_address = USDC_MAINNET_MINT;
365    let market_address = Market::pda(series_id, market_period).0;
366    let position_address = Position::pda(series_id, market_period, winning_authority).0;
367    let ledger_address = Ledger::pda(winning_authority).0;
368    Instruction {
369        program_id: crate::ID,
370        accounts: vec![
371            AccountMeta::new(executor, true),
372            AccountMeta::new(treasury_address, false),
373            AccountMeta::new(treasury_ata_addr, false),
374            AccountMeta::new(recipient_ata, false),
375            AccountMeta::new_readonly(mint_address, false),
376            AccountMeta::new_readonly(token_program, false),
377            AccountMeta::new_readonly(market_address, false),
378            AccountMeta::new(position_address, false),
379            AccountMeta::new(ledger_address, false),
380        ],
381        data: executor_treasury_ix_bytes(EXECUTOR_KIND_DISTRIBUTE_MARKET_REWARD, 0, series_id, 0),
382    }
383}
384
385/// Convenience: fixed executor pubkey ([`EXECUTOR_ADDRESS`](crate::consts::EXECUTOR_ADDRESS)).
386pub fn distribute_market_reward_default_executor(
387    winning_authority: Pubkey,
388    series_id: u16,
389    market_period: u64,
390    token_program: Pubkey,
391) -> Instruction {
392    distribute_market_reward(
393        EXECUTOR_ADDRESS,
394        winning_authority,
395        series_id,
396        market_period,
397        token_program,
398    )
399}
400
401/// **`ExecutorTreasury`** **`EXECUTOR_KIND_MARKET_LINE_RELEASE`**: after **`DISTRIBUTE`** batch(es), **`Treasury`** advances **`next_period[slot]`** toward **`≥ settled_period + 1`** (**`max`** with prior cursor).
402///
403/// **Accounts:** **`executor`**, **`treasury`**, **`market`** (readonly).
404pub fn release_market_line(executor: Pubkey, series_id: u16, period: u64) -> Instruction {
405    let treasury_address = Treasury::pda().0;
406    let market_address = Market::pda(series_id, period).0;
407    Instruction {
408        program_id: crate::ID,
409        accounts: vec![
410            AccountMeta::new(executor, true),
411            AccountMeta::new(treasury_address, false),
412            AccountMeta::new_readonly(market_address, false),
413        ],
414        data: executor_treasury_ix_bytes(EXECUTOR_KIND_MARKET_LINE_RELEASE, 0, series_id, period),
415    }
416}
417
418#[inline(always)]
419pub fn release_market_line_default_executor(series_id: u16, period: u64) -> Instruction {
420    release_market_line(EXECUTOR_ADDRESS, series_id, period)
421}
422
423// let [executor, treasury_info, treasury_ata, mint_info, token_program, recipient_ata]
424
425/// **`pool`**: [`EXECUTOR_PAY_DAILY_JACKPOT`](crate::consts::EXECUTOR_PAY_DAILY_JACKPOT) or [`EXECUTOR_PAY_WEEKLY_JACKPOT`](crate::consts::EXECUTOR_PAY_WEEKLY_JACKPOT).
426///
427/// **`recipient_ata`** is any SPL token account holding USDC (**mint + program-id** enforced on-chain); use **[`associated_usdc_ata`]** when building the ATA address.
428pub fn executor_treasury_pay_recipient_account(
429    executor: Pubkey,
430    recipient_ata: Pubkey,
431    amount: u64,
432    pool: u8,
433    token_program: Pubkey,
434) -> Instruction {
435    let treasury_address = Treasury::pda().0;
436    let treasury_ata_addr = treasury_usdc_ata_with_token_program(&token_program);
437    let mint_address = USDC_MAINNET_MINT;
438    Instruction {
439        program_id: crate::ID,
440        accounts: vec![
441            AccountMeta::new(executor, true),
442            AccountMeta::new(treasury_address, false),
443            AccountMeta::new(treasury_ata_addr, false),
444            AccountMeta::new_readonly(mint_address, false),
445            AccountMeta::new_readonly(token_program, false),
446            AccountMeta::new(recipient_ata, false),
447        ],
448        data: executor_treasury_ix_bytes(EXECUTOR_KIND_JACKPOT_PAY, pool, 0, amount),
449    }
450}
451
452/// Same as **`executor_treasury_pay_recipient_account`** with **`recipient_ata`** = **[`associated_usdc_ata`](associated_usdc_ata)(recipient_owner, token_program)**.
453#[inline(always)]
454pub fn executor_treasury_pay(
455    executor: Pubkey,
456    recipient_owner: Pubkey,
457    amount: u64,
458    pool: u8,
459    token_program: Pubkey,
460) -> Instruction {
461    executor_treasury_pay_recipient_account(
462        executor,
463        associated_usdc_ata(recipient_owner, &token_program),
464        amount,
465        pool,
466        token_program,
467    )
468}
469
470/// Daily jackpot pool preset.
471#[inline(always)]
472pub fn executor_treasury_pay_daily(
473    executor: Pubkey,
474    recipient_owner: Pubkey,
475    amount: u64,
476    token_program: Pubkey,
477) -> Instruction {
478    executor_treasury_pay_recipient_account(
479        executor,
480        associated_usdc_ata(recipient_owner, &token_program),
481        amount,
482        EXECUTOR_PAY_DAILY_JACKPOT,
483        token_program,
484    )
485}
486
487/// Weekly jackpot pool preset.
488#[inline(always)]
489pub fn executor_treasury_pay_weekly(
490    executor: Pubkey,
491    recipient_owner: Pubkey,
492    amount: u64,
493    token_program: Pubkey,
494) -> Instruction {
495    executor_treasury_pay_recipient_account(
496        executor,
497        associated_usdc_ata(recipient_owner, &token_program),
498        amount,
499        EXECUTOR_PAY_WEEKLY_JACKPOT,
500        token_program,
501    )
502}
503
504// Streak indices 0..8: executor, owner, treasury, treasury_ata, fee_collector_ata, mint, system_program, token_program, owner_usdc_ata
505// Indices 9..22: cp_amm ClaimPositionFeeCtx (see `programs/program/src/admin/claim_position_fee.rs`)
506
507/// CPI prefix metas only; caller appends **`cp\_amm`** accounts.
508pub fn claim_position_fee_accounts_prefix(
509    owner: Pubkey,
510    owner_usdc_ata: Pubkey,
511    token_program: Pubkey,
512) -> [AccountMeta; 9] {
513    let executor = EXECUTOR_ADDRESS;
514    let treasury_address = Treasury::pda().0;
515    let treasury_ata_addr = treasury_usdc_ata_with_token_program(&token_program);
516    let team_ata_addr = fee_collector_usdc_ata_with_token_program(&token_program);
517    [
518        AccountMeta::new(executor, true),
519        AccountMeta::new(owner, true),
520        AccountMeta::new(treasury_address, false),
521        AccountMeta::new(treasury_ata_addr, false),
522        AccountMeta::new(team_ata_addr, false),
523        AccountMeta::new_readonly(USDC_MAINNET_MINT, false),
524        AccountMeta::new_readonly(system_program::ID, false),
525        AccountMeta::new_readonly(token_program, false),
526        AccountMeta::new(owner_usdc_ata, false),
527    ]
528}
529
530pub fn claim_position_fee(
531    owner: Pubkey,
532    owner_usdc_ata: Pubkey,
533    cp_amm_accounts: &[AccountMeta],
534    token_program: Pubkey,
535) -> Instruction {
536    let prefix = claim_position_fee_accounts_prefix(owner, owner_usdc_ata, token_program);
537    let mut accounts = Vec::with_capacity(9 + cp_amm_accounts.len());
538    accounts.extend_from_slice(&prefix);
539    accounts.extend_from_slice(cp_amm_accounts);
540    Instruction {
541        program_id: crate::ID,
542        accounts,
543        data: ClaimPositionFee {}.to_bytes(),
544    }
545}
546
547/// Advance `Treasury::current_week` by 1. **`authority`** must be [`crate::consts::EXECUTOR_ADDRESS`].
548///
549/// Call once at the start of each scoring week. All `Ledger` weekly stats reset lazily on next
550/// interaction — no sweep required.
551pub fn admin_init_week(authority: Pubkey) -> Instruction {
552    let treasury_address = Treasury::pda().0;
553    Instruction {
554        program_id: crate::ID,
555        accounts: vec![
556            AccountMeta::new(authority, true),
557            AccountMeta::new(treasury_address, false),
558        ],
559        data: AdminInitWeek {}.to_bytes(),
560    }
561}