Skip to main content

rialo_venus/
lib.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::cmp::Ordering;
5
6use rialo_s_program::{
7    account_info::AccountInfo,
8    entrypoint::ProgramResult,
9    msg,
10    program::{invoke, invoke_signed},
11    program_error::ProgramError,
12    pubkey::Pubkey,
13    rent::Rent,
14    system_instruction, system_program,
15    sysvar::Sysvar,
16};
17use rialo_types::Nonce;
18
19// Re-export dependencies so that programs using rialo-venus don't have to declare them again.
20pub mod reexports {
21    pub use base64;
22    pub use bincode;
23    pub use rialo_events_core;
24    pub use rialo_oracle_processor_interface;
25    pub use rialo_oracle_registry_interface;
26    pub use rialo_s_program;
27    pub use rialo_subscriber_interface;
28    pub use rialo_types;
29    pub use serde;
30}
31
32/// Seed used to generate the PDA containing the workflow data.
33/// The seed for the PDA is `RIALO_WORKFLOW_SEED` + payer key + nonce.
34pub const WORKFLOW_SEED: &str = "rialo_workflow";
35
36/// Derive the Program Derived Address (PDA) for a workflow account.
37///
38/// # Arguments
39///
40/// * `program_id` - The program ID that will own the workflow account
41/// * `payer_key` - The payer key
42/// * `nonce` - A nonce to discriminate between different workflows for this program
43///
44/// # Returns
45///
46/// A tuple containing the PDA and bump seed
47///
48/// # Example
49///
50/// ```rust
51/// use rialo_s_program::pubkey::Pubkey;
52/// use rialo_venus::derive_workflow_address;
53///
54/// let payer_key = Pubkey::new_unique();
55/// let program_id = Pubkey::new_unique();
56/// let nonce = b"My Nonce".to_vec();
57/// let (workflow_address, bump) = derive_workflow_address(&program_id, &payer_key, &nonce);
58/// ```
59pub fn derive_workflow_address<NONCE: Into<Nonce>>(
60    program_id: &Pubkey,
61    payer_key: &Pubkey,
62    nonce: NONCE,
63) -> (Pubkey, u8) {
64    Pubkey::find_program_address(
65        &[
66            WORKFLOW_SEED.as_bytes(),
67            payer_key.as_array(),
68            nonce.into().as_bytes(),
69        ],
70        program_id,
71    )
72}
73
74/// Write workflow data to storage.
75///
76/// This method writes the workflow data to the PDA account, creating the account if required.
77/// If the workflow data has grown larger than the allocated account
78/// space, the account will be automatically resized.
79///
80/// # Arguments
81///
82/// * `program_id` - The program ID that owns the workflow account
83/// * `slug` - A slug to discriminate between different workflows for this payer
84/// * `payer_account` - must be a signer, used for potential resize funding
85/// * `workflow_account` - PDA from `derive_workflow_address`, doesn't have to be initialised
86/// * `system_program_account` - `rialo_s_program::system_program`, used for potential resize
87/// * `data` - The data item to serialise and write
88///
89/// # Returns
90///
91/// * `ProgramResult` - Result of the operation
92///
93/// # Errors
94///
95/// Returns an error if:
96/// * Required accounts are missing
97/// * PDA derivation doesn't match
98/// * Serialization fails
99/// * Account data is too small and resize fails
100pub fn write_to_storage<'account_info, D: serde::Serialize, NONCE: Into<Nonce>>(
101    program_id: &Pubkey,
102    slug: NONCE,
103    payer_account: &AccountInfo<'account_info>,
104    workflow_account: &AccountInfo<'account_info>,
105    system_program_account: &AccountInfo<'account_info>,
106    data: &D,
107) -> ProgramResult {
108    // Ensure the system program account is the correct one
109    if !system_program::check_id(system_program_account.key) {
110        msg!("Error: Incorrect system program ID");
111        return Err(ProgramError::IncorrectProgramId);
112    }
113
114    let slug: Nonce = slug.into();
115    let slug_bytes = slug.as_bytes();
116
117    // Derive the PDA for the workflow account
118    let (workflow_pubkey, bump_seed) =
119        derive_workflow_address(program_id, payer_account.key, slug_bytes);
120
121    // Verify the derived address matches the provided workflow account
122    if workflow_pubkey != *workflow_account.key {
123        msg!(
124            "Error: Provided workflow account key {} doesn't match derived address {}",
125            workflow_account.key,
126            workflow_pubkey
127        );
128        return Err(ProgramError::InvalidAccountData);
129    }
130
131    // TODO: Better error code
132    let data_bytes = bincode::serialize(data).map_err(|_| ProgramError::InvalidAccountData)?;
133    let data_len = data_bytes.len();
134
135    let rent = Rent::get()?;
136    let minimum_balance_required = rent.minimum_balance(data_len);
137
138    // Check if the workflow data account is initialized
139    if workflow_account.kelvins() == 0 {
140        // Create the workflow account
141        let seeds = &[
142            WORKFLOW_SEED.as_bytes(),
143            payer_account.key.as_array(),
144            slug_bytes,
145            &[bump_seed],
146        ];
147
148        msg!(
149            "Creating workflow account {} with {} bytes and {} kelvin",
150            workflow_account.key,
151            data_len,
152            minimum_balance_required,
153        );
154
155        // Create account with PDA
156        invoke_signed(
157            &system_instruction::create_account(
158                payer_account.key,
159                workflow_account.key,
160                minimum_balance_required,
161                data_len as u64,
162                program_id,
163            ),
164            &[payer_account.clone(), workflow_account.clone()],
165            &[seeds],
166        )?;
167
168        msg!(
169            "Workflow account {} initialized successfully",
170            workflow_account.key
171        );
172    } else {
173        // Check if resizing is needed via rent cost
174        if workflow_account.try_borrow_data()?.len() != data_len {
175            let original_data_len = workflow_account.data_len();
176
177            msg!(
178                "Resizing workflow account {} need {} bytes != allocated {}",
179                workflow_account.key,
180                data_len,
181                original_data_len
182            );
183
184            // Calculate kelvin needed for rent exemption after resize
185            let current_balance = workflow_account.kelvins();
186
187            // Transfer kelvin if needed in either direction
188            match minimum_balance_required.cmp(&current_balance) {
189                Ordering::Greater => {
190                    msg!(
191                        "Workflow account {} received {} kelvin because it has grown",
192                        workflow_account.key,
193                        minimum_balance_required - current_balance,
194                    );
195                    invoke(
196                        &system_instruction::transfer(
197                            payer_account.key,
198                            workflow_account.key,
199                            minimum_balance_required - current_balance,
200                        ),
201                        &[
202                            payer_account.clone(),
203                            workflow_account.clone(),
204                            system_program_account.clone(),
205                        ],
206                    )?;
207                }
208
209                Ordering::Less => {
210                    // I wonder if this is even worthwhile in terms of compute cost vs. rent minimisation?
211                    msg!(
212                        "Workflow account {} returned {} kelvin because it has shrunk",
213                        workflow_account.key,
214                        current_balance - minimum_balance_required,
215                    );
216
217                    let from_account = workflow_account;
218                    let to_account = payer_account;
219                    let kelvin = current_balance - minimum_balance_required;
220                    if **from_account.try_borrow_mut_kelvins()? < kelvin {
221                        return Err(ProgramError::InsufficientFunds);
222                    }
223                    **from_account.try_borrow_mut_kelvins()? -= kelvin;
224                    **to_account.try_borrow_mut_kelvins()? += kelvin;
225
226                    // We cannot use `invoke_signed` because: "Transfer: `from` must not carry data"
227                }
228
229                Ordering::Equal => {}
230            }
231
232            /*
233               TODO: realloc is actually limited to MAX_PERMITTED_DATA_INCREASE in one transaction,
234               handle cases when this value is actually exceeded
235            */
236            workflow_account.realloc(data_len, true)?;
237
238            msg!(
239                "Resized workflow account {} from {} to {} bytes",
240                workflow_account.key,
241                original_data_len,
242                data_len,
243            );
244        }
245    }
246
247    // Update the account data with the new workflow information
248    workflow_account
249        .try_borrow_mut_data()?
250        .copy_from_slice(&data_bytes);
251
252    Ok(())
253}
254
255/// Read workflow data from storage.
256///
257/// This method reads the workflow data from the PDA account.
258/// It exists to hide the serialisation strategy.
259///
260/// # Arguments
261///
262/// * `data` - bytes to deserialize
263///
264/// # Returns
265///
266/// * `Result` - Result of the operation
267///
268/// # Errors
269///
270/// Returns an error if:
271/// * Deserialization fails
272pub fn read_from_storage<D: for<'de> serde::Deserialize<'de>>(
273    data: &[u8],
274) -> Result<D, ProgramError> {
275    // TODO: Better error code
276    bincode::deserialize::<D>(data).map_err(|_| ProgramError::InvalidAccountData)
277}
278
279/// Close the workflow account and transfer its kelvin to the payer account.
280///
281/// # Arguments
282///
283/// * `payer_account` - The account that will receive the kelvin from the workflow account
284/// * `workflow_account` - The PDA account to be closed
285///
286/// # Returns
287///
288/// * `ProgramResult` - Result of the operation
289/// # Errors
290///
291/// Returns an error if:
292/// * The workflow account data cannot be borrowed for mutation
293/// * The workflow account kelvin cannot be borrowed for mutation
294/// * The payer account kelvin cannot be borrowed for mutation
295pub fn close_account(
296    payer_account: &AccountInfo<'_>,
297    workflow_account: &AccountInfo<'_>,
298) -> ProgramResult {
299    workflow_account.try_borrow_mut_data()?.fill(0);
300    let workflow_kelvin = workflow_account.kelvins();
301    **workflow_account.try_borrow_mut_kelvins()? = 0;
302    **payer_account.try_borrow_mut_kelvins()? += workflow_kelvin;
303    Ok(())
304}