diff --git a/Cargo.lock b/Cargo.lock index f18e2cb345..cf8c7bbbe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2290,6 +2290,7 @@ dependencies = [ "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 42b86ef024..459d172525 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -15,6 +15,7 @@ hex = "0.3.2" byteorder = "1.2.1" chrono = { version = "0.4.0", features = ["serde"] } generic-array = { version = "0.12.0", default-features = false, features = ["serde"] } +itertools = "0.8.0" log = "0.4.2" ring = "0.13.2" sha2 = "0.8.0" diff --git a/sdk/src/budget_instruction.rs b/sdk/src/budget_instruction.rs index 3a6ea29971..accec702eb 100644 --- a/sdk/src/budget_instruction.rs +++ b/sdk/src/budget_instruction.rs @@ -1,4 +1,7 @@ use crate::budget_expr::BudgetExpr; +use crate::budget_program; +use crate::pubkey::Pubkey; +use crate::transaction_builder::BuilderInstruction; use chrono::prelude::{DateTime, Utc}; /// A smart contract. @@ -22,3 +25,13 @@ pub enum Instruction { /// signed by the containing transaction's `Pubkey`. ApplySignature, } + +impl Instruction { + pub fn new_budget(contract: Pubkey, expr: BudgetExpr) -> BuilderInstruction { + BuilderInstruction::new( + budget_program::id(), + &Instruction::NewBudget(expr), + vec![(contract, false)], + ) + } +} diff --git a/sdk/src/budget_transaction.rs b/sdk/src/budget_transaction.rs index 3524c01654..f393e3122a 100644 --- a/sdk/src/budget_transaction.rs +++ b/sdk/src/budget_transaction.rs @@ -4,12 +4,11 @@ use crate::budget_expr::{BudgetExpr, Condition}; use crate::budget_instruction::Instruction; use crate::budget_program; use crate::hash::Hash; -use crate::payment_plan::Payment; use crate::pubkey::Pubkey; use crate::signature::{Keypair, KeypairUtil}; use crate::system_instruction::SystemInstruction; -use crate::system_program; -use crate::transaction::{self, Transaction}; +use crate::transaction::Transaction; +use crate::transaction_builder::TransactionBuilder; use bincode::deserialize; use chrono::prelude::*; @@ -25,31 +24,12 @@ impl BudgetTransaction { fee: u64, ) -> Transaction { let contract = Keypair::new().pubkey(); - let keys = vec![from_keypair.pubkey(), contract]; - - let system_instruction = SystemInstruction::Move { tokens }; - - let payment = Payment { - tokens: tokens - fee, - to, - }; - let budget_instruction = Instruction::NewBudget(BudgetExpr::Pay(payment)); - - let program_ids = vec![system_program::id(), budget_program::id()]; - - let instructions = vec![ - transaction::Instruction::new(0, &system_instruction, vec![0, 1]), - transaction::Instruction::new(1, &budget_instruction, vec![1]), - ]; - - Transaction::new_with_instructions( - &[from_keypair], - &keys, - last_id, - fee, - program_ids, - instructions, - ) + let from = from_keypair.pubkey(); + let payment = BudgetExpr::new_payment(tokens - fee, to); + TransactionBuilder::new(fee) + .push(SystemInstruction::new_move(from, contract, tokens)) + .push(Instruction::new_budget(contract, payment)) + .sign(&[from_keypair], last_id) } /// Create and sign a new Transaction. Used for unit-testing. @@ -230,23 +210,13 @@ mod tests { #[test] fn test_serialize_claim() { - let expr = BudgetExpr::Pay(Payment { - tokens: 0, - to: Pubkey::default(), - }); - let instruction = Instruction::NewBudget(expr); - let instructions = vec![transaction::Instruction::new(0, &instruction, vec![])]; - let claim0 = Transaction { - account_keys: vec![], - last_id: Hash::default(), - signatures: vec![], - program_ids: vec![], - instructions, - fee: 0, - }; - let buf = serialize(&claim0).unwrap(); - let claim1: Transaction = deserialize(&buf).unwrap(); - assert_eq!(claim1, claim0); + let zero = Hash::default(); + let keypair0 = Keypair::new(); + let pubkey1 = Keypair::new().pubkey(); + let tx0 = BudgetTransaction::new_payment(&keypair0, pubkey1, 1, zero, 1); + let buf = serialize(&tx0).unwrap(); + let tx1: Transaction = deserialize(&buf).unwrap(); + assert_eq!(tx1, tx0); } #[test] diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 3f217c9e4a..27c3a7452d 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -22,6 +22,7 @@ pub mod system_transaction; pub mod timing; pub mod token_program; pub mod transaction; +pub mod transaction_builder; pub mod vote_program; pub mod vote_transaction; diff --git a/sdk/src/system_instruction.rs b/sdk/src/system_instruction.rs index 5e7f56d5e3..e66c0a4f1b 100644 --- a/sdk/src/system_instruction.rs +++ b/sdk/src/system_instruction.rs @@ -1,4 +1,6 @@ use crate::pubkey::Pubkey; +use crate::system_program; +use crate::transaction_builder::BuilderInstruction; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum SystemInstruction { @@ -21,3 +23,13 @@ pub enum SystemInstruction { /// * Transaction::keys[1] - destination Move { tokens: u64 }, } + +impl SystemInstruction { + pub fn new_move(from_id: Pubkey, to_id: Pubkey, tokens: u64) -> BuilderInstruction { + BuilderInstruction::new( + system_program::id(), + &SystemInstruction::Move { tokens }, + vec![(from_id, true), (to_id, false)], + ) + } +} diff --git a/sdk/src/transaction_builder.rs b/sdk/src/transaction_builder.rs new file mode 100644 index 0000000000..e2c3e09631 --- /dev/null +++ b/sdk/src/transaction_builder.rs @@ -0,0 +1,246 @@ +//! A library for composing transactions. + +use crate::hash::Hash; +use crate::pubkey::Pubkey; +use crate::signature::KeypairUtil; +use crate::transaction::{Instruction, Transaction}; +use itertools::Itertools; + +pub type BuilderInstruction = Instruction; + +fn position(keys: &[Pubkey], key: Pubkey) -> u8 { + keys.iter().position(|&k| k == key).unwrap() as u8 +} + +fn create_indexed_instruction( + ix: &Instruction, + keys: &[Pubkey], + program_ids: &[Pubkey], +) -> Instruction { + let accounts: Vec<_> = ix + .accounts + .iter() + .map(|&(k, _)| position(keys, k)) + .collect(); + Instruction { + program_ids_index: position(program_ids, ix.program_ids_index), + userdata: ix.userdata.clone(), + accounts, + } +} + +/// A utility for constructing transactions +#[derive(Default)] +pub struct TransactionBuilder { + fee: u64, + instructions: Vec, +} + +impl TransactionBuilder { + /// Create a new TransactionBuilder. + pub fn new(fee: u64) -> Self { + Self { + fee, + instructions: vec![], + } + } + + /// Add an instruction. + pub fn push(&mut self, instruction: BuilderInstruction) -> &mut Self { + self.instructions.push(instruction); + self + } + + /// Return pubkeys referenced by all instructions, with the ones needing signatures first. + /// No duplicates and order is preserved. + fn keys(&self) -> Vec { + let mut key_and_signed: Vec<_> = self + .instructions + .iter() + .flat_map(|ix| ix.accounts.iter()) + .collect(); + key_and_signed.sort_by(|x, y| y.1.cmp(&x.1)); + key_and_signed.into_iter().map(|x| x.0).unique().collect() + } + + /// Return program ids referenced by all instructions. No duplicates and order is preserved. + fn program_ids(&self) -> Vec { + self.instructions + .iter() + .map(|ix| ix.program_ids_index) + .unique() + .collect() + } + + /// Return the instructions, but indexing lists of keys and program ids. + fn instructions(&self, keys: &[Pubkey], program_ids: &[Pubkey]) -> Vec> { + self.instructions + .iter() + .map(|ix| create_indexed_instruction(ix, keys, program_ids)) + .collect() + } + + /// Return a signed transaction. + pub fn sign(&self, keypairs: &[&T], last_id: Hash) -> Transaction { + let keys = self.keys(); + let program_ids = self.program_ids(); + let instructions = self.instructions(&keys, &program_ids); + for (i, keypair) in keypairs.iter().enumerate() { + assert_eq!(keypair.pubkey(), keys[i], "keypair-pubkey mismatch"); + } + let unsigned_keys = &keys[keypairs.len()..]; + Transaction::new_with_instructions( + keypairs, + unsigned_keys, + last_id, + self.fee, + program_ids, + instructions, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::signature::{Keypair, KeypairUtil}; + + #[test] + fn test_transaction_builder_unique_program_ids() { + let program_id0 = Pubkey::default(); + let program_ids = TransactionBuilder::default() + .push(Instruction::new(program_id0, &0, vec![])) + .push(Instruction::new(program_id0, &0, vec![])) + .program_ids(); + assert_eq!(program_ids, vec![program_id0]); + } + + #[test] + fn test_transaction_builder_unique_program_ids_not_adjacent() { + let program_id0 = Pubkey::default(); + let program_id1 = Keypair::new().pubkey(); + let program_ids = TransactionBuilder::default() + .push(Instruction::new(program_id0, &0, vec![])) + .push(Instruction::new(program_id1, &0, vec![])) + .push(Instruction::new(program_id0, &0, vec![])) + .program_ids(); + assert_eq!(program_ids, vec![program_id0, program_id1]); + } + + #[test] + fn test_transaction_builder_unique_program_ids_order_preserved() { + let program_id0 = Keypair::new().pubkey(); + let program_id1 = Pubkey::default(); // Key less than program_id0 + let program_ids = TransactionBuilder::default() + .push(Instruction::new(program_id0, &0, vec![])) + .push(Instruction::new(program_id1, &0, vec![])) + .push(Instruction::new(program_id0, &0, vec![])) + .program_ids(); + assert_eq!(program_ids, vec![program_id0, program_id1]); + } + + #[test] + fn test_transaction_builder_unique_keys_both_signed() { + let program_id = Pubkey::default(); + let id0 = Pubkey::default(); + let keys = TransactionBuilder::default() + .push(Instruction::new(program_id, &0, vec![(id0, true)])) + .push(Instruction::new(program_id, &0, vec![(id0, true)])) + .keys(); + assert_eq!(keys, vec![id0]); + } + + #[test] + fn test_transaction_builder_unique_keys_one_signed() { + let program_id = Pubkey::default(); + let id0 = Pubkey::default(); + let keys = TransactionBuilder::default() + .push(Instruction::new(program_id, &0, vec![(id0, false)])) + .push(Instruction::new(program_id, &0, vec![(id0, true)])) + .keys(); + assert_eq!(keys, vec![id0]); + } + + #[test] + fn test_transaction_builder_unique_keys_order_preserved() { + let program_id = Pubkey::default(); + let id0 = Keypair::new().pubkey(); + let id1 = Pubkey::default(); // Key less than id0 + let keys = TransactionBuilder::default() + .push(Instruction::new(program_id, &0, vec![(id0, false)])) + .push(Instruction::new(program_id, &0, vec![(id1, false)])) + .keys(); + assert_eq!(keys, vec![id0, id1]); + } + + #[test] + fn test_transaction_builder_unique_keys_not_adjacent() { + let program_id = Pubkey::default(); + let id0 = Pubkey::default(); + let id1 = Keypair::new().pubkey(); + let keys = TransactionBuilder::default() + .push(Instruction::new(program_id, &0, vec![(id0, false)])) + .push(Instruction::new(program_id, &0, vec![(id1, false)])) + .push(Instruction::new(program_id, &0, vec![(id0, true)])) + .keys(); + assert_eq!(keys, vec![id0, id1]); + } + + #[test] + fn test_transaction_builder_signed_keys_first() { + let program_id = Pubkey::default(); + let id0 = Pubkey::default(); + let id1 = Keypair::new().pubkey(); + let keys = TransactionBuilder::default() + .push(Instruction::new(program_id, &0, vec![(id0, false)])) + .push(Instruction::new(program_id, &0, vec![(id1, true)])) + .keys(); + assert_eq!(keys, vec![id1, id0]); + } + + #[test] + #[should_panic] + fn test_transaction_builder_missing_key() { + let keypair = Keypair::new(); + TransactionBuilder::default().sign(&[&keypair], Hash::default()); + } + + #[test] + #[should_panic] + fn test_transaction_builder_wrong_key() { + let program_id = Pubkey::default(); + let keypair0 = Keypair::new(); + let wrong_id = Pubkey::default(); + TransactionBuilder::default() + .push(Instruction::new(program_id, &0, vec![(wrong_id, true)])) + .sign(&[&keypair0], Hash::default()); + } + + #[test] + fn test_transaction_builder_correct_key() { + let program_id = Pubkey::default(); + let keypair0 = Keypair::new(); + let id0 = keypair0.pubkey(); + let tx = TransactionBuilder::default() + .push(Instruction::new(program_id, &0, vec![(id0, true)])) + .sign(&[&keypair0], Hash::default()); + assert_eq!(tx.instructions[0], Instruction::new(0, &0, vec![0])); + } + + #[test] + fn test_transaction_builder_kitchen_sink() { + let program_id0 = Pubkey::default(); + let program_id1 = Keypair::new().pubkey(); + let id0 = Pubkey::default(); + let keypair1 = Keypair::new(); + let id1 = keypair1.pubkey(); + let tx = TransactionBuilder::default() + .push(Instruction::new(program_id0, &0, vec![(id0, false)])) + .push(Instruction::new(program_id1, &0, vec![(id1, true)])) + .push(Instruction::new(program_id0, &0, vec![(id1, false)])) + .sign(&[&keypair1], Hash::default()); + assert_eq!(tx.instructions[0], Instruction::new(0, &0, vec![1])); + assert_eq!(tx.instructions[1], Instruction::new(1, &0, vec![0])); + assert_eq!(tx.instructions[2], Instruction::new(0, &0, vec![0])); + } +}