solana_budget_program/
budget_instruction.rs

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/// An instruction to progress the smart contract.
28#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
29pub enum BudgetInstruction {
30    /// Declare and instantiate `BudgetExpr`.
31    InitializeAccount(Box<BudgetExpr>),
32
33    /// Tell a payment plan acknowledge the given `DateTime` has past.
34    ApplyTimestamp(DateTime<Utc>),
35
36    /// Tell the budget that the `InitializeAccount` with `Signature` has been
37    /// signed by the containing transaction's `Pubkey`.
38    ApplySignature,
39
40    /// Load an account and pass its data to the budget for inspection.
41    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
73/// Create a new payment script.
74pub 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
79/// Create a future payment script.
80pub 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
93/// Create a multisig payment script.
94pub 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
106/// Make a payment when an account has the given data
107pub 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
153/// Apply account data to a contract waiting on an AccountData witness.
154pub 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); // No panic! indicates success.
174    }
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}