ephemeral_rollups_sdk/
ephem.rs

1use crate::ephem::utils::accounts_to_indices;
2use crate::solana_compat::solana::{
3    invoke, AccountInfo, AccountMeta, Instruction, ProgramResult, Pubkey,
4};
5use magicblock_magic_program_api::args::{
6    ActionArgs, BaseActionArgs, CommitAndUndelegateArgs, CommitTypeArgs, MagicBaseIntentArgs,
7    ShortAccountMeta, UndelegateTypeArgs,
8};
9use magicblock_magic_program_api::instruction::MagicBlockInstruction;
10use std::collections::HashMap;
11
12const EXPECTED_KEY_MSG: &str = "Key expected to exist!";
13
14/// Instruction builder for magicprogram
15pub struct MagicInstructionBuilder<'info> {
16    pub payer: AccountInfo<'info>,
17    pub magic_context: AccountInfo<'info>,
18    pub magic_program: AccountInfo<'info>,
19    pub magic_action: MagicAction<'info>,
20}
21
22impl<'info> MagicInstructionBuilder<'info> {
23    /// Build instruction for supplied an action and prepares accounts
24    pub fn build(self) -> (Vec<AccountInfo<'info>>, Instruction) {
25        // set those to be first
26        let mut all_accounts = vec![self.payer, self.magic_context];
27        // collect all accounts to be used in instruction
28        self.magic_action.collect_accounts(&mut all_accounts);
29        // filter duplicates & get indices map
30        let indices_map = utils::filter_duplicates_with_map(&mut all_accounts);
31
32        // construct args of ScheduleAction instruction
33        let args = self.magic_action.build_args(&indices_map);
34        // create accounts metas
35        let accounts_meta = all_accounts
36            .iter()
37            .map(|account| AccountMeta {
38                pubkey: *account.key,
39                is_signer: account.is_signer,
40                is_writable: account.is_writable,
41            })
42            .collect();
43
44        (
45            all_accounts,
46            Instruction::new_with_bincode(
47                *self.magic_program.key,
48                &MagicBlockInstruction::ScheduleBaseIntent(args),
49                accounts_meta,
50            ),
51        )
52    }
53
54    /// Builds instruction for action & invokes magicprogram
55    pub fn build_and_invoke(self) -> ProgramResult {
56        let (accounts, ix) = self.build();
57        invoke(&ix, &accounts)
58    }
59}
60
61/// Action that user wants to perform on base layer
62pub enum MagicAction<'info> {
63    BaseActions(Vec<CallHandler<'info>>),
64    Commit(CommitType<'info>),
65    CommitAndUndelegate(CommitAndUndelegate<'info>),
66}
67
68impl<'info> MagicAction<'info> {
69    /// Collects accounts. May contain duplicates that would have to be processd
70    /// TODO: could be &mut Vec<&'a AccountInfo<'info>>
71    fn collect_accounts(&self, accounts_container: &mut Vec<AccountInfo<'info>>) {
72        match self {
73            MagicAction::BaseActions(call_handlers) => call_handlers
74                .iter()
75                .for_each(|call_handler| call_handler.collect_accounts(accounts_container)),
76            MagicAction::Commit(commit_type) => commit_type.collect_accounts(accounts_container),
77            MagicAction::CommitAndUndelegate(commit_and_undelegate) => {
78                commit_and_undelegate.collect_accounts(accounts_container)
79            }
80        }
81    }
82
83    /// Creates argument for CPI
84    fn build_args(self, indices_map: &HashMap<Pubkey, u8>) -> MagicBaseIntentArgs {
85        match self {
86            MagicAction::BaseActions(call_handlers) => {
87                let call_handlers_args = call_handlers
88                    .into_iter()
89                    .map(|call_handler| call_handler.into_args(indices_map))
90                    .collect();
91                MagicBaseIntentArgs::BaseActions(call_handlers_args)
92            }
93            MagicAction::Commit(value) => MagicBaseIntentArgs::Commit(value.into_args(indices_map)),
94            MagicAction::CommitAndUndelegate(value) => {
95                MagicBaseIntentArgs::CommitAndUndelegate(value.into_args(indices_map))
96            }
97        }
98    }
99}
100
101/// Type of commit , can be whether standalone or with some custom actions on Base layer post commit
102pub enum CommitType<'info> {
103    /// Regular commit without actions
104    Standalone(Vec<AccountInfo<'info>>), // accounts to commit
105    /// Commits accounts and runs actions
106    WithHandler {
107        commited_accounts: Vec<AccountInfo<'info>>,
108        call_handlers: Vec<CallHandler<'info>>,
109    },
110}
111
112impl<'info> CommitType<'info> {
113    pub fn commited_accounts(&self) -> &[AccountInfo<'info>] {
114        match self {
115            Self::Standalone(commited_accounts) => commited_accounts,
116            Self::WithHandler {
117                commited_accounts, ..
118            } => commited_accounts,
119        }
120    }
121
122    fn collect_accounts(&self, accounts_container: &mut Vec<AccountInfo<'info>>) {
123        match self {
124            Self::Standalone(accounts) => accounts_container.extend(accounts.clone()),
125            Self::WithHandler {
126                commited_accounts,
127                call_handlers,
128            } => {
129                accounts_container.extend(commited_accounts.clone());
130                call_handlers
131                    .iter()
132                    .for_each(|call_handler| call_handler.collect_accounts(accounts_container));
133            }
134        }
135    }
136
137    fn into_args(self, indices_map: &HashMap<Pubkey, u8>) -> CommitTypeArgs {
138        match self {
139            Self::Standalone(accounts) => {
140                let accounts_indices = accounts_to_indices(accounts.as_slice(), indices_map);
141                CommitTypeArgs::Standalone(accounts_indices)
142            }
143            Self::WithHandler {
144                commited_accounts,
145                call_handlers,
146            } => {
147                let commited_accounts_indices =
148                    accounts_to_indices(commited_accounts.as_slice(), indices_map);
149                let call_handlers_args = call_handlers
150                    .into_iter()
151                    .map(|call_handler| call_handler.into_args(indices_map))
152                    .collect();
153                CommitTypeArgs::WithBaseActions {
154                    committed_accounts: commited_accounts_indices,
155                    base_actions: call_handlers_args,
156                }
157            }
158        }
159    }
160}
161
162/// Type of undelegate, can be whether standalone or with some custom actions on Base layer post commit
163/// No CommitedAccounts since it is only used with CommitAction.
164pub enum UndelegateType<'info> {
165    Standalone,
166    WithHandler(Vec<CallHandler<'info>>),
167}
168
169impl<'info> UndelegateType<'info> {
170    fn collect_accounts(&self, accounts_container: &mut Vec<AccountInfo<'info>>) {
171        match self {
172            Self::Standalone => {}
173            Self::WithHandler(call_handlers) => call_handlers
174                .iter()
175                .for_each(|call_handler| call_handler.collect_accounts(accounts_container)),
176        }
177    }
178
179    fn into_args(self, indices_map: &HashMap<Pubkey, u8>) -> UndelegateTypeArgs {
180        match self {
181            Self::Standalone => UndelegateTypeArgs::Standalone,
182            Self::WithHandler(call_handlers) => {
183                let call_handlers_args = call_handlers
184                    .into_iter()
185                    .map(|call_handler| call_handler.into_args(indices_map))
186                    .collect();
187                UndelegateTypeArgs::WithBaseActions {
188                    base_actions: call_handlers_args,
189                }
190            }
191        }
192    }
193}
194
195pub struct CommitAndUndelegate<'info> {
196    pub commit_type: CommitType<'info>,
197    pub undelegate_type: UndelegateType<'info>,
198}
199
200impl<'info> CommitAndUndelegate<'info> {
201    fn collect_accounts(&self, accounts_container: &mut Vec<AccountInfo<'info>>) {
202        self.commit_type.collect_accounts(accounts_container);
203        self.undelegate_type.collect_accounts(accounts_container);
204    }
205
206    fn into_args(self, indices_map: &HashMap<Pubkey, u8>) -> CommitAndUndelegateArgs {
207        let commit_type_args = self.commit_type.into_args(indices_map);
208        let undelegate_type_args = self.undelegate_type.into_args(indices_map);
209        CommitAndUndelegateArgs {
210            commit_type: commit_type_args,
211            undelegate_type: undelegate_type_args,
212        }
213    }
214}
215
216pub struct CallHandler<'info> {
217    pub args: ActionArgs,
218    pub compute_units: u32,
219    pub escrow_authority: AccountInfo<'info>,
220    pub destination_program: Pubkey,
221    pub accounts: Vec<ShortAccountMeta>,
222}
223
224impl<'info> CallHandler<'info> {
225    fn collect_accounts(&self, container: &mut Vec<AccountInfo<'info>>) {
226        container.push(self.escrow_authority.clone());
227    }
228
229    fn into_args(self, indices_map: &HashMap<Pubkey, u8>) -> BaseActionArgs {
230        let escrow_authority_index = indices_map
231            .get(self.escrow_authority.key)
232            .expect(EXPECTED_KEY_MSG);
233
234        BaseActionArgs {
235            args: self.args,
236            compute_units: self.compute_units,
237            destination_program: self.destination_program.to_bytes().into(),
238            escrow_authority: *escrow_authority_index,
239            accounts: self.accounts,
240        }
241    }
242}
243
244/// CPI to trigger a commit for one or more accounts in the ER
245#[inline(always)]
246pub fn commit_accounts<'a, 'info>(
247    payer: &'a AccountInfo<'info>,
248    account_infos: Vec<&'a AccountInfo<'info>>,
249    magic_context: &'a AccountInfo<'info>,
250    magic_program: &'a AccountInfo<'info>,
251) -> ProgramResult {
252    let ix = create_schedule_commit_ix(payer, &account_infos, magic_context, magic_program, false);
253    let mut all_accounts = vec![payer.clone(), magic_context.clone()];
254    all_accounts.extend(account_infos.into_iter().cloned());
255    invoke(&ix, &all_accounts)
256}
257
258/// CPI to trigger a commit and undelegate one or more accounts in the ER
259#[inline(always)]
260pub fn commit_and_undelegate_accounts<'a, 'info>(
261    payer: &'a AccountInfo<'info>,
262    account_infos: Vec<&'a AccountInfo<'info>>,
263    magic_context: &'a AccountInfo<'info>,
264    magic_program: &'a AccountInfo<'info>,
265) -> ProgramResult {
266    let ix = create_schedule_commit_ix(payer, &account_infos, magic_context, magic_program, true);
267    let mut all_accounts = vec![payer.clone(), magic_context.clone()];
268    all_accounts.extend(account_infos.into_iter().cloned());
269    invoke(&ix, &all_accounts)
270}
271
272pub fn create_schedule_commit_ix<'a, 'info>(
273    payer: &'a AccountInfo<'info>,
274    account_infos: &[&'a AccountInfo<'info>],
275    magic_context: &'a AccountInfo<'info>,
276    magic_program: &'a AccountInfo<'info>,
277    allow_undelegation: bool,
278) -> Instruction {
279    let instruction = if allow_undelegation {
280        MagicBlockInstruction::ScheduleCommitAndUndelegate
281    } else {
282        MagicBlockInstruction::ScheduleCommit
283    };
284    let mut account_metas = vec![
285        AccountMeta {
286            pubkey: *payer.key,
287            is_signer: true,
288            is_writable: true,
289        },
290        AccountMeta {
291            pubkey: *magic_context.key,
292            is_signer: false,
293            is_writable: true,
294        },
295    ];
296    account_metas.extend(account_infos.iter().map(|x| AccountMeta {
297        pubkey: *x.key,
298        is_signer: x.is_signer,
299        is_writable: x.is_writable,
300    }));
301    Instruction::new_with_bincode(*magic_program.key, &instruction, account_metas)
302}
303
304mod utils {
305    use crate::ephem::EXPECTED_KEY_MSG;
306    use crate::solana_compat::solana::{AccountInfo, Pubkey};
307    use std::collections::hash_map::Entry;
308    use std::collections::HashMap;
309
310    #[inline(always)]
311    pub fn accounts_to_indices(
312        accounts: &[AccountInfo],
313        indices_map: &HashMap<Pubkey, u8>,
314    ) -> Vec<u8> {
315        accounts
316            .iter()
317            .map(|account| *indices_map.get(account.key).expect(EXPECTED_KEY_MSG))
318            .collect()
319    }
320
321    /// Removes duplicates from array by pubkey
322    /// Returns a map of key to index in cleaned array
323    pub fn filter_duplicates_with_map(container: &mut Vec<AccountInfo>) -> HashMap<Pubkey, u8> {
324        let mut map = HashMap::new();
325        container.retain(|el| match map.entry(*el.key) {
326            Entry::Occupied(_) => false,
327            Entry::Vacant(entry) => {
328                // insert dummy value. Can't use index counter here
329                entry.insert(1);
330                true
331            }
332        });
333        // update map with valid indices
334        container.iter().enumerate().for_each(|(i, account)| {
335            *map.get_mut(account.key).unwrap() = i as u8;
336        });
337
338        map
339    }
340}
341
342#[test]
343fn test_instruction_equality() {
344    let serialized = bincode::serialize(&MagicBlockInstruction::ScheduleCommit).unwrap();
345    assert_eq!(vec![1, 0, 0, 0], serialized);
346
347    let serialized =
348        bincode::serialize(&MagicBlockInstruction::ScheduleCommitAndUndelegate).unwrap();
349    assert_eq!(vec![2, 0, 0, 0], serialized);
350}