Remove unused exchange program and bench client (#18463)

This commit is contained in:
Justin Starry
2021-07-12 21:59:11 -05:00
committed by GitHub
parent 8ad4ffdee5
commit cfece66403
24 changed files with 11 additions and 3544 deletions

View File

@ -1,32 +0,0 @@
[package]
name = "solana-exchange-program"
version = "1.8.0"
description = "Solana Exchange program"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
documentation = "https://docs.rs/solana-exchange-program"
edition = "2018"
[dependencies]
bincode = "1.3.3"
log = "0.4.14"
num-derive = { version = "0.3" }
num-traits = { version = "0.2" }
serde = "1.0.126"
serde_derive = "1.0.103"
solana-logger = { path = "../../logger", version = "=1.8.0" }
solana-metrics = { path = "../../metrics", version = "=1.8.0" }
solana-sdk = { path = "../../sdk", version = "=1.8.0" }
thiserror = "1.0"
[dev-dependencies]
solana-runtime = { path = "../../runtime", version = "=1.8.0" }
[lib]
crate-type = ["lib", "cdylib"]
name = "solana_exchange_program"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

View File

@ -1,131 +0,0 @@
//! Exchange program
use crate::exchange_state::*;
use crate::id;
use serde_derive::{Deserialize, Serialize};
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::pubkey::Pubkey;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct OrderRequestInfo {
/// Side of market of the order (bid/ask)
pub side: OrderSide,
/// Token pair to trade
pub pair: AssetPair,
/// Number of tokens to exchange; refers to the primary or the secondary depending on the order side
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,
}
#[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),
/// Order request
/// key 0 - Signer
/// key 1 - Account in which to record the trade order
/// key 2 - Token account to source tokens from
OrderRequest(OrderRequestInfo),
/// Order cancellation
/// key 0 - Signer
/// key 1 - Order to cancel
OrderCancellation,
/// Trade swap request
/// key 0 - Signer
/// key 2 - 'To' trade order
/// key 3 - `From` trade order
/// key 6 - Token account in which to deposit the brokers profit from the swap.
SwapRequest,
}
pub fn account_request(owner: &Pubkey, new: &Pubkey) -> Instruction {
let account_metas = vec![
AccountMeta::new(*owner, true),
AccountMeta::new(*new, false),
];
Instruction::new_with_bincode(id(), &ExchangeInstruction::AccountRequest, account_metas)
}
pub fn 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_with_bincode(
id(),
&ExchangeInstruction::TransferRequest(token, tokens),
account_metas,
)
}
pub fn trade_request(
owner: &Pubkey,
trade: &Pubkey,
side: OrderSide,
pair: AssetPair,
tokens: u64,
price: u64,
src_account: &Pubkey,
) -> Instruction {
let account_metas = vec![
AccountMeta::new(*owner, true),
AccountMeta::new(*trade, false),
AccountMeta::new(*src_account, false),
];
Instruction::new_with_bincode(
id(),
&ExchangeInstruction::OrderRequest(OrderRequestInfo {
side,
pair,
tokens,
price,
}),
account_metas,
)
}
pub fn order_cancellation(owner: &Pubkey, order: &Pubkey) -> Instruction {
let account_metas = vec![
AccountMeta::new(*owner, true),
AccountMeta::new(*order, false),
];
Instruction::new_with_bincode(id(), &ExchangeInstruction::OrderCancellation, account_metas)
}
pub fn swap_request(
owner: &Pubkey,
to_trade: &Pubkey,
from_trade: &Pubkey,
profit_account: &Pubkey,
) -> Instruction {
let account_metas = vec![
AccountMeta::new(*owner, true),
AccountMeta::new(*to_trade, false),
AccountMeta::new(*from_trade, false),
AccountMeta::new(*profit_account, false),
];
Instruction::new_with_bincode(id(), &ExchangeInstruction::SwapRequest, account_metas)
}

View File

@ -1,920 +0,0 @@
//! Config processor
use crate::exchange_instruction::*;
use crate::exchange_state::*;
use crate::faucet;
use log::*;
use num_derive::{FromPrimitive, ToPrimitive};
use serde_derive::Serialize;
use solana_metrics::inc_new_counter_info;
use solana_sdk::{
account::{ReadableAccount, WritableAccount},
decode_error::DecodeError,
instruction::InstructionError,
keyed_account::KeyedAccount,
process_instruction::InvokeContext,
program_utils::limited_deserialize,
pubkey::Pubkey,
};
use std::cmp;
use thiserror::Error;
#[derive(Error, Debug, Serialize, Clone, PartialEq, FromPrimitive, ToPrimitive)]
pub enum ExchangeError {
#[error("Signer does not own account")]
SignerDoesNotOwnAccount,
#[error("Signer does not own order")]
SignerDoesNotOwnOrder,
#[error("The From account balance is too low")]
FromAccountBalanceTooLow,
#[error("Attmept operation on mismatched tokens")]
TokenMismatch,
#[error("From trade balance is too low")]
FromTradeBalanceTooLow,
#[error("Serialization failed")]
SerializeFailed,
}
impl<T> DecodeError<T> for ExchangeError {
fn type_of() -> &'static str {
"ExchangeError"
}
}
pub struct ExchangeProcessor {}
impl ExchangeProcessor {
#[allow(clippy::needless_pass_by_value)]
fn map_to_invalid_arg(err: std::boxed::Box<bincode::ErrorKind>) -> InstructionError {
warn!("Deserialize failed, not a valid state: {:?}", err);
InstructionError::InvalidArgument
}
fn is_account_unallocated(data: &[u8]) -> Result<(), InstructionError> {
let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?;
if let ExchangeState::Unallocated = state {
Ok(())
} else {
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)?;
if let ExchangeState::Account(account) = state {
Ok(account)
} else {
error!("Not a valid account");
Err(InstructionError::InvalidAccountData)
}
}
fn deserialize_order(data: &[u8]) -> Result<OrderInfo, InstructionError> {
let state: ExchangeState = bincode::deserialize(data).map_err(Self::map_to_invalid_arg)?;
if let ExchangeState::Trade(info) = state {
Ok(info)
} else {
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(ExchangeError::SerializeFailed.into())
}
}
}
fn trade_to_token_account(trade: &OrderInfo) -> TokenAccountInfo {
// Turn trade order into token account
let token = match trade.side {
OrderSide::Ask => trade.pair.Quote,
OrderSide::Bid => trade.pair.Base,
};
let mut account = TokenAccountInfo::default().owner(&trade.owner);
account.tokens[token] = trade.tokens_settled;
account
}
fn calculate_swap(
scaler: u64,
to_trade: &mut OrderInfo,
from_trade: &mut OrderInfo,
profit_account: &mut TokenAccountInfo,
) -> Result<(), InstructionError> {
if to_trade.tokens == 0 || from_trade.tokens == 0 {
error!("Inactive Trade, balance is zero");
return Err(InstructionError::InvalidArgument);
}
if to_trade.price == 0 || from_trade.price == 0 {
error!("Inactive Trade, price is zero");
return 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");
return 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.Base;
let secondary_token = from_trade.pair.Quote;
// Update tokens
if to_trade.tokens < primary_cost {
error!("Not enough tokens in to account");
return Err(InstructionError::InvalidArgument);
}
if from_trade.tokens < secondary_cost {
error!("Not enough tokens in from account");
return Err(InstructionError::InvalidArgument);
}
to_trade.tokens -= primary_cost;
to_trade.tokens_settled += secondary_tokens;
from_trade.tokens -= secondary_cost;
from_trade.tokens_settled += primary_tokens;
profit_account.tokens[primary_token] += primary_profit;
profit_account.tokens[secondary_token] += secondary_profit;
Ok(())
}
fn do_account_request(keyed_accounts: &[KeyedAccount]) -> Result<(), InstructionError> {
const OWNER_INDEX: usize = 0;
const NEW_ACCOUNT_INDEX: usize = 1;
if keyed_accounts.len() < 2 {
error!("Not enough accounts");
return Err(InstructionError::InvalidArgument);
}
Self::is_account_unallocated(keyed_accounts[NEW_ACCOUNT_INDEX].try_account_ref()?.data())?;
Self::serialize(
&ExchangeState::Account(
TokenAccountInfo::default()
.owner(keyed_accounts[OWNER_INDEX].unsigned_key())
.tokens(100_000, 100_000, 100_000, 100_000),
),
&mut keyed_accounts[NEW_ACCOUNT_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)
}
fn do_transfer_request(
keyed_accounts: &[KeyedAccount],
token: Token,
tokens: u64,
) -> Result<(), InstructionError> {
const OWNER_INDEX: usize = 0;
const TO_ACCOUNT_INDEX: usize = 1;
const FROM_ACCOUNT_INDEX: usize = 2;
if keyed_accounts.len() < 3 {
error!("Not enough accounts");
return Err(InstructionError::InvalidArgument);
}
let mut to_account =
Self::deserialize_account(keyed_accounts[TO_ACCOUNT_INDEX].try_account_ref()?.data())?;
if &faucet::id() == keyed_accounts[FROM_ACCOUNT_INDEX].unsigned_key() {
to_account.tokens[token] += tokens;
} else {
let state: ExchangeState =
bincode::deserialize(keyed_accounts[FROM_ACCOUNT_INDEX].try_account_ref()?.data())
.map_err(Self::map_to_invalid_arg)?;
match state {
ExchangeState::Account(mut from_account) => {
if &from_account.owner != keyed_accounts[OWNER_INDEX].unsigned_key() {
error!("Signer does not own from account");
return Err(ExchangeError::SignerDoesNotOwnAccount.into());
}
if from_account.tokens[token] < tokens {
error!("From account balance too low");
return Err(ExchangeError::FromAccountBalanceTooLow.into());
}
from_account.tokens[token] -= tokens;
to_account.tokens[token] += tokens;
Self::serialize(
&ExchangeState::Account(from_account),
&mut keyed_accounts[FROM_ACCOUNT_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)?;
}
ExchangeState::Trade(mut from_trade) => {
if &from_trade.owner != keyed_accounts[OWNER_INDEX].unsigned_key() {
error!("Signer does not own from account");
return Err(ExchangeError::SignerDoesNotOwnAccount.into());
}
let from_token = match from_trade.side {
OrderSide::Ask => from_trade.pair.Quote,
OrderSide::Bid => from_trade.pair.Base,
};
if token != from_token {
error!("Trade to transfer from does not hold correct token");
return Err(ExchangeError::TokenMismatch.into());
}
if from_trade.tokens_settled < tokens {
error!("From trade balance too low");
return Err(ExchangeError::FromTradeBalanceTooLow.into());
}
from_trade.tokens_settled -= tokens;
to_account.tokens[token] += tokens;
Self::serialize(
&ExchangeState::Trade(from_trade),
&mut keyed_accounts[FROM_ACCOUNT_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)?;
}
_ => {
error!("Not a valid from account for transfer");
return Err(InstructionError::InvalidArgument);
}
}
}
Self::serialize(
&ExchangeState::Account(to_account),
&mut keyed_accounts[TO_ACCOUNT_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)
}
fn do_order_request(
keyed_accounts: &[KeyedAccount],
info: &OrderRequestInfo,
) -> Result<(), InstructionError> {
const OWNER_INDEX: usize = 0;
const ORDER_INDEX: usize = 1;
const ACCOUNT_INDEX: usize = 2;
if keyed_accounts.len() < 3 {
error!("Not enough accounts");
return Err(InstructionError::InvalidArgument);
}
Self::is_account_unallocated(keyed_accounts[ORDER_INDEX].try_account_ref()?.data())?;
let mut account =
Self::deserialize_account(keyed_accounts[ACCOUNT_INDEX].try_account_ref_mut()?.data())?;
if &account.owner != keyed_accounts[OWNER_INDEX].unsigned_key() {
error!("Signer does not own account");
return Err(ExchangeError::SignerDoesNotOwnAccount.into());
}
let from_token = match info.side {
OrderSide::Ask => info.pair.Base,
OrderSide::Bid => info.pair.Quote,
};
if account.tokens[from_token] < info.tokens {
error!("From token balance is too low");
return Err(ExchangeError::FromAccountBalanceTooLow.into());
}
if let Err(e) = check_trade(info.side, info.tokens, info.price) {
bincode::serialize(&e).unwrap();
}
// Trade holds the tokens in escrow
account.tokens[from_token] -= info.tokens;
inc_new_counter_info!("exchange_processor-trades", 1);
Self::serialize(
&ExchangeState::Trade(OrderInfo {
owner: *keyed_accounts[OWNER_INDEX].unsigned_key(),
side: info.side,
pair: info.pair,
tokens: info.tokens,
price: info.price,
tokens_settled: 0,
}),
&mut keyed_accounts[ORDER_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)?;
Self::serialize(
&ExchangeState::Account(account),
&mut keyed_accounts[ACCOUNT_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)
}
fn do_order_cancellation(keyed_accounts: &[KeyedAccount]) -> Result<(), InstructionError> {
const OWNER_INDEX: usize = 0;
const ORDER_INDEX: usize = 1;
if keyed_accounts.len() < 2 {
error!("Not enough accounts");
return Err(InstructionError::InvalidArgument);
}
let order = Self::deserialize_order(keyed_accounts[ORDER_INDEX].try_account_ref()?.data())?;
if &order.owner != keyed_accounts[OWNER_INDEX].unsigned_key() {
error!("Signer does not own order");
return Err(ExchangeError::SignerDoesNotOwnOrder.into());
}
let token = match order.side {
OrderSide::Ask => order.pair.Base,
OrderSide::Bid => order.pair.Quote,
};
let mut account = TokenAccountInfo::default().owner(&order.owner);
account.tokens[token] = order.tokens;
account.tokens[token] += order.tokens_settled;
// Turn trade order into a token account
Self::serialize(
&ExchangeState::Account(account),
&mut keyed_accounts[ORDER_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)
}
fn do_swap_request(keyed_accounts: &[KeyedAccount]) -> Result<(), InstructionError> {
const TO_ORDER_INDEX: usize = 1;
const FROM_ORDER_INDEX: usize = 2;
const PROFIT_ACCOUNT_INDEX: usize = 3;
if keyed_accounts.len() < 4 {
error!("Not enough accounts");
return Err(InstructionError::InvalidArgument);
}
let mut to_order =
Self::deserialize_order(keyed_accounts[TO_ORDER_INDEX].try_account_ref()?.data())?;
let mut from_order =
Self::deserialize_order(keyed_accounts[FROM_ORDER_INDEX].try_account_ref()?.data())?;
let mut profit_account = Self::deserialize_account(
keyed_accounts[PROFIT_ACCOUNT_INDEX]
.try_account_ref()?
.data(),
)?;
if to_order.side != OrderSide::Ask {
error!("To trade is not a To");
return Err(InstructionError::InvalidArgument);
}
if from_order.side != OrderSide::Bid {
error!("From trade is not a From");
return Err(InstructionError::InvalidArgument);
}
if to_order.pair != from_order.pair {
error!("Mismatched token pairs");
return Err(InstructionError::InvalidArgument);
}
if to_order.side == from_order.side {
error!("Matching trade sides");
return Err(InstructionError::InvalidArgument);
}
if let Err(e) =
Self::calculate_swap(SCALER, &mut to_order, &mut from_order, &mut profit_account)
{
error!(
"Swap calculation failed from {} for {} to {} for {}",
from_order.tokens, from_order.price, to_order.tokens, to_order.price,
);
return Err(e);
}
inc_new_counter_info!("exchange_processor-swaps", 1);
if to_order.tokens == 0 {
// Turn into token account
Self::serialize(
&ExchangeState::Account(Self::trade_to_token_account(&from_order)),
&mut keyed_accounts[TO_ORDER_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)?;
} else {
Self::serialize(
&ExchangeState::Trade(to_order),
&mut keyed_accounts[TO_ORDER_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)?;
}
if from_order.tokens == 0 {
// Turn into token account
Self::serialize(
&ExchangeState::Account(Self::trade_to_token_account(&from_order)),
&mut keyed_accounts[FROM_ORDER_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)?;
} else {
Self::serialize(
&ExchangeState::Trade(from_order),
&mut keyed_accounts[FROM_ORDER_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)?;
}
Self::serialize(
&ExchangeState::Account(profit_account),
&mut keyed_accounts[PROFIT_ACCOUNT_INDEX]
.try_account_ref_mut()?
.data_as_mut_slice(),
)
}
}
pub fn process_instruction(
_program_id: &Pubkey,
data: &[u8],
invoke_context: &mut dyn InvokeContext,
) -> Result<(), InstructionError> {
let keyed_accounts = invoke_context.get_keyed_accounts()?;
solana_logger::setup();
match limited_deserialize::<ExchangeInstruction>(data)? {
ExchangeInstruction::AccountRequest => {
ExchangeProcessor::do_account_request(keyed_accounts)
}
ExchangeInstruction::TransferRequest(token, tokens) => {
ExchangeProcessor::do_transfer_request(keyed_accounts, token, tokens)
}
ExchangeInstruction::OrderRequest(info) => {
ExchangeProcessor::do_order_request(keyed_accounts, &info)
}
ExchangeInstruction::OrderCancellation => {
ExchangeProcessor::do_order_cancellation(keyed_accounts)
}
ExchangeInstruction::SwapRequest => ExchangeProcessor::do_swap_request(keyed_accounts),
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{exchange_instruction, id};
use solana_runtime::bank::Bank;
use solana_runtime::bank_client::BankClient;
use solana_sdk::client::SyncClient;
use solana_sdk::genesis_config::create_genesis_config;
use solana_sdk::message::Message;
use solana_sdk::signature::{Keypair, Signer};
use solana_sdk::system_instruction;
use std::mem;
#[allow(clippy::too_many_arguments)]
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_tokens_settled_expect: u64,
secondary_tokens_settled_expect: u64,
profit_account_tokens: Tokens,
) -> Result<(), InstructionError> {
trace!(
"Swap {} for {} to {} for {}",
primary_tokens,
primary_price,
secondary_tokens,
secondary_price,
);
let mut to_trade = OrderInfo::default();
let mut from_trade = OrderInfo::default().side(OrderSide::Bid);
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 to_trade,
&mut from_trade,
&mut profit_account,
)?;
trace!(
"{:?} {:?} {:?} {:?}\n{:?}\n{:?}\n{:?}\n{:?}",
to_trade.tokens,
primary_tokens_expect,
from_trade.tokens,
secondary_tokens_expect,
primary_tokens_settled_expect,
secondary_tokens_settled_expect,
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_trade.tokens_settled, primary_tokens_settled_expect);
assert_eq!(from_trade.tokens_settled, secondary_tokens_settled_expect);
assert_eq!(profit_account.tokens, profit_account_tokens);
Ok(())
}
#[test]
#[rustfmt::skip]
fn test_calculate_swap() {
solana_logger::setup();
try_calc(1, 50, 2, 50, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err();
try_calc(1, 50, 1, 0, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err();
try_calc(1, 0, 1, 50, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err();
try_calc(1, 50, 1, 50, 0, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err();
try_calc(1, 50, 0, 50, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap_err();
try_calc(1, 1, 2, 2, 3, 1, 2, 0, 0, Tokens::new( 0, 0, 0, 0)).unwrap_err();
try_calc(1, 50, 1, 50, 1, 0, 0, 50, 50, Tokens::new( 0, 0, 0, 0)).unwrap();
try_calc(1, 1, 2, 3, 3, 0, 0, 2, 1, Tokens::new( 0, 1, 0, 0)).unwrap();
try_calc(1, 2, 2, 3, 3, 1, 0, 2, 1, Tokens::new( 0, 1, 0, 0)).unwrap();
try_calc(1, 3, 2, 3, 3, 2, 0, 2, 1, Tokens::new( 0, 1, 0, 0)).unwrap();
try_calc(1, 3, 2, 6, 3, 1, 0, 4, 2, Tokens::new( 0, 2, 0, 0)).unwrap();
try_calc(1000, 1, 2000, 3, 3000, 0, 0, 2, 1, Tokens::new( 0, 1, 0, 0)).unwrap();
try_calc(1, 3, 2, 7, 3, 1, 1, 4, 2, Tokens::new( 0, 2, 0, 0)).unwrap();
try_calc(1000, 3000, 333, 1000, 500, 0, 1,999, 1998, Tokens::new(1002, 0, 0, 0)).unwrap();
try_calc(1000, 50, 100, 50, 101, 0,45, 5, 49, Tokens::new( 1, 0, 0, 0)).unwrap();
}
fn create_bank(lamports: u64) -> (Bank, Keypair) {
let (genesis_config, mint_keypair) = create_genesis_config(lamports);
let mut bank = Bank::new(&genesis_config);
bank.add_builtin("exchange_program", id(), process_instruction);
(bank, mint_keypair)
}
fn create_client(bank: Bank, mint_keypair: Keypair) -> (BankClient, Keypair) {
let owner = Keypair::new();
let bank_client = BankClient::new(bank);
bank_client
.transfer_and_confirm(42, &mint_keypair, &owner.pubkey())
.unwrap();
(bank_client, owner)
}
fn create_account(client: &BankClient, owner: &Keypair) -> Pubkey {
let new = Keypair::new();
let instruction = system_instruction::create_account(
&owner.pubkey(),
&new.pubkey(),
1,
mem::size_of::<ExchangeState>() as u64,
&id(),
);
client
.send_and_confirm_message(
&[owner, &new],
Message::new(&[instruction], Some(&owner.pubkey())),
)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
new.pubkey()
}
fn create_token_account(client: &BankClient, owner: &Keypair) -> Pubkey {
let new = create_account(client, owner);
let instruction = exchange_instruction::account_request(&owner.pubkey(), &new);
client
.send_and_confirm_instruction(owner, instruction)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
new
}
fn transfer(client: &BankClient, owner: &Keypair, to: &Pubkey, token: Token, tokens: u64) {
let instruction = exchange_instruction::transfer_request(
&owner.pubkey(),
to,
&faucet::id(),
token,
tokens,
);
client
.send_and_confirm_instruction(owner, instruction)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
}
fn trade(
client: &BankClient,
owner: &Keypair,
side: OrderSide,
pair: AssetPair,
from_token: Token,
src_tokens: u64,
trade_tokens: u64,
price: u64,
) -> (Pubkey, Pubkey) {
let trade = create_account(client, owner);
let src = create_token_account(client, owner);
transfer(client, owner, &src, from_token, src_tokens);
let instruction = exchange_instruction::trade_request(
&owner.pubkey(),
&trade,
side,
pair,
trade_tokens,
price,
&src,
);
client
.send_and_confirm_instruction(owner, instruction)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
(trade, src)
}
#[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_data = client.get_account_data(&new).unwrap().unwrap();
// Check results
assert_eq!(
TokenAccountInfo::default()
.owner(&owner.pubkey())
.tokens(100_000, 100_000, 100_000, 100_000),
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 = exchange_instruction::account_request(&owner.pubkey(), &new);
client
.send_and_confirm_instruction(&owner, 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 = exchange_instruction::transfer_request(
&owner.pubkey(),
&new,
&faucet::id(),
Token::A,
42,
);
client
.send_and_confirm_instruction(&owner, instruction)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
let new_account_data = client.get_account_data(&new).unwrap().unwrap();
// Check results
assert_eq!(
TokenAccountInfo::default()
.owner(&owner.pubkey())
.tokens(100_042, 100_000, 100_000, 100_000),
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) = trade(
&client,
&owner,
OrderSide::Ask,
AssetPair::default(),
Token::A,
42,
2,
1000,
);
let trade_account_data = client.get_account_data(&trade).unwrap().unwrap();
let src_account_data = client.get_account_data(&src).unwrap().unwrap();
// check results
assert_eq!(
OrderInfo {
owner: owner.pubkey(),
side: OrderSide::Ask,
pair: AssetPair::default(),
tokens: 2,
price: 1000,
tokens_settled: 0
},
ExchangeProcessor::deserialize_order(&trade_account_data).unwrap()
);
assert_eq!(
TokenAccountInfo::default()
.owner(&owner.pubkey())
.tokens(100_040, 100_000, 100_000, 100_000),
ExchangeProcessor::deserialize_account(&src_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 profit = create_token_account(&client, &owner);
let (to_trade, _) = trade(
&client,
&owner,
OrderSide::Ask,
AssetPair::default(),
Token::A,
2,
2,
2000,
);
let (from_trade, _) = trade(
&client,
&owner,
OrderSide::Bid,
AssetPair::default(),
Token::B,
3,
3,
3000,
);
let instruction =
exchange_instruction::swap_request(&owner.pubkey(), &to_trade, &from_trade, &profit);
client
.send_and_confirm_instruction(&owner, instruction)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
let to_trade_account_data = client.get_account_data(&to_trade).unwrap().unwrap();
let from_trade_account_data = client.get_account_data(&from_trade).unwrap().unwrap();
let profit_account_data = client.get_account_data(&profit).unwrap().unwrap();
// check results
assert_eq!(
OrderInfo {
owner: owner.pubkey(),
side: OrderSide::Ask,
pair: AssetPair::default(),
tokens: 1,
price: 2000,
tokens_settled: 2,
},
ExchangeProcessor::deserialize_order(&to_trade_account_data).unwrap()
);
assert_eq!(
TokenAccountInfo::default()
.owner(&owner.pubkey())
.tokens(1, 0, 0, 0),
ExchangeProcessor::deserialize_account(&from_trade_account_data).unwrap()
);
assert_eq!(
TokenAccountInfo::default()
.owner(&owner.pubkey())
.tokens(100_000, 100_001, 100_000, 100_000),
ExchangeProcessor::deserialize_account(&profit_account_data).unwrap()
);
}
#[test]
fn test_exchange_trade_to_token_account() {
solana_logger::setup();
let (bank, mint_keypair) = create_bank(10_000);
let (client, owner) = create_client(bank, mint_keypair);
let profit = create_token_account(&client, &owner);
let (to_trade, _) = trade(
&client,
&owner,
OrderSide::Ask,
AssetPair::default(),
Token::A,
3,
3,
2000,
);
let (from_trade, _) = trade(
&client,
&owner,
OrderSide::Bid,
AssetPair::default(),
Token::B,
3,
3,
3000,
);
let instruction =
exchange_instruction::swap_request(&owner.pubkey(), &to_trade, &from_trade, &profit);
client
.send_and_confirm_instruction(&owner, instruction)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
let new = create_token_account(&client, &owner);
let instruction =
exchange_instruction::transfer_request(&owner.pubkey(), &new, &to_trade, Token::B, 1);
client
.send_and_confirm_instruction(&owner, instruction)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
let instruction =
exchange_instruction::transfer_request(&owner.pubkey(), &new, &from_trade, Token::A, 1);
client
.send_and_confirm_instruction(&owner, instruction)
.unwrap_or_else(|_| panic!("{}:{}", line!(), file!()));
let new_account_data = client.get_account_data(&new).unwrap().unwrap();
// Check results
assert_eq!(
TokenAccountInfo::default()
.owner(&owner.pubkey())
.tokens(100_001, 100_001, 100_000, 100_000),
ExchangeProcessor::deserialize_account(&new_account_data).unwrap()
);
}
}

View File

@ -1,226 +0,0 @@
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() -> Self {
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 {
Self {
A: a,
B: b,
C: c,
D: d,
}
}
}
impl std::ops::Index<Token> 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<Token> 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 struct AssetPair {
// represents a pair of two token enums that defines a market
pub Base: Token,
// "primary" token and numerator for pricing purposes
pub Quote: Token,
// "secondary" token and denominator for pricing purposes
}
impl Default for AssetPair {
fn default() -> AssetPair {
AssetPair {
Base: Token::A,
Quote: Token::B,
}
}
}
/// 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
}
}
/// side of the exchange between two tokens in a pair
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum OrderSide {
/// Offer the Base asset and Accept the Quote asset
Ask, // to
/// Offer the Quote asset and Accept the Base asset
Bid, // from
}
impl fmt::Display for OrderSide {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
OrderSide::Ask => write!(f, "A")?,
OrderSide::Bid => write!(f, "B")?,
}
Ok(())
}
}
/// Trade accounts are populated with this structure
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct OrderInfo {
/// Owner of the trade order
pub owner: Pubkey,
/// side of the order in the market (bid/ask)
pub side: OrderSide,
/// Token pair indicating two tokens to exchange, first is primary
pub pair: AssetPair,
/// Number of tokens to exchange; primary or secondary depending on side. Once
/// this number goes to zero this trade order will be converted into a regular token account
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,
/// Number of tokens that have been settled so far. These nay be transferred to another
/// token account by the owner.
pub tokens_settled: u64,
}
impl Default for OrderInfo {
fn default() -> Self {
Self {
owner: Pubkey::default(),
pair: AssetPair::default(),
side: OrderSide::Ask,
tokens: 0,
price: 0,
tokens_settled: 0,
}
}
}
impl OrderInfo {
pub fn pair(mut self, pair: AssetPair) -> Self {
self.pair = pair;
self
}
pub fn side(mut self, side: OrderSide) -> Self {
self.side = side;
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(side: OrderSide, tokens: u64, price: u64) -> Result<(), ExchangeError> {
match side {
OrderSide::Ask => {
if tokens * price / SCALER == 0 {
return Err(ExchangeError::InvalidTrade(format!(
"To trade of {} for {}/{} results in 0 tradeable tokens",
tokens, SCALER, price
)));
}
}
OrderSide::Bid => {
if tokens * SCALER / price == 0 {
return Err(ExchangeError::InvalidTrade(format!(
"From trade of {} for {}?{} results in 0 tradeable tokens",
tokens, SCALER, price
)));
}
}
}
Ok(())
}
/// 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 data is unallocated
Unallocated,
// Token account
Account(TokenAccountInfo),
// Trade order account
Trade(OrderInfo),
Invalid,
}
impl Default for ExchangeState {
fn default() -> Self {
ExchangeState::Unallocated
}
}

View File

@ -1,19 +0,0 @@
#![allow(clippy::integer_arithmetic)]
pub mod exchange_instruction;
pub mod exchange_processor;
pub mod exchange_state;
#[macro_use]
extern crate solana_metrics;
use crate::exchange_processor::process_instruction;
solana_sdk::declare_program!(
"Exchange11111111111111111111111111111111111",
solana_exchange_program,
process_instruction
);
pub mod faucet {
solana_sdk::declare_id!("ExchangeFaucet11111111111111111111111111111");
}