1use anchor_lang::prelude::*;
2use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
3use anchor_lang::system_program;
4
5#[cfg(feature = "no-entrypoint")]
6pub use gpl_session_macros::*;
7
8declare_id!("3ao63wcSRNa76bncC2M3KupNtXBFiDyNbgK52VG7dLaE");
9
10#[cfg(not(feature = "no-entrypoint"))]
11solana_security_txt::security_txt! {
12 name: "gpl_session",
13 project_url: "https://gum.fun",
14 contacts: "email:hello@gum.fun,twitter:@gumisfunn",
15 policy: "",
16 preferred_languages: "en",
17 source_code: "https://github.com/gumhq/gpl"
18}
19
20#[program]
21pub mod gpl_session {
22 use super::*;
23
24 pub fn create_session(
26 ctx: Context<CreateSessionToken>,
27 top_up: Option<bool>,
28 valid_until: Option<i64>,
29 ) -> Result<()> {
30 let top_up = top_up.unwrap_or(false);
32 let valid_until = valid_until.unwrap_or(Clock::get()?.unix_timestamp + 60 * 60 * 1);
34 create_session_token_handler(ctx, top_up, valid_until)
35 }
36
37 pub fn revoke_session(ctx: Context<RevokeSessionToken>) -> Result<()> {
39 revoke_session_token_handler(ctx)
40 }
41}
42
43#[derive(Accounts)]
45pub struct CreateSessionToken<'info> {
46 #[account(
47 init,
48 seeds = [
49 SessionToken::SEED_PREFIX.as_bytes(),
50 target_program.key().as_ref(),
51 session_signer.key().as_ref(),
52 authority.key().as_ref()
53 ],
54 bump,
55 payer = authority,
56 space = SessionToken::LEN
57 )]
58 pub session_token: Account<'info, SessionToken>,
59
60 #[account(mut)]
61 pub session_signer: Signer<'info>,
62 #[account(mut)]
63 pub authority: Signer<'info>,
64
65 #[account(executable)]
67 pub target_program: AccountInfo<'info>,
68
69 pub system_program: Program<'info, System>,
70}
71
72pub fn create_session_token_handler(
74 ctx: Context<CreateSessionToken>,
75 top_up: bool,
76 valid_until: i64,
77) -> Result<()> {
78 require!(
80 valid_until <= Clock::get()?.unix_timestamp + 60 * 60 * 24,
81 SessionError::ValidityTooLong
82 );
83
84 let session_token = &mut ctx.accounts.session_token;
85 session_token.set_inner(SessionToken {
86 authority: ctx.accounts.authority.key(),
87 target_program: ctx.accounts.target_program.key(),
88 session_signer: ctx.accounts.session_signer.key(),
89 valid_until,
90 });
91
92 if top_up {
95 system_program::transfer(
96 CpiContext::new(
97 ctx.accounts.system_program.to_account_info(),
98 system_program::Transfer {
99 from: ctx.accounts.authority.to_account_info(),
100 to: ctx.accounts.session_signer.to_account_info(),
101 },
102 ),
103 LAMPORTS_PER_SOL.checked_div(100).unwrap(),
104 )?;
105 }
106
107 Ok(())
108}
109
110#[derive(Accounts)]
119pub struct RevokeSessionToken<'info> {
120 #[account(
121 mut,
122 seeds = [
123 SessionToken::SEED_PREFIX.as_bytes(),
124 session_token.target_program.key().as_ref(),
125 session_token.session_signer.key().as_ref(),
126 session_token.authority.key().as_ref()
127 ],
128 bump,
129 has_one = authority,
130 close = authority,
131 )]
132 pub session_token: Account<'info, SessionToken>,
133
134 #[account(mut)]
135 pub authority: SystemAccount<'info>,
137
138 pub system_program: Program<'info, System>,
139}
140
141pub fn revoke_session_token_handler(_: Context<RevokeSessionToken>) -> Result<()> {
143 Ok(())
144}
145
146pub struct ValidityChecker<'info> {
147 pub session_token: Account<'info, SessionToken>,
148 pub session_signer: Signer<'info>,
149 pub authority: Pubkey,
150 pub target_program: Pubkey,
151}
152
153#[account]
155#[derive(Copy)]
156pub struct SessionToken {
157 pub authority: Pubkey,
158 pub target_program: Pubkey,
159 pub session_signer: Pubkey,
160 pub valid_until: i64,
161}
162
163impl SessionToken {
164 pub const LEN: usize = 8 + std::mem::size_of::<Self>();
165 pub const SEED_PREFIX: &'static str = "session_token";
166
167 fn is_expired(&self) -> Result<bool> {
168 let now = Clock::get()?.unix_timestamp;
169 Ok(now < self.valid_until)
170 }
171
172 pub fn validate(&self, ctx: ValidityChecker) -> Result<bool> {
174 let target_program = ctx.target_program;
175 let session_signer = ctx.session_signer.key();
176 let authority = ctx.authority.key();
177
178 let seeds = &[
180 SessionToken::SEED_PREFIX.as_bytes(),
181 target_program.as_ref(),
182 session_signer.as_ref(),
183 authority.as_ref(),
184 ];
185
186 let (pda, _) = Pubkey::find_program_address(seeds, &crate::id());
187
188 require_eq!(pda, ctx.session_token.key(), SessionError::InvalidToken);
189
190 self.is_expired()
192 }
193}
194
195pub trait Session<'info> {
196 fn session_token(&self) -> Option<Account<'info, SessionToken>>;
197 fn session_signer(&self) -> Signer<'info>;
198 fn session_authority(&self) -> Pubkey;
199 fn target_program(&self) -> Pubkey;
200
201 fn is_valid(&self) -> Result<bool> {
202 let session_token = self.session_token().ok_or(SessionError::NoToken)?;
203 let validity_ctx = ValidityChecker {
204 session_token: session_token.clone(),
205 session_signer: self.session_signer(),
206 authority: self.session_authority(),
207 target_program: self.target_program(),
208 };
209 session_token.validate(validity_ctx)
211 }
212}
213
214#[error_code]
215pub enum SessionError {
216 #[msg("Requested validity is too long")]
217 ValidityTooLong,
218 #[msg("Invalid session token")]
219 InvalidToken,
220 #[msg("No session token provided")]
221 NoToken,
222}