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(¤t_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}