1use anchor_lang::{prelude::*, solana_program::native_token::LAMPORTS_PER_SOL, system_program};
2
3#[cfg(feature = "no-entrypoint")]
4pub use session_keys_macros::*;
5
6declare_id!("KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5");
7
8#[cfg(not(feature = "no-entrypoint"))]
9solana_security_txt::security_txt! {
10 name: "session_keys",
11 project_url: "https://magicblock.gg",
12 contacts: "email:dev@magicblock.gg,twitter:@magicblock",
13 policy: "",
14 preferred_languages: "en",
15 source_code: "https://github.com/magicblock-labs"
16}
17
18#[program]
19pub mod gpl_session {
20 use super::*;
21
22 pub fn create_session(
24 ctx: Context<CreateSessionToken>,
25 top_up: Option<bool>,
26 valid_until: Option<i64>,
27 lamports: Option<u64>,
28 ) -> Result<()> {
29 let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
30 create_session_token_handler(ctx, top_up, valid_until, lamports)
31 }
32
33 pub fn create_session_with_payer(
34 ctx: Context<CreateSessionTokenWithPayer>,
35 top_up: Option<bool>,
36 valid_until: Option<i64>,
37 lamports: Option<u64>,
38 ) -> Result<()> {
39 let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
40 create_session_token_with_payer_handler(ctx, top_up, valid_until, lamports)
41 }
42 pub fn revoke_session(ctx: Context<RevokeSessionToken>) -> Result<()> {
44 revoke_session_token_handler(ctx)
45 }
46}
47
48fn process_session_params(top_up: Option<bool>, valid_until: Option<i64>) -> Result<(bool, i64)> {
49 let top_up = top_up.unwrap_or(false);
50 let valid_until = valid_until.unwrap_or(Clock::get()?.unix_timestamp + 60 * 60 * 1);
51 Ok((top_up, valid_until))
52}
53
54#[derive(Accounts)]
56pub struct CreateSessionToken<'info> {
57 #[account(
58 init,
59 seeds = [
60 SessionToken::SEED_PREFIX.as_bytes(),
61 target_program.key().as_ref(),
62 session_signer.key().as_ref(),
63 authority.key().as_ref()
64 ],
65 bump,
66 payer = authority,
67 space = SessionToken::LEN
68 )]
69 pub session_token: Account<'info, SessionToken>,
70
71 #[account(mut)]
72 pub session_signer: Signer<'info>,
73 #[account(mut)]
74 pub authority: Signer<'info>,
75
76 #[account(executable)]
78 pub target_program: AccountInfo<'info>,
79
80 pub system_program: Program<'info, System>,
81}
82
83fn create_session_token_internal<'info>(
84 session_token: &mut Account<'info, SessionToken>,
85 authority: Pubkey,
86 target_program: Pubkey,
87 session_signer: Pubkey,
88 system_program: AccountInfo<'info>,
89 payer: AccountInfo<'info>,
90 session_signer_account: AccountInfo<'info>,
91 top_up: bool,
92 valid_until: i64,
93 lamports: Option<u64>,
94) -> Result<()> {
95 require!(
97 valid_until <= Clock::get()?.unix_timestamp + (60 * 60 * 24 * 7),
98 SessionError::ValidityTooLong
99 );
100
101 session_token.set_inner(SessionToken {
102 authority,
103 target_program,
104 session_signer,
105 valid_until,
106 });
107
108 if top_up {
110 system_program::transfer(
111 CpiContext::new(
112 system_program,
113 system_program::Transfer {
114 from: payer,
115 to: session_signer_account,
116 },
117 ),
118 lamports.unwrap_or(LAMPORTS_PER_SOL / 100),
119 )?;
120 }
121
122 Ok(())
123}
124
125pub fn create_session_token_handler(
127 ctx: Context<CreateSessionToken>,
128 top_up: bool,
129 valid_until: i64,
130 lamports: Option<u64>,
131) -> Result<()> {
132 create_session_token_internal(
133 &mut ctx.accounts.session_token,
134 ctx.accounts.authority.key(),
135 ctx.accounts.target_program.key(),
136 ctx.accounts.session_signer.key(),
137 ctx.accounts.system_program.to_account_info(),
138 ctx.accounts.authority.to_account_info(),
139 ctx.accounts.session_signer.to_account_info(),
140 top_up,
141 valid_until,
142 lamports,
143 )
144}
145
146#[derive(Accounts)]
148pub struct CreateSessionTokenWithPayer<'info> {
149 #[account(
150 init,
151 seeds = [
152 SessionToken::SEED_PREFIX.as_bytes(),
153 target_program.key().as_ref(),
154 session_signer.key().as_ref(),
155 authority.key().as_ref()
156 ],
157 bump,
158 payer = payer,
159 space = SessionToken::LEN
160 )]
161 pub session_token: Account<'info, SessionToken>,
162
163 #[account(mut)]
164 pub session_signer: Signer<'info>,
165 #[account(mut)]
166 pub payer: Signer<'info>,
167 pub authority: Signer<'info>,
168
169 #[account(executable)]
171 pub target_program: AccountInfo<'info>,
172
173 pub system_program: Program<'info, System>,
174}
175
176pub fn create_session_token_with_payer_handler(
178 ctx: Context<CreateSessionTokenWithPayer>,
179 top_up: bool,
180 valid_until: i64,
181 lamports: Option<u64>,
182) -> Result<()> {
183 create_session_token_internal(
184 &mut ctx.accounts.session_token,
185 ctx.accounts.authority.key(),
186 ctx.accounts.target_program.key(),
187 ctx.accounts.session_signer.key(),
188 ctx.accounts.system_program.to_account_info(),
189 ctx.accounts.payer.to_account_info(),
190 ctx.accounts.session_signer.to_account_info(),
191 top_up,
192 valid_until,
193 lamports,
194 )
195}
196
197#[derive(Accounts)]
206pub struct RevokeSessionToken<'info> {
207 #[account(
208 mut,
209 seeds = [
210 SessionToken::SEED_PREFIX.as_bytes(),
211 session_token.target_program.key().as_ref(),
212 session_token.session_signer.key().as_ref(),
213 session_token.authority.key().as_ref()
214 ],
215 bump,
216 has_one = authority,
217 close = authority,
218 )]
219 pub session_token: Account<'info, SessionToken>,
220
221 #[account(mut)]
222 pub authority: SystemAccount<'info>,
224
225 pub system_program: Program<'info, System>,
226}
227
228pub fn revoke_session_token_handler(_: Context<RevokeSessionToken>) -> Result<()> {
230 Ok(())
231}
232
233pub struct ValidityChecker<'info> {
234 pub session_token: Account<'info, SessionToken>,
235 pub session_signer: Signer<'info>,
236 pub authority: Pubkey,
237 pub target_program: Pubkey,
238}
239
240#[account]
242#[derive(Copy)]
243pub struct SessionToken {
244 pub authority: Pubkey,
245 pub target_program: Pubkey,
246 pub session_signer: Pubkey,
247 pub valid_until: i64,
248}
249
250impl SessionToken {
251 pub const LEN: usize = 8 + std::mem::size_of::<Self>();
252 pub const SEED_PREFIX: &'static str = "session_token";
253
254 fn is_expired(&self) -> Result<bool> {
255 let now = Clock::get()?.unix_timestamp;
256 Ok(now < self.valid_until)
257 }
258
259 pub fn validate(&self, ctx: ValidityChecker) -> Result<bool> {
261 let target_program = ctx.target_program;
262 let session_signer = ctx.session_signer.key();
263 let authority = ctx.authority.key();
264
265 let seeds = &[
267 SessionToken::SEED_PREFIX.as_bytes(),
268 target_program.as_ref(),
269 session_signer.as_ref(),
270 authority.as_ref(),
271 ];
272
273 let (pda, _) = Pubkey::find_program_address(seeds, &crate::id());
274
275 require_eq!(pda, ctx.session_token.key(), SessionError::InvalidToken);
276
277 self.is_expired()
279 }
280}
281
282pub trait Session<'info> {
283 fn session_token(&self) -> Option<Account<'info, SessionToken>>;
284 fn session_signer(&self) -> Signer<'info>;
285 fn session_authority(&self) -> Pubkey;
286 fn target_program(&self) -> Pubkey;
287
288 fn is_valid(&self) -> Result<bool> {
289 let session_token = self.session_token().ok_or(SessionError::NoToken)?;
290 let validity_ctx = ValidityChecker {
291 session_token: session_token.clone(),
292 session_signer: self.session_signer(),
293 authority: self.session_authority(),
294 target_program: self.target_program(),
295 };
296 session_token.validate(validity_ctx)
298 }
299}
300
301#[error_code]
302pub enum SessionError {
303 #[msg("Requested validity is too long")]
304 ValidityTooLong,
305 #[msg("Invalid session token")]
306 InvalidToken,
307 #[msg("No session token provided")]
308 NoToken,
309}