diff --git a/Cargo.lock b/Cargo.lock index cfda020b9..24e0c8c7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2234,6 +2234,29 @@ dependencies = [ "tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "solana-exchange-api" +version = "0.13.0" +dependencies = [ + "bincode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-logger 0.13.0", + "solana-runtime 0.13.0", + "solana-sdk 0.13.0", +] + +[[package]] +name = "solana-exchange-program" +version = "0.13.0" +dependencies = [ + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-exchange-api 0.13.0", + "solana-logger 0.13.0", + "solana-sdk 0.13.0", +] + [[package]] name = "solana-failure" version = "0.13.0" @@ -2271,6 +2294,7 @@ dependencies = [ "solana 0.13.0", "solana-budget-api 0.13.0", "solana-config-api 0.13.0", + "solana-exchange-api 0.13.0", "solana-sdk 0.13.0", "solana-storage-api 0.13.0", "solana-token-api 0.13.0", diff --git a/Cargo.toml b/Cargo.toml index 3a54ab582..c98c0a6c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ members = [ "programs/budget_program", "programs/config_api", "programs/config_program", + "programs/exchange_api", + "programs/exchange_program", "programs/token_api", "programs/token_program", "programs/failure_program", diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 542cd1a35..9de0c6172 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -17,6 +17,8 @@ solana-budget-api = { path = "../programs/budget_api", version = "0.13.0" } solana-storage-api = { path = "../programs/storage_api", version = "0.13.0" } solana-token-api = { path = "../programs/token_api", version = "0.13.0" } solana-config-api = { path = "../programs/config_api", version = "0.13.0" } +solana-exchange-api = { path = "../programs/exchange_api", version = "0.13.0" } + [dev-dependencies] hashbrown = "0.1.8" diff --git a/genesis/src/main.rs b/genesis/src/main.rs index c2ed7e134..6517e98b8 100644 --- a/genesis/src/main.rs +++ b/genesis/src/main.rs @@ -79,6 +79,10 @@ fn main() -> Result<(), Box> { ), ("solana_token_program".to_string(), solana_token_api::id()), ("solana_config_program".to_string(), solana_config_api::id()), + ( + "solana_exchange_program".to_string(), + solana_exchange_api::id(), + ), ]); create_new_ledger(ledger_path, &genesis_block)?; @@ -124,6 +128,10 @@ mod tests { 133, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); + let exchange = Pubkey::new(&[ + 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]); assert_eq!(solana_sdk::system_program::id(), system); assert_eq!(solana_sdk::native_loader::id(), native); @@ -133,6 +141,7 @@ mod tests { assert_eq!(solana_token_api::id(), token); assert_eq!(solana_vote_api::id(), vote); assert_eq!(solana_config_api::id(), config); + assert_eq!(solana_exchange_api::id(), exchange); } #[test] @@ -147,6 +156,7 @@ mod tests { solana_token_api::id(), solana_vote_api::id(), solana_config_api::id(), + solana_exchange_api::id(), ]; assert!(ids.into_iter().all(move |id| unique.insert(id))); } diff --git a/programs/exchange_api/Cargo.toml b/programs/exchange_api/Cargo.toml new file mode 100644 index 000000000..662d813fd --- /dev/null +++ b/programs/exchange_api/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "solana-exchange-api" +version = "0.13.0" +description = "Solana Exchange program API" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +log = "0.4.2" +bincode = "1.1.2" +serde = "1.0.89" +serde_derive = "1.0.89" +solana-logger = { path = "../../logger", version = "0.13.0" } +solana-sdk = { path = "../../sdk", version = "0.13.0" } + +[dev-dependencies] +solana-runtime = { path = "../../runtime", version = "0.13.0" } + +[lib] +name = "solana_exchange_api" +crate-type = ["lib"] diff --git a/programs/exchange_api/src/exchange_instruction.rs b/programs/exchange_api/src/exchange_instruction.rs new file mode 100644 index 000000000..9203e75cf --- /dev/null +++ b/programs/exchange_api/src/exchange_instruction.rs @@ -0,0 +1,139 @@ +//! Exchange program + +use crate::exchange_state::*; +use crate::id; +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::transaction::{AccountMeta, Instruction}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct TradeRequestInfo { + /// Direction of trade + pub direction: Direction, + /// Token pair to trade + pub pair: TokenPair, + /// Number of tokens to exchange; refers to the primary or the secondary depending on the direction + pub tokens: u64, + /// The price ratio the primary price over the secondary price. The primary price is fixed + /// and equal to the variable `SCALER`. + pub price: u64, + /// Token account to deposit tokens on successful swap + pub dst_account: Pubkey, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum ExchangeInstruction { + /// New token account + /// key 0 - Signer + /// key 1 - New token account + AccountRequest, + /// Transfer tokens between two accounts + /// key 0 - Account to transfer tokens to + /// key 1 - Account to transfer tokens from. This can be the exchange program itself, + /// the exchange has a limitless number of tokens it can transfer. + TransferRequest(Token, u64), + /// Trade request + /// key 0 - Signer + /// key 1 - Account in which to record the swap + /// key 2 - Token account associated with this trade + TradeRequest(TradeRequestInfo), + /// Trade cancellation + /// key 0 - Signer + /// key 1 -Ttrade order to cancel + TradeCancellation, + /// Trade swap request + /// key 0 - Signer + /// key 1 - Account in which to record the swap + /// key 2 - 'To' trade order + /// key 3 - `From` trade order + /// key 4 - Token account associated with the To Trade + /// key 5 - Token account associated with From trade + /// key 6 - Token account in which to deposit the brokers profit from the swap. + SwapRequest, +} +impl ExchangeInstruction { + pub fn new_account_request(owner: &Pubkey, new: &Pubkey) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*new, false), + ]; + Instruction::new(id(), &ExchangeInstruction::AccountRequest, account_metas) + } + + pub fn new_transfer_request( + owner: &Pubkey, + to: &Pubkey, + from: &Pubkey, + token: Token, + tokens: u64, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*to, false), + AccountMeta::new(*from, false), + ]; + Instruction::new( + id(), + &ExchangeInstruction::TransferRequest(token, tokens), + account_metas, + ) + } + + pub fn new_trade_request( + owner: &Pubkey, + trade: &Pubkey, + direction: Direction, + pair: TokenPair, + tokens: u64, + price: u64, + src_account: &Pubkey, + dst_account: &Pubkey, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*trade, false), + AccountMeta::new(*src_account, false), + ]; + Instruction::new( + id(), + &ExchangeInstruction::TradeRequest(TradeRequestInfo { + direction, + pair, + tokens, + price, + dst_account: *dst_account, + }), + account_metas, + ) + } + + pub fn new_trade_cancellation(owner: &Pubkey, trade: &Pubkey, account: &Pubkey) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*trade, false), + AccountMeta::new(*account, false), + ]; + Instruction::new(id(), &ExchangeInstruction::TradeCancellation, account_metas) + } + + pub fn new_swap_request( + owner: &Pubkey, + swap: &Pubkey, + to_trade: &Pubkey, + from_trade: &Pubkey, + to_trade_account: &Pubkey, + from_trade_account: &Pubkey, + profit_account: &Pubkey, + ) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*owner, true), + AccountMeta::new(*swap, false), + AccountMeta::new(*to_trade, false), + AccountMeta::new(*from_trade, false), + AccountMeta::new(*to_trade_account, false), + AccountMeta::new(*from_trade_account, false), + AccountMeta::new(*profit_account, false), + ]; + Instruction::new(id(), &ExchangeInstruction::SwapRequest, account_metas) + } +} diff --git a/programs/exchange_api/src/exchange_processor.rs b/programs/exchange_api/src/exchange_processor.rs new file mode 100644 index 000000000..5bda46cb8 --- /dev/null +++ b/programs/exchange_api/src/exchange_processor.rs @@ -0,0 +1,824 @@ +//! Config processor + +use crate::exchange_instruction::*; +use crate::exchange_state::*; +use crate::id; +use log::*; +use solana_sdk::account::KeyedAccount; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::transaction::InstructionError; +use std::cmp; + +pub struct ExchangeProcessor {} + +impl ExchangeProcessor { + #[allow(clippy::needless_pass_by_value)] + fn map_to_invalid_arg(err: std::boxed::Box) -> InstructionError { + warn!("Deserialze failed: {:?}", err); + InstructionError::InvalidArgument + } + + fn is_account_unallocated(data: &[u8]) -> Result<(), InstructionError> { + let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + match state { + ExchangeState::Unallocated => Ok(()), + _ => { + error!("New account is already in use"); + Err(InstructionError::InvalidAccountData)? + } + } + } + + fn deserialize_account(data: &[u8]) -> Result<(TokenAccountInfo), InstructionError> { + let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + match state { + ExchangeState::Account(account) => Ok(account), + _ => { + error!("Not a valid account"); + Err(InstructionError::InvalidAccountData)? + } + } + } + + fn deserialize_trade(data: &[u8]) -> Result<(TradeOrderInfo), InstructionError> { + let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?; + match state { + ExchangeState::Trade(info) => Ok(info), + _ => { + error!("Not a valid trade"); + Err(InstructionError::InvalidAccountData)? + } + } + } + + fn serialize(state: &ExchangeState, data: &mut [u8]) -> Result<(), InstructionError> { + let writer = std::io::BufWriter::new(data); + match bincode::serialize_into(writer, state) { + Ok(_) => Ok(()), + Err(e) => { + error!("Serialize failed: {:?}", e); + Err(InstructionError::GenericError)? + } + } + } + + fn calculate_swap( + scaler: u64, + swap: &mut TradeSwapInfo, + to_trade: &mut TradeOrderInfo, + from_trade: &mut TradeOrderInfo, + to_trade_account: &mut TokenAccountInfo, + from_trade_account: &mut TokenAccountInfo, + profit_account: &mut TokenAccountInfo, + ) -> Result<(), InstructionError> { + if to_trade.tokens == 0 || from_trade.tokens == 0 { + error!("Inactive Trade, balance is zero"); + Err(InstructionError::InvalidArgument)? + } + if to_trade.price == 0 || from_trade.price == 0 { + error!("Inactive Trade, price is zero"); + Err(InstructionError::InvalidArgument)? + } + + // Calc swap + + trace!("tt {} ft {}", to_trade.tokens, from_trade.tokens); + trace!("tp {} fp {}", to_trade.price, from_trade.price); + + let max_to_secondary = to_trade.tokens * to_trade.price / scaler; + let max_to_primary = from_trade.tokens * scaler / from_trade.price; + + trace!("mtp {} mts {}", max_to_primary, max_to_secondary); + + let max_primary = cmp::min(max_to_primary, to_trade.tokens); + let max_secondary = cmp::min(max_to_secondary, from_trade.tokens); + + trace!("mp {} ms {}", max_primary, max_secondary); + + let primary_tokens = if max_secondary < max_primary { + max_secondary * scaler / from_trade.price + } else { + max_primary + }; + let secondary_tokens = if max_secondary < max_primary { + max_secondary + } else { + max_primary * to_trade.price / scaler + }; + + if primary_tokens == 0 || secondary_tokens == 0 { + error!("Trade quantities to low to be fulfilled"); + Err(InstructionError::InvalidArgument)? + } + + trace!("pt {} st {}", primary_tokens, secondary_tokens); + + let primary_cost = cmp::max(primary_tokens, secondary_tokens * scaler / to_trade.price); + let secondary_cost = cmp::max(secondary_tokens, primary_tokens * from_trade.price / scaler); + + trace!("pc {} sc {}", primary_cost, secondary_cost); + + let primary_profit = primary_cost - primary_tokens; + let secondary_profit = secondary_cost - secondary_tokens; + + trace!("pp {} sp {}", primary_profit, secondary_profit); + + let primary_token = to_trade.pair.primary(); + let secondary_token = from_trade.pair.secondary(); + + // Update tokens/accounts + + if to_trade.tokens < primary_cost { + error!("Not enough tokens in to account"); + Err(InstructionError::InvalidArgument)? + } + if from_trade.tokens < secondary_cost { + error!("Not enough tokens in from account"); + Err(InstructionError::InvalidArgument)? + } + to_trade.tokens -= primary_cost; + from_trade.tokens -= secondary_cost; + + to_trade_account.tokens[secondary_token] += secondary_tokens; + from_trade_account.tokens[primary_token] += primary_tokens; + + profit_account.tokens[primary_token] += primary_profit; + profit_account.tokens[secondary_token] += secondary_profit; + + swap.pair = to_trade.pair; + swap.primary_tokens = primary_cost; + swap.primary_price = to_trade.price; + swap.secondary_tokens = secondary_cost; + swap.secondary_price = from_trade.price; + + Ok(()) + } + + fn do_account_request(ka: &mut [KeyedAccount]) -> Result<(), InstructionError> { + if ka.len() < 2 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + + Self::is_account_unallocated(&ka[1].account.data[..])?; + Self::serialize( + &ExchangeState::Account(TokenAccountInfo::default().owner(&ka[0].unsigned_key())), + &mut ka[1].account.data[..], + ) + } + + fn do_transfer_request( + ka: &mut [KeyedAccount], + token: Token, + tokens: u64, + ) -> Result<(), InstructionError> { + if ka.len() < 3 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + + let mut to_account = Self::deserialize_account(&ka[1].account.data[..])?; + + if &id() == ka[2].unsigned_key() { + to_account.tokens[token] += tokens; + } else { + let mut from_account = Self::deserialize_account(&ka[2].account.data[..])?; + + if &from_account.owner != ka[0].unsigned_key() { + error!("Signer does not own from account"); + Err(InstructionError::GenericError)? + } + + if from_account.tokens[token] < tokens { + error!("From account balance too low"); + Err(InstructionError::GenericError)? + } + + from_account.tokens[token] -= tokens; + to_account.tokens[token] += tokens; + + Self::serialize( + &ExchangeState::Account(from_account), + &mut ka[1].account.data[..], + )?; + } + + Self::serialize( + &ExchangeState::Account(to_account), + &mut ka[1].account.data[..], + ) + } + + fn do_trade_request( + ka: &mut [KeyedAccount], + info: TradeRequestInfo, + ) -> Result<(), InstructionError> { + if ka.len() < 3 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + + Self::is_account_unallocated(&ka[1].account.data[..])?; + + let mut account = Self::deserialize_account(&ka[2].account.data[..])?; + + if &account.owner != ka[0].unsigned_key() { + error!("Signer does not own account"); + Err(InstructionError::GenericError)? + } + let from_token = match info.direction { + Direction::To => info.pair.primary(), + Direction::From => info.pair.secondary(), + }; + if account.tokens[from_token] < info.tokens { + error!("From token balance is too low"); + Err(InstructionError::GenericError)? + } + + if let Err(e) = check_trade(info.direction, info.tokens, info.price) { + bincode::serialize(&e).unwrap(); + } + + // Trade holds the tokens in escrow + account.tokens[from_token] -= info.tokens; + + Self::serialize( + &ExchangeState::Trade(TradeOrderInfo { + owner: *ka[0].unsigned_key(), + direction: info.direction, + pair: info.pair, + tokens: info.tokens, + price: info.price, + src_account: *ka[2].unsigned_key(), + dst_account: info.dst_account, + }), + &mut ka[1].account.data[..], + )?; + Self::serialize( + &ExchangeState::Account(account), + &mut ka[2].account.data[..], + ) + } + + fn do_trade_cancellation(ka: &mut [KeyedAccount]) -> Result<(), InstructionError> { + if ka.len() < 3 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + let mut trade = Self::deserialize_trade(&ka[1].account.data[..])?; + let mut account = Self::deserialize_account(&ka[2].account.data[..])?; + + if &trade.owner != ka[0].unsigned_key() { + error!("Signer does not own trade"); + Err(InstructionError::GenericError)? + } + + if &account.owner != ka[0].unsigned_key() { + error!("Signer does not own account"); + Err(InstructionError::GenericError)? + } + + let token = match trade.direction { + Direction::To => trade.pair.primary(), + Direction::From => trade.pair.secondary(), + }; + + // Outstanding tokens transferred back to account + account.tokens[token] += trade.tokens; + // Trade becomes invalid + trade.tokens = 0; + + Self::serialize(&ExchangeState::Trade(trade), &mut ka[1].account.data[..])?; + Self::serialize( + &ExchangeState::Account(account), + &mut ka[2].account.data[..], + ) + } + + fn do_swap_request(ka: &mut [KeyedAccount]) -> Result<(), InstructionError> { + if ka.len() < 7 { + error!("Not enough accounts"); + Err(InstructionError::InvalidArgument)? + } + Self::is_account_unallocated(&ka[1].account.data[..])?; + let mut to_trade = Self::deserialize_trade(&ka[2].account.data[..])?; + let mut from_trade = Self::deserialize_trade(&ka[3].account.data[..])?; + let mut to_trade_account = Self::deserialize_account(&ka[4].account.data[..])?; + let mut from_trade_account = Self::deserialize_account(&ka[5].account.data[..])?; + let mut profit_account = Self::deserialize_account(&ka[6].account.data[..])?; + + if &to_trade.dst_account != ka[4].unsigned_key() { + error!("To trade account and to account differ"); + Err(InstructionError::InvalidArgument)? + } + if &from_trade.dst_account != ka[5].unsigned_key() { + error!("From trade account and from account differ"); + Err(InstructionError::InvalidArgument)? + } + if to_trade.direction != Direction::To { + error!("To trade is not a To"); + Err(InstructionError::InvalidArgument)? + } + if from_trade.direction != Direction::From { + error!("From trade is not a From"); + Err(InstructionError::InvalidArgument)? + } + if to_trade.pair != from_trade.pair { + error!("Mismatched token pairs"); + Err(InstructionError::InvalidArgument)? + } + if to_trade.direction == from_trade.direction { + error!("Matching trade directions"); + Err(InstructionError::InvalidArgument)? + } + + let mut swap = TradeSwapInfo::default(); + swap.to_trade_order = *ka[2].unsigned_key(); + swap.from_trade_order = *ka[3].unsigned_key(); + + if let Err(e) = Self::calculate_swap( + SCALER, + &mut swap, + &mut to_trade, + &mut from_trade, + &mut to_trade_account, + &mut from_trade_account, + &mut profit_account, + ) { + error!( + "Swap calculation failed from {} for {} to {} for {}", + from_trade.tokens, from_trade.price, to_trade.tokens, to_trade.price, + ); + Err(e)? + } + + Self::serialize(&ExchangeState::Swap(swap), &mut ka[1].account.data[..])?; + Self::serialize(&ExchangeState::Trade(to_trade), &mut ka[2].account.data[..])?; + Self::serialize( + &ExchangeState::Trade(from_trade), + &mut ka[3].account.data[..], + )?; + Self::serialize( + &ExchangeState::Account(to_trade_account), + &mut ka[4].account.data[..], + )?; + Self::serialize( + &ExchangeState::Account(from_trade_account), + &mut ka[5].account.data[..], + )?; + Self::serialize( + &ExchangeState::Account(profit_account), + &mut ka[6].account.data[..], + ) + } +} + +pub fn process_instruction( + _program_id: &Pubkey, + keyed_accounts: &mut [KeyedAccount], + data: &[u8], + _tick_height: u64, +) -> Result<(), InstructionError> { + let command = bincode::deserialize::(data).map_err(|err| { + info!("Invalid transaction data: {:?} {:?}", data, err); + InstructionError::InvalidInstructionData + })?; + + trace!("{:?}", command); + + match command { + ExchangeInstruction::AccountRequest => { + ExchangeProcessor::do_account_request(keyed_accounts) + } + ExchangeInstruction::TransferRequest(token, tokens) => { + ExchangeProcessor::do_transfer_request(keyed_accounts, token, tokens) + } + ExchangeInstruction::TradeRequest(info) => { + ExchangeProcessor::do_trade_request(keyed_accounts, info) + } + ExchangeInstruction::TradeCancellation => { + ExchangeProcessor::do_trade_cancellation(keyed_accounts) + } + ExchangeInstruction::SwapRequest => ExchangeProcessor::do_swap_request(keyed_accounts), + } +} + +#[cfg(test)] +mod test { + use super::*; + use solana_runtime::bank::Bank; + use solana_runtime::bank_client::BankClient; + use solana_sdk::genesis_block::GenesisBlock; + use solana_sdk::signature::{Keypair, KeypairUtil}; + use solana_sdk::system_instruction::SystemInstruction; + use std::mem; + + fn try_calc( + scaler: u64, + primary_tokens: u64, + primary_price: u64, + secondary_tokens: u64, + secondary_price: u64, + primary_tokens_expect: u64, + secondary_tokens_expect: u64, + primary_account_tokens: Tokens, + secondary_account_tokens: Tokens, + profit_account_tokens: Tokens, + ) -> Result<(), InstructionError> { + trace!( + "Swap {} for {} to {} for {}", + primary_tokens, + primary_price, + secondary_tokens, + secondary_price, + ); + let mut swap = TradeSwapInfo::default(); + let mut to_trade = TradeOrderInfo::default(); + let mut from_trade = TradeOrderInfo::default().direction(Direction::From); + let mut to_account = TokenAccountInfo::default(); + let mut from_account = TokenAccountInfo::default(); + let mut profit_account = TokenAccountInfo::default(); + + to_trade.tokens = primary_tokens; + to_trade.price = primary_price; + from_trade.tokens = secondary_tokens; + from_trade.price = secondary_price; + ExchangeProcessor::calculate_swap( + scaler, + &mut swap, + &mut to_trade, + &mut from_trade, + &mut to_account, + &mut from_account, + &mut profit_account, + )?; + + trace!( + "{:?} {:?} {:?} {:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}", + to_trade.tokens, + primary_tokens_expect, + from_trade.tokens, + secondary_tokens_expect, + to_account.tokens, + primary_account_tokens, + from_account.tokens, + secondary_account_tokens, + profit_account.tokens, + profit_account_tokens + ); + + assert_eq!(to_trade.tokens, primary_tokens_expect); + assert_eq!(from_trade.tokens, secondary_tokens_expect); + assert_eq!(to_account.tokens, primary_account_tokens); + assert_eq!(from_account.tokens, secondary_account_tokens); + assert_eq!(profit_account.tokens, profit_account_tokens); + assert_eq!(swap.primary_tokens, primary_tokens - to_trade.tokens); + assert_eq!(swap.primary_price, to_trade.price); + assert_eq!(swap.secondary_tokens, secondary_tokens - from_trade.tokens); + assert_eq!(swap.secondary_price, from_trade.price); + Ok(()) + } + + #[test] + #[rustfmt::skip] + fn test_calculate_swap() { + solana_logger::setup(); + + try_calc(1, 50, 2, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 50, 1, 0, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 0, 1, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 50, 1, 50, 0, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 50, 0, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + try_calc(1, 1, 2, 2, 3, 1, 2, Tokens::new(0, 0, 0, 0), Tokens::new( 0, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap_err(); + + try_calc(1, 50, 1, 50, 1, 0, 0, Tokens::new(0, 50, 0, 0), Tokens::new( 50, 0, 0, 0), Tokens::new( 0, 0, 0, 0)).unwrap(); + try_calc(1, 1, 2, 3, 3, 0, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap(); + try_calc(1, 2, 2, 3, 3, 1, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap(); + try_calc(1, 3, 2, 3, 3, 2, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap(); + try_calc(1, 3, 2, 6, 3, 1, 0, Tokens::new(0, 4, 0, 0), Tokens::new( 2, 0, 0, 0), Tokens::new( 0, 2, 0, 0)).unwrap(); + try_calc(1000, 1, 2000, 3, 3000, 0, 0, Tokens::new(0, 2, 0, 0), Tokens::new( 1, 0, 0, 0), Tokens::new( 0, 1, 0, 0)).unwrap(); + try_calc(1, 3, 2, 7, 3, 1, 1, Tokens::new(0, 4, 0, 0), Tokens::new( 2, 0, 0, 0), Tokens::new( 0, 2, 0, 0)).unwrap(); + try_calc(1000, 3000, 333, 1000, 500, 0, 1, Tokens::new(0, 999, 0, 0), Tokens::new(1998, 0, 0, 0), Tokens::new(1002, 0, 0, 0)).unwrap(); + try_calc(1000, 50, 100, 50, 101, 0,45, Tokens::new(0, 5, 0, 0), Tokens::new( 49, 0, 0, 0), Tokens::new( 1, 0, 0, 0)).unwrap(); + } + + fn create_bank(lamports: u64) -> (Bank, Keypair) { + let (genesis_block, mint_keypair) = GenesisBlock::new(lamports); + let mut bank = Bank::new(&genesis_block); + bank.add_instruction_processor(id(), process_instruction); + (bank, mint_keypair) + } + + fn create_client(bank: &Bank, mint_keypair: Keypair) -> (BankClient, Pubkey) { + let owner = Keypair::new(); + let pubkey = owner.pubkey(); + let mint_client = BankClient::new(&bank, mint_keypair); + mint_client + .process_instruction(SystemInstruction::new_move( + &mint_client.pubkey(), + &owner.pubkey(), + 42, + )) + .expect("new_move"); + + let client = BankClient::new(&bank, owner); + + (client, pubkey) + } + + fn create_account(client: &BankClient, owner: &Pubkey) -> Pubkey { + let new = Keypair::new().pubkey(); + let instruction = SystemInstruction::new_program_account( + &owner, + &new, + 1, + mem::size_of::() as u64, + &id(), + ); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + new + } + + fn create_token_account(client: &BankClient, owner: &Pubkey) -> Pubkey { + let new = Keypair::new().pubkey(); + let instruction = SystemInstruction::new_program_account( + &owner, + &new, + 1, + mem::size_of::() as u64, + &id(), + ); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + let instruction = ExchangeInstruction::new_account_request(&owner, &new); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + new + } + + fn transfer(client: &BankClient, owner: &Pubkey, to: &Pubkey, token: Token, tokens: u64) { + let instruction = + ExchangeInstruction::new_transfer_request(owner, to, &id(), token, tokens); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + } + + fn trade( + client: &BankClient, + owner: &Pubkey, + direction: Direction, + pair: TokenPair, + from_token: Token, + src_tokens: u64, + trade_tokens: u64, + price: u64, + ) -> (Pubkey, Pubkey, Pubkey) { + let trade = create_account(&client, &owner); + let src = create_token_account(&client, &owner); + let dst = create_token_account(&client, &owner); + transfer(&client, &owner, &src, from_token, src_tokens); + + let instruction = ExchangeInstruction::new_trade_request( + owner, + &trade, + direction, + pair, + trade_tokens, + price, + &src, + &dst, + ); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + (trade, src, dst) + } + + fn deserialize_swap(data: &[u8]) -> TradeSwapInfo { + let state: ExchangeState = + bincode::deserialize(data).expect(&format!("{}:{}", line!(), file!())); + match state { + ExchangeState::Swap(info) => info, + _ => panic!("Not a valid swap"), + } + } + + #[test] + fn test_exchange_new_account() { + solana_logger::setup(); + let (bank, mint_keypair) = create_bank(10_000); + let (client, owner) = create_client(&bank, mint_keypair); + + let new = create_token_account(&client, &owner); + let new_account = bank.get_account(&new).unwrap(); + + // Check results + + assert_eq!( + TokenAccountInfo::default().owner(&owner), + ExchangeProcessor::deserialize_account(&new_account.data[..]).unwrap() + ); + } + + #[test] + fn test_exchange_new_account_not_unallocated() { + solana_logger::setup(); + let (bank, mint_keypair) = create_bank(10_000); + let (client, owner) = create_client(&bank, mint_keypair); + + let new = create_token_account(&client, &owner); + let instruction = ExchangeInstruction::new_account_request(&owner, &new); + client + .process_instruction(instruction) + .expect_err(&format!("{}:{}", line!(), file!())); + } + + #[test] + fn test_exchange_new_transfer_request() { + solana_logger::setup(); + let (bank, mint_keypair) = create_bank(10_000); + let (client, owner) = create_client(&bank, mint_keypair); + + let new = create_token_account(&client, &owner); + + let instruction = + ExchangeInstruction::new_transfer_request(&owner, &new, &id(), Token::A, 42); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + + let new_account = bank.get_account(&new).unwrap(); + + // Check results + + assert_eq!( + TokenAccountInfo::default() + .owner(&owner) + .tokens(42, 0, 0, 0), + ExchangeProcessor::deserialize_account(&new_account.data[..]).unwrap() + ); + } + + #[test] + fn test_exchange_new_trade_request() { + solana_logger::setup(); + let (bank, mint_keypair) = create_bank(10_000); + let (client, owner) = create_client(&bank, mint_keypair); + + let (trade, src, dst) = trade( + &client, + &owner, + Direction::To, + TokenPair::AB, + Token::A, + 42, + 2, + 1000, + ); + + let trade_account = bank.get_account(&trade).unwrap(); + let src_account = bank.get_account(&src).unwrap(); + let dst_account = bank.get_account(&dst).unwrap(); + + // check results + + assert_eq!( + TradeOrderInfo { + owner: owner, + direction: Direction::To, + pair: TokenPair::AB, + tokens: 2, + price: 1000, + src_account: src, + dst_account: dst + }, + ExchangeProcessor::deserialize_trade(&trade_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default() + .owner(&owner) + .tokens(40, 0, 0, 0), + ExchangeProcessor::deserialize_account(&src_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 0, 0, 0), + ExchangeProcessor::deserialize_account(&dst_account.data[..]).unwrap() + ); + } + + #[test] + fn test_exchange_new_swap_request() { + solana_logger::setup(); + let (bank, mint_keypair) = create_bank(10_000); + let (client, owner) = create_client(&bank, mint_keypair); + + let swap = create_account(&client, &owner); + let profit = create_token_account(&client, &owner); + let (to_trade, to_src, to_dst) = trade( + &client, + &owner, + Direction::To, + TokenPair::AB, + Token::A, + 2, + 2, + 2000, + ); + let (from_trade, from_src, from_dst) = trade( + &client, + &owner, + Direction::From, + TokenPair::AB, + Token::B, + 3, + 3, + 3000, + ); + + let instruction = ExchangeInstruction::new_swap_request( + &owner, + &swap, + &to_trade, + &from_trade, + &to_dst, + &from_dst, + &profit, + ); + client + .process_instruction(instruction) + .expect(&format!("{}:{}", line!(), file!())); + + let to_trade_account = bank.get_account(&to_trade).unwrap(); + let to_src_account = bank.get_account(&to_src).unwrap(); + let to_dst_account = bank.get_account(&to_dst).unwrap(); + let from_trade_account = bank.get_account(&from_trade).unwrap(); + let from_src_account = bank.get_account(&from_src).unwrap(); + let from_dst_account = bank.get_account(&from_dst).unwrap(); + let profit_account = bank.get_account(&profit).unwrap(); + let swap_account = bank.get_account(&swap).unwrap(); + + // check results + + assert_eq!( + TradeOrderInfo { + owner: owner, + direction: Direction::To, + pair: TokenPair::AB, + tokens: 1, + price: 2000, + src_account: to_src, + dst_account: to_dst + }, + ExchangeProcessor::deserialize_trade(&to_trade_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 0, 0, 0), + ExchangeProcessor::deserialize_account(&to_src_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 2, 0, 0), + ExchangeProcessor::deserialize_account(&to_dst_account.data[..]).unwrap() + ); + assert_eq!( + TradeOrderInfo { + owner: owner, + direction: Direction::From, + pair: TokenPair::AB, + tokens: 0, + price: 3000, + src_account: from_src, + dst_account: from_dst + }, + ExchangeProcessor::deserialize_trade(&from_trade_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 0, 0, 0), + ExchangeProcessor::deserialize_account(&from_src_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(1, 0, 0, 0), + ExchangeProcessor::deserialize_account(&from_dst_account.data[..]).unwrap() + ); + assert_eq!( + TokenAccountInfo::default().owner(&owner).tokens(0, 1, 0, 0), + ExchangeProcessor::deserialize_account(&profit_account.data[..]).unwrap() + ); + assert_eq!( + TradeSwapInfo { + pair: TokenPair::AB, + to_trade_order: to_trade, + from_trade_order: from_trade, + primary_tokens: 1, + primary_price: 2000, + secondary_tokens: 3, + secondary_price: 3000, + }, + deserialize_swap(&swap_account.data[..]) + ); + } +} diff --git a/programs/exchange_api/src/exchange_state.rs b/programs/exchange_api/src/exchange_state.rs new file mode 100644 index 000000000..762f92814 --- /dev/null +++ b/programs/exchange_api/src/exchange_state.rs @@ -0,0 +1,267 @@ +use serde_derive::{Deserialize, Serialize}; +use solana_sdk::pubkey::Pubkey; +use std::{error, fmt}; + +/// Fixed-point scaler, 10 = one base 10 digit to the right of the decimal, 100 = 2, ... +/// Used by both price and amount in their fixed point representation +pub const SCALER: u64 = 1000; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum ExchangeError { + InvalidTrade(String), +} +impl error::Error for ExchangeError {} +impl fmt::Display for ExchangeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ExchangeError::InvalidTrade(s) => write!(f, "{}", s), + } + } +} + +/// Supported token types +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum Token { + A, + B, + C, + D, +} +impl Default for Token { + fn default() -> Token { + Token::A + } +} + +// Values of tokens, could be quantities, prices, etc... +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[allow(non_snake_case)] +pub struct Tokens { + pub A: u64, + pub B: u64, + pub C: u64, + pub D: u64, +} +impl Tokens { + pub fn new(a: u64, b: u64, c: u64, d: u64) -> Self { + Tokens { + A: a, + B: b, + C: c, + D: d, + } + } +} +impl std::ops::Index for Tokens { + type Output = u64; + fn index(&self, t: Token) -> &u64 { + match t { + Token::A => &self.A, + Token::B => &self.B, + Token::C => &self.C, + Token::D => &self.D, + } + } +} +impl std::ops::IndexMut for Tokens { + fn index_mut(&mut self, t: Token) -> &mut u64 { + match t { + Token::A => &mut self.A, + Token::B => &mut self.B, + Token::C => &mut self.C, + Token::D => &mut self.D, + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[allow(non_snake_case)] +pub enum TokenPair { + AB, + AC, + AD, + BC, + BD, + CD, +} +impl Default for TokenPair { + fn default() -> TokenPair { + TokenPair::AB + } +} +impl TokenPair { + pub fn primary(self) -> Token { + match self { + TokenPair::AB => Token::A, + TokenPair::AC => Token::A, + TokenPair::AD => Token::A, + TokenPair::BC => Token::B, + TokenPair::BD => Token::B, + TokenPair::CD => Token::C, + } + } + pub fn secondary(self) -> Token { + match self { + TokenPair::AB => Token::B, + TokenPair::AC => Token::C, + TokenPair::AD => Token::D, + TokenPair::BC => Token::C, + TokenPair::BD => Token::D, + TokenPair::CD => Token::D, + } + } +} + +/// Token accounts are populated with this structure +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct TokenAccountInfo { + /// Investor who owns this account + pub owner: Pubkey, + /// Current number of tokens this account holds + pub tokens: Tokens, +} +impl TokenAccountInfo { + pub fn owner(mut self, owner: &Pubkey) -> Self { + self.owner = *owner; + self + } + pub fn tokens(mut self, a: u64, b: u64, c: u64, d: u64) -> Self { + self.tokens = Tokens { + A: a, + B: b, + C: c, + D: d, + }; + self + } +} + +/// Direction of the exchange between two tokens in a pair +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum Direction { + /// Trade first token type (primary) in the pair 'To' the second + To, + /// Trade first token type in the pair 'From' the second (secondary) + From, +} +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Direction::To => write!(f, "T")?, + Direction::From => write!(f, "F")?, + } + Ok(()) + } +} + +/// Trade accounts are populated with this structure +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct TradeOrderInfo { + /// Owner of the trade order + pub owner: Pubkey, + /// Direction of the exchange + pub direction: Direction, + /// Token pair indicating two tokens to exchange, first is primary + pub pair: TokenPair, + /// Number of tokens to exchange; primary or secondary depending on direction + pub tokens: u64, + /// Scaled price of the secondary token given the primary is equal to the scale value + /// If scale is 1 and price is 2 then ratio is 1:2 or 1 primary token for 2 secondary tokens + pub price: u64, + /// account which the tokens were source from. The trade account holds the tokens in escrow + /// until either one or more part of a swap or the trade is cancelled. + pub src_account: Pubkey, + /// account which the tokens the tokens will be deposited into on a successful trade + pub dst_account: Pubkey, +} +impl Default for TradeOrderInfo { + fn default() -> TradeOrderInfo { + TradeOrderInfo { + owner: Pubkey::default(), + pair: TokenPair::AB, + direction: Direction::To, + tokens: 0, + price: 0, + src_account: Pubkey::default(), + dst_account: Pubkey::default(), + } + } +} +impl TradeOrderInfo { + pub fn pair(mut self, pair: TokenPair) -> Self { + self.pair = pair; + self + } + pub fn direction(mut self, direction: Direction) -> Self { + self.direction = direction; + self + } + pub fn tokens(mut self, tokens: u64) -> Self { + self.tokens = tokens; + self + } + pub fn price(mut self, price: u64) -> Self { + self.price = price; + self + } +} + +pub fn check_trade(direction: Direction, tokens: u64, price: u64) -> Result<(), ExchangeError> { + match direction { + Direction::To => { + if tokens * price / SCALER == 0 { + Err(ExchangeError::InvalidTrade(format!( + "To trade of {} for {}/{} results in 0 tradeable tokens", + tokens, SCALER, price + )))? + } + } + Direction::From => { + if tokens * SCALER / price == 0 { + Err(ExchangeError::InvalidTrade(format!( + "From trade of {} for {}?{} results in 0 tradeable tokens", + tokens, SCALER, price + )))? + } + } + } + Ok(()) +} + +/// Swap accounts are populated with this structure +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct TradeSwapInfo { + /// Pair swapped + pub pair: TokenPair, + /// `To` trade order + pub to_trade_order: Pubkey, + /// `From` trade order + pub from_trade_order: Pubkey, + /// Number of primary tokens exchanged + pub primary_tokens: u64, + /// Price the primary tokens were exchanged for + pub primary_price: u64, + /// Number of secondary tokens exchanged + pub secondary_tokens: u64, + /// Price the secondary tokens were exchanged for + pub secondary_price: u64, +} + +/// Type of exchange account, account's user data is populated with this enum +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum ExchangeState { + /// Account's Userdata is unallocated + Unallocated, + // Token account + Account(TokenAccountInfo), + // Trade order account + Trade(TradeOrderInfo), + // Swap account + Swap(TradeSwapInfo), + Invalid, +} +impl Default for ExchangeState { + fn default() -> ExchangeState { + ExchangeState::Unallocated + } +} diff --git a/programs/exchange_api/src/exchange_transaction.rs b/programs/exchange_api/src/exchange_transaction.rs new file mode 100644 index 000000000..35c4418aa --- /dev/null +++ b/programs/exchange_api/src/exchange_transaction.rs @@ -0,0 +1,123 @@ +use crate::exchange_instruction::*; +use crate::exchange_state::*; +use crate::id; +use solana_sdk::hash::Hash; +use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::{Keypair, KeypairUtil}; +use solana_sdk::system_instruction::SystemInstruction; +use solana_sdk::transaction::Transaction; +use std::mem; + +pub struct ExchangeTransaction {} + +impl ExchangeTransaction { + pub fn new_account_request( + owner: &Keypair, + new: &Pubkey, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let space = mem::size_of::() as u64; + let create_ix = SystemInstruction::new_program_account(owner_id, new, 1, space, &id()); + let request_ix = ExchangeInstruction::new_account_request(owner_id, new); + let mut tx = Transaction::new(vec![create_ix, request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } + + pub fn new_transfer_request( + owner: &Keypair, + to: &Pubkey, + from: &Pubkey, + token: Token, + tokens: u64, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let request_ix = + ExchangeInstruction::new_transfer_request(owner_id, to, from, token, tokens); + let mut tx = Transaction::new(vec![request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } + + #[allow(clippy::too_many_arguments)] + pub fn new_trade_request( + owner: &Keypair, + trade: &Pubkey, + direction: Direction, + pair: TokenPair, + tokens: u64, + price: u64, + src_account: &Pubkey, + dst_account: &Pubkey, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let space = mem::size_of::() as u64; + let create_ix = SystemInstruction::new_program_account(owner_id, trade, 1, space, &id()); + let request_ix = ExchangeInstruction::new_trade_request( + owner_id, + trade, + direction, + pair, + tokens, + price, + src_account, + dst_account, + ); + let mut tx = Transaction::new(vec![create_ix, request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } + + pub fn new_trade_cancellation( + owner: &Keypair, + trade: &Pubkey, + account: &Pubkey, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let request_ix = ExchangeInstruction::new_trade_cancellation(owner_id, trade, account); + let mut tx = Transaction::new(vec![request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } + + pub fn new_swap_request( + owner: &Keypair, + swap: &Pubkey, + to_trade: &Pubkey, + from_trade: &Pubkey, + to_trade_account: &Pubkey, + from_trade_account: &Pubkey, + profit_account: &Pubkey, + recent_blockhash: Hash, + fee: u64, + ) -> Transaction { + let owner_id = &owner.pubkey(); + let space = mem::size_of::() as u64; + let create_ix = SystemInstruction::new_program_account(owner_id, swap, 1, space, &id()); + let request_ix = ExchangeInstruction::new_swap_request( + owner_id, + swap, + to_trade, + from_trade, + to_trade_account, + from_trade_account, + profit_account, + ); + let mut tx = Transaction::new(vec![create_ix, request_ix]); + tx.fee = fee; + tx.sign(&[owner], recent_blockhash); + tx + } +} diff --git a/programs/exchange_api/src/lib.rs b/programs/exchange_api/src/lib.rs new file mode 100644 index 000000000..a4505e151 --- /dev/null +++ b/programs/exchange_api/src/lib.rs @@ -0,0 +1,19 @@ +pub mod exchange_instruction; +pub mod exchange_processor; +pub mod exchange_state; +pub mod exchange_transaction; + +use solana_sdk::pubkey::Pubkey; + +pub const EXCHANGE_PROGRAM_ID: [u8; 32] = [ + 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, +]; + +pub fn check_id(program_id: &Pubkey) -> bool { + program_id.as_ref() == EXCHANGE_PROGRAM_ID +} + +pub fn id() -> Pubkey { + Pubkey::new(&EXCHANGE_PROGRAM_ID) +} diff --git a/programs/exchange_program/Cargo.toml b/programs/exchange_program/Cargo.toml new file mode 100644 index 000000000..f5b47e577 --- /dev/null +++ b/programs/exchange_program/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "solana-exchange-program" +version = "0.13.0" +description = "Solana exchange program" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" + +[dependencies] +log = "0.4.2" +solana-exchange-api = { path = "../exchange_api", version = "0.13.0" } +solana-logger = { path = "../../logger", version = "0.13.0" } +solana-sdk = { path = "../../sdk", version = "0.13.0" } + +[lib] +name = "solana_exchange_program" +crate-type = ["cdylib"] + + diff --git a/programs/exchange_program/src/lib.rs b/programs/exchange_program/src/lib.rs new file mode 100644 index 000000000..47e6d44ff --- /dev/null +++ b/programs/exchange_program/src/lib.rs @@ -0,0 +1,3 @@ +use solana_exchange_api::exchange_processor::process_instruction; + +solana_sdk::process_instruction_entrypoint!(process_instruction); diff --git a/sdk/src/transaction.rs b/sdk/src/transaction.rs index 0cc11a93d..000e8cf42 100644 --- a/sdk/src/transaction.rs +++ b/sdk/src/transaction.rs @@ -101,6 +101,7 @@ impl GenericInstruction { } /// Account metadata used to define Instructions +#[derive(Debug)] pub struct AccountMeta { /// An account's public key pub pubkey: Pubkey,