1#![allow(unexpected_cfgs)]
2#![allow(clippy::diverging_sub_expression)]
3
4use anchor_lang::prelude::*;
5use anchor_lang::solana_program::{program::invoke, system_instruction};
6
7const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
8
9#[cfg(feature = "no-entrypoint")]
10pub use session_keys_macros::*;
11
12declare_id!("KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5");
13
14#[cfg(not(feature = "no-entrypoint"))]
15solana_security_txt::security_txt! {
16 name: "session_keys",
17 project_url: "https://magicblock.gg",
18 contacts: "email:dev@magicblock.gg,twitter:@magicblock",
19 policy: "",
20 preferred_languages: "en",
21 source_code: "https://github.com/magicblock-labs"
22}
23
24#[program]
25pub mod gpl_session {
26 use super::*;
27
28 pub fn create_session(
30 ctx: Context<CreateSessionToken>,
31 top_up: Option<bool>,
32 valid_until: Option<i64>,
33 lamports: Option<u64>,
34 ) -> Result<()> {
35 let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
36 create_session_token_handler(ctx, top_up, valid_until, lamports)
37 }
38
39 pub fn create_session_with_payer(
40 ctx: Context<CreateSessionTokenWithPayer>,
41 top_up: Option<bool>,
42 valid_until: Option<i64>,
43 lamports: Option<u64>,
44 ) -> Result<()> {
45 let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
46 create_session_token_with_payer_handler(ctx, top_up, valid_until, lamports)
47 }
48 pub fn revoke_session(ctx: Context<RevokeSessionToken>) -> Result<()> {
50 revoke_session_token_handler(ctx)
51 }
52
53 pub fn create_session_v2(
59 ctx: Context<CreateSessionTokenV2>,
60 top_up: Option<bool>,
61 valid_until: Option<i64>,
62 lamports: Option<u64>,
63 ) -> Result<()> {
64 let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
65 create_session_token_handler_v2(ctx, top_up, valid_until, lamports)
66 }
67
68 pub fn revoke_session_v2(ctx: Context<RevokeSessionTokenV2>) -> Result<()> {
69 revoke_session_token_handler_v2(ctx)
70 }
71}
72
73fn process_session_params(top_up: Option<bool>, valid_until: Option<i64>) -> Result<(bool, i64)> {
74 let top_up = top_up.unwrap_or(false);
75 let valid_until = valid_until.unwrap_or(Clock::get()?.unix_timestamp + 60 * 60);
76 Ok((top_up, valid_until))
77}
78
79#[derive(Accounts)]
81pub struct CreateSessionToken<'info> {
82 #[account(
83 init,
84 seeds = [
85 SessionToken::SEED_PREFIX.as_bytes(),
86 target_program.key().as_ref(),
87 session_signer.key().as_ref(),
88 authority.key().as_ref()
89 ],
90 bump,
91 payer = authority,
92 space = SessionToken::LEN
93 )]
94 pub session_token: Account<'info, SessionToken>,
95
96 #[account(mut)]
97 pub session_signer: Signer<'info>,
98 #[account(mut)]
99 pub authority: Signer<'info>,
100
101 #[account(executable)]
103 pub target_program: UncheckedAccount<'info>,
104
105 pub system_program: Program<'info, System>,
106}
107
108struct CreateSessionTokenParams {
109 authority: Pubkey,
110 target_program: Pubkey,
111 session_signer: Pubkey,
112 top_up: bool,
113 valid_until: i64,
114 lamports: Option<u64>,
115}
116
117fn create_session_token_internal<'info>(
118 session_token: &mut Account<'info, SessionToken>,
119 params: CreateSessionTokenParams,
120 system_program: AccountInfo<'info>,
121 payer: AccountInfo<'info>,
122 session_signer_account: AccountInfo<'info>,
123) -> Result<()> {
124 let authority = params.authority;
125 let target_program = params.target_program;
126 let session_signer = params.session_signer;
127 let top_up = params.top_up;
128 let valid_until = params.valid_until;
129 let lamports = params.lamports;
130 require!(
132 valid_until <= Clock::get()?.unix_timestamp + (60 * 60 * 24 * 7),
133 SessionError::ValidityTooLong
134 );
135
136 session_token.set_inner(SessionToken {
137 authority,
138 target_program,
139 session_signer,
140 valid_until,
141 });
142
143 if top_up {
145 let amount = lamports.unwrap_or(LAMPORTS_PER_SOL / 100);
146 invoke(
147 &system_instruction::transfer(payer.key, session_signer_account.key, amount),
148 &[payer, session_signer_account, system_program],
149 )?;
150 }
151
152 Ok(())
153}
154
155pub fn create_session_token_handler(
157 ctx: Context<CreateSessionToken>,
158 top_up: bool,
159 valid_until: i64,
160 lamports: Option<u64>,
161) -> Result<()> {
162 create_session_token_internal(
163 &mut ctx.accounts.session_token,
164 CreateSessionTokenParams {
165 authority: ctx.accounts.authority.key(),
166 target_program: ctx.accounts.target_program.key(),
167 session_signer: ctx.accounts.session_signer.key(),
168 top_up,
169 valid_until,
170 lamports,
171 },
172 ctx.accounts.system_program.to_account_info(),
173 ctx.accounts.authority.to_account_info(),
174 ctx.accounts.session_signer.to_account_info(),
175 )
176}
177
178#[derive(Accounts)]
180pub struct CreateSessionTokenWithPayer<'info> {
181 #[account(
182 init,
183 seeds = [
184 SessionToken::SEED_PREFIX.as_bytes(),
185 target_program.key().as_ref(),
186 session_signer.key().as_ref(),
187 authority.key().as_ref()
188 ],
189 bump,
190 payer = payer,
191 space = SessionToken::LEN
192 )]
193 pub session_token: Account<'info, SessionToken>,
194
195 #[account(mut)]
196 pub session_signer: Signer<'info>,
197 #[account(mut)]
198 pub payer: Signer<'info>,
199 pub authority: Signer<'info>,
200
201 #[account(executable)]
203 pub target_program: UncheckedAccount<'info>,
204
205 pub system_program: Program<'info, System>,
206}
207
208pub fn create_session_token_with_payer_handler(
210 ctx: Context<CreateSessionTokenWithPayer>,
211 top_up: bool,
212 valid_until: i64,
213 lamports: Option<u64>,
214) -> Result<()> {
215 create_session_token_internal(
216 &mut ctx.accounts.session_token,
217 CreateSessionTokenParams {
218 authority: ctx.accounts.authority.key(),
219 target_program: ctx.accounts.target_program.key(),
220 session_signer: ctx.accounts.session_signer.key(),
221 top_up,
222 valid_until,
223 lamports,
224 },
225 ctx.accounts.system_program.to_account_info(),
226 ctx.accounts.payer.to_account_info(),
227 ctx.accounts.session_signer.to_account_info(),
228 )
229}
230
231#[derive(Accounts)]
240pub struct RevokeSessionToken<'info> {
241 #[account(
242 mut,
243 seeds = [
244 SessionToken::SEED_PREFIX.as_bytes(),
245 session_token.target_program.key().as_ref(),
246 session_token.session_signer.key().as_ref(),
247 session_token.authority.key().as_ref()
248 ],
249 bump,
250 has_one = authority,
251 close = authority,
252 )]
253 pub session_token: Account<'info, SessionToken>,
254
255 #[account(mut)]
256 pub authority: SystemAccount<'info>,
258
259 pub system_program: Program<'info, System>,
260}
261
262pub fn revoke_session_token_handler(_: Context<RevokeSessionToken>) -> Result<()> {
264 Ok(())
265}
266
267#[derive(Accounts)]
271pub struct CreateSessionTokenV2<'info> {
272 #[account(
273 init,
274 seeds = [
275 SessionTokenV2::SEED_PREFIX.as_bytes(),
276 target_program.key().as_ref(),
277 session_signer.key().as_ref(),
278 authority.key().as_ref()
279 ],
280 bump,
281 payer = fee_payer,
282 space = SessionTokenV2::LEN
283 )]
284 pub session_token: Account<'info, SessionTokenV2>,
285
286 #[account(mut)]
287 pub session_signer: Signer<'info>,
288 #[account(mut)]
289 pub fee_payer: Signer<'info>,
290 pub authority: Signer<'info>,
291
292 #[account(executable)]
294 pub target_program: UncheckedAccount<'info>,
295
296 pub system_program: Program<'info, System>,
297}
298
299struct CreateSessionTokenV2Params {
300 authority: Pubkey,
301 target_program: Pubkey,
302 session_signer: Pubkey,
303 fee_payer: Pubkey,
304 top_up: bool,
305 valid_until: i64,
306 lamports: Option<u64>,
307}
308
309fn create_session_token_v2_internal<'info>(
310 session_token: &mut Account<'info, SessionTokenV2>,
311 params: CreateSessionTokenV2Params,
312 system_program: AccountInfo<'info>,
313 payer: AccountInfo<'info>,
314 session_signer_account: AccountInfo<'info>,
315) -> Result<()> {
316 let authority = params.authority;
317 let target_program = params.target_program;
318 let session_signer = params.session_signer;
319 let fee_payer = params.fee_payer;
320 let top_up = params.top_up;
321 let valid_until = params.valid_until;
322 let lamports = params.lamports;
323 require!(
325 valid_until <= Clock::get()?.unix_timestamp + (60 * 60 * 24 * 7),
326 SessionError::ValidityTooLong
327 );
328
329 session_token.set_inner(SessionTokenV2 {
330 authority,
331 target_program,
332 session_signer,
333 fee_payer,
334 valid_until,
335 });
336
337 if top_up {
339 let amount = lamports.unwrap_or(LAMPORTS_PER_SOL / 100);
340 invoke(
341 &system_instruction::transfer(payer.key, session_signer_account.key, amount),
342 &[payer, session_signer_account, system_program],
343 )?;
344 }
345
346 Ok(())
347}
348
349pub fn create_session_token_handler_v2(
351 ctx: Context<CreateSessionTokenV2>,
352 top_up: bool,
353 valid_until: i64,
354 lamports: Option<u64>,
355) -> Result<()> {
356 create_session_token_v2_internal(
357 &mut ctx.accounts.session_token,
358 CreateSessionTokenV2Params {
359 authority: ctx.accounts.authority.key(),
360 target_program: ctx.accounts.target_program.key(),
361 session_signer: ctx.accounts.session_signer.key(),
362 fee_payer: ctx.accounts.fee_payer.key(),
363 top_up,
364 valid_until,
365 lamports,
366 },
367 ctx.accounts.system_program.to_account_info(),
368 ctx.accounts.fee_payer.to_account_info(),
369 ctx.accounts.session_signer.to_account_info(),
370 )
371}
372
373#[derive(Accounts)]
377pub struct RevokeSessionTokenV2<'info> {
378 #[account(
379 mut,
380 seeds = [
381 SessionTokenV2::SEED_PREFIX.as_bytes(),
382 session_token.target_program.key().as_ref(),
383 session_token.session_signer.key().as_ref(),
384 session_token.authority.key().as_ref()
385 ],
386 bump,
387 has_one = fee_payer,
388 has_one = authority,
389 close = fee_payer,
390 )]
391 pub session_token: Account<'info, SessionTokenV2>,
392
393 #[account(mut)]
394 pub fee_payer: SystemAccount<'info>,
396
397 pub authority: SystemAccount<'info>,
399
400 pub system_program: Program<'info, System>,
401}
402
403pub fn revoke_session_token_handler_v2(ctx: Context<RevokeSessionTokenV2>) -> Result<()> {
405 if !ctx.accounts.session_token.is_expired()? {
407 require!(
408 ctx.accounts.authority.is_signer,
409 SessionError::InvalidAuthority
410 );
411 }
412 Ok(())
413}
414
415pub struct ValidityChecker<'info> {
416 pub session_token: Account<'info, SessionToken>,
417 pub session_signer: Signer<'info>,
418 pub authority: Pubkey,
419 pub target_program: Pubkey,
420}
421
422pub struct ValidityCheckerV2<'info> {
423 pub session_token: Account<'info, SessionTokenV2>,
424 pub session_signer: Signer<'info>,
425 pub authority: Pubkey,
426 pub target_program: Pubkey,
427}
428
429#[account]
431#[derive(Copy)]
432pub struct SessionToken {
433 pub authority: Pubkey,
434 pub target_program: Pubkey,
435 pub session_signer: Pubkey,
436 pub valid_until: i64,
437}
438
439#[account]
440#[derive(Copy)]
441pub struct SessionTokenV2 {
442 pub authority: Pubkey,
443 pub target_program: Pubkey,
444 pub session_signer: Pubkey,
445 pub fee_payer: Pubkey,
447 pub valid_until: i64,
448}
449
450impl SessionToken {
451 pub const LEN: usize = 8 + std::mem::size_of::<Self>();
452 pub const SEED_PREFIX: &'static str = "session_token";
453
454 fn is_expired(&self) -> Result<bool> {
455 let now = Clock::get()?.unix_timestamp;
456 Ok(now < self.valid_until)
457 }
458
459 pub fn validate(&self, ctx: ValidityChecker) -> Result<bool> {
461 let target_program = ctx.target_program;
462 let session_signer = ctx.session_signer.key();
463 let authority = ctx.authority.key();
464
465 let seeds = &[
467 SessionToken::SEED_PREFIX.as_bytes(),
468 target_program.as_ref(),
469 session_signer.as_ref(),
470 authority.as_ref(),
471 ];
472
473 let (pda, _) = Pubkey::find_program_address(seeds, &crate::id());
474
475 require_eq!(pda, ctx.session_token.key(), SessionError::InvalidToken);
476
477 self.is_expired()
479 }
480}
481
482impl SessionTokenV2 {
483 pub const LEN: usize = 8 + std::mem::size_of::<Self>();
484 pub const SEED_PREFIX: &'static str = "session_token_v2";
485}
486
487impl SessionTokenV2 {
488 pub fn is_expired(&self) -> Result<bool> {
490 let now = Clock::get()?.unix_timestamp;
491 Ok(now >= self.valid_until)
492 }
493
494 pub fn validate(&self, ctx: ValidityCheckerV2) -> Result<bool> {
496 let target_program = ctx.target_program;
497 let session_signer = ctx.session_signer.key();
498 let authority = ctx.authority.key();
499
500 let seeds = &[
502 SessionTokenV2::SEED_PREFIX.as_bytes(),
503 target_program.as_ref(),
504 session_signer.as_ref(),
505 authority.as_ref(),
506 ];
507
508 let (pda, _) = Pubkey::find_program_address(seeds, &crate::id());
509
510 require_eq!(pda, ctx.session_token.key(), SessionError::InvalidToken);
511
512 let expired = self.is_expired()?;
514 Ok(!expired)
515 }
516}
517
518pub trait Session<'info> {
519 fn session_token(&self) -> Option<Account<'info, SessionToken>>;
520 fn session_signer(&self) -> Signer<'info>;
521 fn session_authority(&self) -> Pubkey;
522 fn target_program(&self) -> Pubkey;
523
524 fn is_valid(&self) -> Result<bool> {
525 let session_token = self.session_token().ok_or(SessionError::NoToken)?;
526 let validity_ctx = ValidityChecker {
527 session_token: session_token.clone(),
528 session_signer: self.session_signer(),
529 authority: self.session_authority(),
530 target_program: self.target_program(),
531 };
532 session_token.validate(validity_ctx)
534 }
535}
536
537pub trait SessionV2<'info> {
538 fn session_token(&self) -> Option<Account<'info, SessionTokenV2>>;
539 fn session_signer(&self) -> Signer<'info>;
540 fn session_authority(&self) -> Pubkey;
541 fn target_program(&self) -> Pubkey;
542
543 fn is_valid(&self) -> Result<bool> {
544 let session_token = self.session_token().ok_or(SessionError::NoToken)?;
545 let validity_ctx = ValidityCheckerV2 {
546 session_token: session_token.clone(),
547 session_signer: self.session_signer(),
548 authority: self.session_authority(),
549 target_program: self.target_program(),
550 };
551 session_token.validate(validity_ctx)
553 }
554}
555
556#[error_code]
557pub enum SessionError {
558 #[msg("Requested validity is too long")]
559 ValidityTooLong,
560 #[msg("Invalid session token")]
561 InvalidToken,
562 #[msg("No session token provided")]
563 NoToken,
564 #[msg("Invalid authority")]
565 InvalidAuthority,
566}