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