solana_gpt_oracle/
lib.rs

1use anchor_lang::prelude::ProgramError;
2use anchor_lang::prelude::*;
3use anchor_lang::solana_program::instruction::Instruction;
4use anchor_lang::solana_program::program::invoke_signed;
5use ephemeral_rollups_sdk::anchor::{delegate, ephemeral};
6use ephemeral_rollups_sdk::cpi::DelegateConfig;
7
8declare_id!("LLMrieZMpbJFwN52WgmBNMxYojrpRVYXdC1RCweEbab");
9
10const ORACLE_IDENTITY: Pubkey = pubkey!("A1ooMmN1fz6LbEFrjh6GukFS2ZeRYFzdyFjeafyyS7Ca");
11
12#[ephemeral]
13#[program]
14pub mod solana_gpt_oracle {
15    use super::*;
16
17    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
18        Ok(())
19    }
20
21    pub fn create_llm_context(ctx: Context<CreateLlmContext>, text: String) -> Result<()> {
22        let context_account = &mut ctx.accounts.context_account;
23        context_account.text = text;
24        ctx.accounts.counter.count += 1;
25        Ok(())
26    }
27
28    pub fn interact_with_llm(
29        ctx: Context<InteractWithLlm>,
30        text: String,
31        callback_program_id: Pubkey,
32        callback_discriminator: [u8; 8],
33        account_metas: Option<Vec<AccountMeta>>,
34    ) -> Result<()> {
35        let interaction = &mut ctx.accounts.interaction;
36        let current_len = interaction.to_account_info().data_len();
37        let space = Interaction::space(&text, account_metas.as_ref().map_or(0, |m| m.len()));
38        let rent = Rent::get()?;
39
40        let mut additional_rent = rent.minimum_balance(space);
41
42        let interaction_info = interaction.to_account_info();
43        let payer_info = ctx.accounts.payer.to_account_info();
44        let system_program_info = ctx.accounts.system_program.to_account_info();
45
46        if interaction_info.owner.eq(&anchor_lang::system_program::ID) {
47            let create_instruction =
48                anchor_lang::solana_program::system_instruction::create_account(
49                    &ctx.accounts.payer.key(),
50                    &interaction.key(),
51                    additional_rent,
52                    space as u64,
53                    &crate::ID,
54                );
55
56            let payer = ctx.accounts.payer.key();
57            let context_account = ctx.accounts.context_account.key();
58            let signer_seeds: &[&[&[u8]]] = &[&[
59                Interaction::seed(),
60                payer.as_ref(),
61                context_account.as_ref(),
62                &[ctx.bumps.interaction],
63            ]];
64
65            anchor_lang::solana_program::program::invoke_signed(
66                &create_instruction,
67                &[
68                    payer_info.clone(),
69                    interaction_info.clone(),
70                    system_program_info.clone(),
71                ],
72                signer_seeds,
73            )?;
74        } else {
75            additional_rent = additional_rent.saturating_sub(rent.minimum_balance(current_len));
76            interaction_info.realloc(space, false)?;
77            if additional_rent > 0 {
78                let cpi_context = CpiContext::new(
79                    system_program_info,
80                    anchor_lang::system_program::Transfer {
81                        from: payer_info.clone(),
82                        to: interaction_info.clone(),
83                    },
84                );
85                anchor_lang::system_program::transfer(cpi_context, additional_rent)?;
86            }
87        }
88
89        let mut interaction_data = interaction.try_borrow_mut_data()?;
90        let mut interaction =
91            Interaction::try_deserialize_unchecked(&mut interaction_data.as_ref())
92                .unwrap_or_default();
93
94        interaction.context = ctx.accounts.context_account.key();
95        interaction.user = ctx.accounts.payer.key();
96        interaction.text = text;
97        interaction.callback_program_id = callback_program_id;
98        interaction.callback_discriminator = callback_discriminator;
99        interaction.callback_account_metas = account_metas.unwrap_or_default();
100        interaction.is_processed = false;
101
102        interaction.try_serialize(&mut interaction_data.as_mut())?;
103        Ok(())
104    }
105
106    pub fn callback_from_llm<'info>(
107        ctx: Context<'_, '_, '_, 'info, CallbackFromLlm<'info>>,
108        response: String,
109    ) -> Result<()> {
110        let response_data = [
111            ctx.accounts.interaction.callback_discriminator.to_vec(),
112            response.try_to_vec()?,
113        ]
114        .concat();
115
116        // Prepare accounts metas
117        let mut accounts_metas: Vec<anchor_lang::solana_program::instruction::AccountMeta> =
118            vec![anchor_lang::solana_program::instruction::AccountMeta {
119                pubkey: ctx.accounts.identity.key(),
120                is_signer: true,
121                is_writable: false,
122            }];
123        accounts_metas.extend(
124            ctx.accounts
125                .interaction
126                .callback_account_metas
127                .iter()
128                .map(
129                    |meta| anchor_lang::solana_program::instruction::AccountMeta {
130                        pubkey: meta.pubkey,
131                        is_signer: meta.is_signer,
132                        is_writable: meta.is_writable,
133                    },
134                ),
135        );
136
137        // Verify payer is not in remaining accounts
138        if ctx
139            .remaining_accounts
140            .iter()
141            .any(|acc| acc.key().eq(&ctx.accounts.payer.key()))
142        {
143            return Err(ProgramError::InvalidAccountData.into());
144        }
145
146        // Set processed flag
147        ctx.accounts.interaction.is_processed = true;
148
149        // CPI to the callback program
150        let instruction = Instruction {
151            program_id: ctx.accounts.program.key(),
152            accounts: accounts_metas,
153            data: response_data.to_vec(),
154        };
155        let mut remaining_accounts: Vec<AccountInfo<'info>> = ctx.remaining_accounts.to_vec();
156        remaining_accounts.push(ctx.accounts.identity.to_account_info());
157        remaining_accounts.push(ctx.accounts.program.to_account_info());
158        let identity_bump = ctx.bumps.identity;
159        invoke_signed(
160            &instruction,
161            &remaining_accounts,
162            &[&[b"identity", &[identity_bump]]],
163        )?;
164        Ok(())
165    }
166
167    pub fn callback_from_oracle(ctx: Context<CallbackFromOracle>, response: String) -> Result<()> {
168        if !ctx.accounts.identity.to_account_info().is_signer {
169            return Err(ProgramError::InvalidAccountData.into());
170        }
171        msg!("Callback response: {:?}", response);
172        Ok(())
173    }
174
175    pub fn delegate_interaction(ctx: Context<DelegateInteraction>) -> Result<()> {
176        ctx.accounts.delegate_interaction(
177            &ctx.accounts.payer,
178            &[
179                Interaction::seed(),
180                &ctx.accounts.payer.key().to_bytes(),
181                &ctx.accounts.context_account.key().to_bytes(),
182            ],
183            DelegateConfig::default(),
184        )?;
185        Ok(())
186    }
187}
188
189/// Contexts
190
191#[derive(Accounts)]
192pub struct Initialize<'info> {
193    #[account(mut)]
194    pub payer: Signer<'info>,
195    #[account(
196        init,
197        payer = payer,
198        space = 8,
199        seeds = [b"identity"],
200        bump
201    )]
202    pub identity: Account<'info, Identity>,
203    #[account(
204        init,
205        payer = payer,
206        space = 8 + 32,
207        seeds = [b"counter"],
208        bump
209    )]
210    pub counter: Account<'info, Counter>,
211    pub system_program: Program<'info, System>,
212}
213
214#[derive(Accounts)]
215#[instruction(text: String)]
216pub struct CreateLlmContext<'info> {
217    #[account(mut)]
218    pub payer: Signer<'info>,
219    #[account(mut,
220        seeds = [b"counter"],
221        bump
222    )]
223    pub counter: Account<'info, Counter>,
224    #[account(
225        init,
226        payer = payer,
227        space = 8 + text.as_bytes().len() + 8,
228        seeds = [ContextAccount::seed(), &counter.count.to_le_bytes()],
229        bump
230    )]
231    pub context_account: Account<'info, ContextAccount>,
232    pub system_program: Program<'info, System>,
233}
234
235#[derive(Accounts)]
236#[instruction(text: String, callback_program_id: Pubkey, callback_discriminator: [u8; 8], account_metas: Option<Vec<AccountMeta>>)]
237pub struct InteractWithLlm<'info> {
238    #[account(mut)]
239    pub payer: Signer<'info>,
240    /// CHECK: the correct interaction account
241    #[account(
242        mut,
243        seeds = [Interaction::seed(), payer.key().as_ref(), context_account.key().as_ref()],
244        bump
245    )]
246    pub interaction: AccountInfo<'info>,
247    /// CHECK: we accept any context
248    pub context_account: Account<'info, ContextAccount>,
249    pub system_program: Program<'info, System>,
250}
251
252#[derive(Accounts)]
253pub struct CallbackFromLlm<'info> {
254    #[account(mut, address = ORACLE_IDENTITY)]
255    pub payer: Signer<'info>,
256    #[account(seeds = [b"identity"], bump)]
257    pub identity: Account<'info, Identity>,
258    /// CHECK: we accept any context
259    #[account(mut)]
260    pub interaction: Account<'info, Interaction>,
261    /// CHECK: the callback program
262    pub program: AccountInfo<'info>,
263}
264
265#[derive(Accounts)]
266pub struct CallbackFromOracle<'info> {
267    #[account(seeds = [b"identity"], bump)]
268    pub identity: Account<'info, Identity>,
269}
270
271#[delegate]
272#[derive(Accounts)]
273pub struct DelegateInteraction<'info> {
274    #[account(mut)]
275    pub payer: Signer<'info>,
276    /// CHECK: the correct interaction account
277    #[account(
278        mut, del,
279        seeds = [Interaction::seed(), payer.key().as_ref(), context_account.key().as_ref()],
280        bump
281    )]
282    pub interaction: AccountInfo<'info>,
283    /// CHECK: we accept any context
284    pub context_account: Account<'info, ContextAccount>,
285}
286
287/// Accounts
288
289#[account]
290pub struct ContextAccount {
291    pub text: String,
292}
293
294impl ContextAccount {
295    pub fn seed() -> &'static [u8] {
296        b"test-context"
297    }
298}
299
300#[account]
301#[derive(Default, Debug)]
302pub struct Interaction {
303    pub context: Pubkey,
304    pub user: Pubkey,
305    pub text: String,
306    pub callback_program_id: Pubkey,
307    pub callback_discriminator: [u8; 8],
308    pub callback_account_metas: Vec<AccountMeta>,
309    pub is_processed: bool,
310}
311
312impl Interaction {
313    pub fn seed() -> &'static [u8] {
314        b"interaction"
315    }
316
317    pub fn space(text: &String, account_metas_len: usize) -> usize {
318        121 + text.as_bytes().len() + account_metas_len * AccountMeta::size()
319    }
320}
321
322#[derive(InitSpace, AnchorSerialize, AnchorDeserialize, Clone, Debug)]
323pub struct AccountMeta {
324    pub pubkey: Pubkey,
325    pub is_signer: bool,
326    pub is_writable: bool,
327}
328
329impl AccountMeta {
330    pub fn size() -> usize {
331        8 + AccountMeta::INIT_SPACE
332    }
333}
334
335#[account]
336pub struct Counter {
337    pub count: u32,
338}
339
340#[account]
341pub struct Identity {}