1use crate::{budget_expr::BudgetExpr, budget_state::BudgetState, id};
2use bincode::serialized_size;
3use chrono::prelude::{DateTime, Utc};
4use num_derive::{FromPrimitive, ToPrimitive};
5use serde_derive::{Deserialize, Serialize};
6use solana_sdk::{
7 decode_error::DecodeError,
8 hash::Hash,
9 instruction::{AccountMeta, Instruction},
10 pubkey::Pubkey,
11 system_instruction,
12};
13use thiserror::Error;
14
15#[derive(Error, Debug, Clone, PartialEq, FromPrimitive, ToPrimitive)]
16pub enum BudgetError {
17 #[error("destination missing")]
18 DestinationMissing,
19}
20
21impl<T> DecodeError<T> for BudgetError {
22 fn type_of() -> &'static str {
23 "BudgetError"
24 }
25}
26
27#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
29pub enum BudgetInstruction {
30 InitializeAccount(Box<BudgetExpr>),
32
33 ApplyTimestamp(DateTime<Utc>),
35
36 ApplySignature,
39
40 ApplyAccountData,
42}
43
44fn initialize_account(contract: &Pubkey, expr: BudgetExpr) -> Instruction {
45 let mut keys = vec![];
46 if let BudgetExpr::Pay(payment) = &expr {
47 keys.push(AccountMeta::new(payment.to, false));
48 }
49 keys.push(AccountMeta::new(*contract, false));
50 Instruction::new_with_bincode(
51 id(),
52 &BudgetInstruction::InitializeAccount(Box::new(expr)),
53 keys,
54 )
55}
56
57pub fn create_account(
58 from: &Pubkey,
59 contract: &Pubkey,
60 lamports: u64,
61 expr: BudgetExpr,
62) -> Vec<Instruction> {
63 if !expr.verify(lamports) {
64 panic!("invalid budget expression");
65 }
66 let space = serialized_size(&BudgetState::new(expr.clone())).unwrap();
67 vec![
68 system_instruction::create_account(&from, contract, lamports, space, &id()),
69 initialize_account(contract, expr),
70 ]
71}
72
73pub fn payment(from: &Pubkey, to: &Pubkey, contract: &Pubkey, lamports: u64) -> Vec<Instruction> {
75 let expr = BudgetExpr::new_payment(lamports, to);
76 create_account(from, &contract, lamports, expr)
77}
78
79pub fn on_date(
81 from: &Pubkey,
82 to: &Pubkey,
83 contract: &Pubkey,
84 dt: DateTime<Utc>,
85 dt_pubkey: &Pubkey,
86 cancelable: Option<Pubkey>,
87 lamports: u64,
88) -> Vec<Instruction> {
89 let expr = BudgetExpr::new_cancelable_future_payment(dt, dt_pubkey, lamports, to, cancelable);
90 create_account(from, contract, lamports, expr)
91}
92
93pub fn when_signed(
95 from: &Pubkey,
96 to: &Pubkey,
97 contract: &Pubkey,
98 witness: &Pubkey,
99 cancelable: Option<Pubkey>,
100 lamports: u64,
101) -> Vec<Instruction> {
102 let expr = BudgetExpr::new_cancelable_authorized_payment(witness, lamports, to, cancelable);
103 create_account(from, contract, lamports, expr)
104}
105
106pub fn when_account_data(
108 from: &Pubkey,
109 to: &Pubkey,
110 contract: &Pubkey,
111 account_pubkey: &Pubkey,
112 account_program_id: &Pubkey,
113 account_hash: Hash,
114 lamports: u64,
115) -> Vec<Instruction> {
116 let expr = BudgetExpr::new_payment_when_account_data(
117 account_pubkey,
118 account_program_id,
119 account_hash,
120 lamports,
121 to,
122 );
123 create_account(from, contract, lamports, expr)
124}
125
126pub fn apply_timestamp(
127 from: &Pubkey,
128 contract: &Pubkey,
129 to: &Pubkey,
130 dt: DateTime<Utc>,
131) -> Instruction {
132 let mut account_metas = vec![
133 AccountMeta::new(*from, true),
134 AccountMeta::new(*contract, false),
135 ];
136 if from != to {
137 account_metas.push(AccountMeta::new(*to, false));
138 }
139 Instruction::new_with_bincode(id(), &BudgetInstruction::ApplyTimestamp(dt), account_metas)
140}
141
142pub fn apply_signature(from: &Pubkey, contract: &Pubkey, to: &Pubkey) -> Instruction {
143 let mut account_metas = vec![
144 AccountMeta::new(*from, true),
145 AccountMeta::new(*contract, false),
146 ];
147 if from != to {
148 account_metas.push(AccountMeta::new(*to, false));
149 }
150 Instruction::new_with_bincode(id(), &BudgetInstruction::ApplySignature, account_metas)
151}
152
153pub fn apply_account_data(witness_pubkey: &Pubkey, contract: &Pubkey, to: &Pubkey) -> Instruction {
155 let account_metas = vec![
156 AccountMeta::new_readonly(*witness_pubkey, false),
157 AccountMeta::new(*contract, false),
158 AccountMeta::new(*to, false),
159 ];
160 Instruction::new_with_bincode(id(), &BudgetInstruction::ApplyAccountData, account_metas)
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::budget_expr::BudgetExpr;
167
168 #[test]
169 fn test_budget_instruction_verify() {
170 let alice_pubkey = solana_sdk::pubkey::new_rand();
171 let bob_pubkey = solana_sdk::pubkey::new_rand();
172 let budget_pubkey = solana_sdk::pubkey::new_rand();
173 payment(&alice_pubkey, &bob_pubkey, &budget_pubkey, 1); }
175
176 #[test]
177 #[should_panic]
178 fn test_budget_instruction_overspend() {
179 let alice_pubkey = solana_sdk::pubkey::new_rand();
180 let bob_pubkey = solana_sdk::pubkey::new_rand();
181 let budget_pubkey = solana_sdk::pubkey::new_rand();
182 let expr = BudgetExpr::new_payment(2, &bob_pubkey);
183 create_account(&alice_pubkey, &budget_pubkey, 1, expr);
184 }
185
186 #[test]
187 #[should_panic]
188 fn test_budget_instruction_underspend() {
189 let alice_pubkey = solana_sdk::pubkey::new_rand();
190 let bob_pubkey = solana_sdk::pubkey::new_rand();
191 let budget_pubkey = solana_sdk::pubkey::new_rand();
192 let expr = BudgetExpr::new_payment(1, &bob_pubkey);
193 create_account(&alice_pubkey, &budget_pubkey, 2, expr);
194 }
195}