Add exchange program (#3444)
This commit is contained in:
parent
de2b6bc9fc
commit
6505221629
|
@ -2234,6 +2234,29 @@ dependencies = [
|
||||||
"tokio-codec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "solana-failure"
|
name = "solana-failure"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
|
@ -2271,6 +2294,7 @@ dependencies = [
|
||||||
"solana 0.13.0",
|
"solana 0.13.0",
|
||||||
"solana-budget-api 0.13.0",
|
"solana-budget-api 0.13.0",
|
||||||
"solana-config-api 0.13.0",
|
"solana-config-api 0.13.0",
|
||||||
|
"solana-exchange-api 0.13.0",
|
||||||
"solana-sdk 0.13.0",
|
"solana-sdk 0.13.0",
|
||||||
"solana-storage-api 0.13.0",
|
"solana-storage-api 0.13.0",
|
||||||
"solana-token-api 0.13.0",
|
"solana-token-api 0.13.0",
|
||||||
|
|
|
@ -18,6 +18,8 @@ members = [
|
||||||
"programs/budget_program",
|
"programs/budget_program",
|
||||||
"programs/config_api",
|
"programs/config_api",
|
||||||
"programs/config_program",
|
"programs/config_program",
|
||||||
|
"programs/exchange_api",
|
||||||
|
"programs/exchange_program",
|
||||||
"programs/token_api",
|
"programs/token_api",
|
||||||
"programs/token_program",
|
"programs/token_program",
|
||||||
"programs/failure_program",
|
"programs/failure_program",
|
||||||
|
|
|
@ -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-storage-api = { path = "../programs/storage_api", version = "0.13.0" }
|
||||||
solana-token-api = { path = "../programs/token_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-config-api = { path = "../programs/config_api", version = "0.13.0" }
|
||||||
|
solana-exchange-api = { path = "../programs/exchange_api", version = "0.13.0" }
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hashbrown = "0.1.8"
|
hashbrown = "0.1.8"
|
||||||
|
|
|
@ -79,6 +79,10 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
),
|
),
|
||||||
("solana_token_program".to_string(), solana_token_api::id()),
|
("solana_token_program".to_string(), solana_token_api::id()),
|
||||||
("solana_config_program".to_string(), solana_config_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)?;
|
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,
|
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,
|
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::system_program::id(), system);
|
||||||
assert_eq!(solana_sdk::native_loader::id(), native);
|
assert_eq!(solana_sdk::native_loader::id(), native);
|
||||||
|
@ -133,6 +141,7 @@ mod tests {
|
||||||
assert_eq!(solana_token_api::id(), token);
|
assert_eq!(solana_token_api::id(), token);
|
||||||
assert_eq!(solana_vote_api::id(), vote);
|
assert_eq!(solana_vote_api::id(), vote);
|
||||||
assert_eq!(solana_config_api::id(), config);
|
assert_eq!(solana_config_api::id(), config);
|
||||||
|
assert_eq!(solana_exchange_api::id(), exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -147,6 +156,7 @@ mod tests {
|
||||||
solana_token_api::id(),
|
solana_token_api::id(),
|
||||||
solana_vote_api::id(),
|
solana_vote_api::id(),
|
||||||
solana_config_api::id(),
|
solana_config_api::id(),
|
||||||
|
solana_exchange_api::id(),
|
||||||
];
|
];
|
||||||
assert!(ids.into_iter().all(move |id| unique.insert(id)));
|
assert!(ids.into_iter().all(move |id| unique.insert(id)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "solana-exchange-api"
|
||||||
|
version = "0.13.0"
|
||||||
|
description = "Solana Exchange program API"
|
||||||
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
|
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"]
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<bincode::ErrorKind>) -> 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::<ExchangeInstruction>(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::<ExchangeState>() 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::<ExchangeState>() 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[..])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<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 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::<ExchangeState>() 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::<ExchangeState>() 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::<ExchangeState>() 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "solana-exchange-program"
|
||||||
|
version = "0.13.0"
|
||||||
|
description = "Solana exchange program"
|
||||||
|
authors = ["Solana Maintainers <maintainers@solana.com>"]
|
||||||
|
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"]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
use solana_exchange_api::exchange_processor::process_instruction;
|
||||||
|
|
||||||
|
solana_sdk::process_instruction_entrypoint!(process_instruction);
|
|
@ -101,6 +101,7 @@ impl<P, Q> GenericInstruction<P, Q> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Account metadata used to define Instructions
|
/// Account metadata used to define Instructions
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct AccountMeta {
|
pub struct AccountMeta {
|
||||||
/// An account's public key
|
/// An account's public key
|
||||||
pub pubkey: Pubkey,
|
pub pubkey: Pubkey,
|
||||||
|
|
Loading…
Reference in New Issue