Remove unused exchange program and bench client (#18463)
This commit is contained in:
parent
8ad4ffdee5
commit
cfece66403
|
@ -4295,35 +4295,6 @@ dependencies = [
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-bench-exchange"
|
|
||||||
version = "1.8.0"
|
|
||||||
dependencies = [
|
|
||||||
"clap 2.33.3",
|
|
||||||
"itertools 0.10.1",
|
|
||||||
"log 0.4.14",
|
|
||||||
"num-derive",
|
|
||||||
"num-traits",
|
|
||||||
"rand 0.7.3",
|
|
||||||
"rayon",
|
|
||||||
"serde_json",
|
|
||||||
"serde_yaml",
|
|
||||||
"solana-clap-utils",
|
|
||||||
"solana-client",
|
|
||||||
"solana-core",
|
|
||||||
"solana-exchange-program",
|
|
||||||
"solana-faucet",
|
|
||||||
"solana-genesis",
|
|
||||||
"solana-gossip",
|
|
||||||
"solana-local-cluster",
|
|
||||||
"solana-logger 1.8.0",
|
|
||||||
"solana-metrics",
|
|
||||||
"solana-net-utils",
|
|
||||||
"solana-runtime",
|
|
||||||
"solana-sdk",
|
|
||||||
"solana-version",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solana-bench-streamer"
|
name = "solana-bench-streamer"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -4685,23 +4656,6 @@ dependencies = [
|
||||||
"tar",
|
"tar",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "solana-exchange-program"
|
|
||||||
version = "1.8.0"
|
|
||||||
dependencies = [
|
|
||||||
"bincode",
|
|
||||||
"log 0.4.14",
|
|
||||||
"num-derive",
|
|
||||||
"num-traits",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"solana-logger 1.8.0",
|
|
||||||
"solana-metrics",
|
|
||||||
"solana-runtime",
|
|
||||||
"solana-sdk",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "solana-failure-program"
|
name = "solana-failure-program"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -4803,7 +4757,6 @@ dependencies = [
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"solana-clap-utils",
|
"solana-clap-utils",
|
||||||
"solana-cli-config",
|
"solana-cli-config",
|
||||||
"solana-exchange-program",
|
|
||||||
"solana-ledger",
|
"solana-ledger",
|
||||||
"solana-logger 1.8.0",
|
"solana-logger 1.8.0",
|
||||||
"solana-runtime",
|
"solana-runtime",
|
||||||
|
@ -5022,7 +4975,6 @@ dependencies = [
|
||||||
"solana-config-program",
|
"solana-config-program",
|
||||||
"solana-core",
|
"solana-core",
|
||||||
"solana-download-utils",
|
"solana-download-utils",
|
||||||
"solana-exchange-program",
|
|
||||||
"solana-faucet",
|
"solana-faucet",
|
||||||
"solana-gossip",
|
"solana-gossip",
|
||||||
"solana-ledger",
|
"solana-ledger",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"accounts-cluster-bench",
|
"accounts-cluster-bench",
|
||||||
"bench-exchange",
|
|
||||||
"bench-streamer",
|
"bench-streamer",
|
||||||
"bench-tps",
|
"bench-tps",
|
||||||
"accounts-bench",
|
"accounts-bench",
|
||||||
|
@ -44,7 +43,6 @@ members = [
|
||||||
"program-test",
|
"program-test",
|
||||||
"programs/bpf_loader",
|
"programs/bpf_loader",
|
||||||
"programs/config",
|
"programs/config",
|
||||||
"programs/exchange",
|
|
||||||
"programs/failure",
|
"programs/failure",
|
||||||
"programs/noop",
|
"programs/noop",
|
||||||
"programs/ownable",
|
"programs/ownable",
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
/target/
|
|
||||||
/config/
|
|
||||||
/config-local/
|
|
||||||
/farf/
|
|
|
@ -1,39 +0,0 @@
|
||||||
[package]
|
|
||||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
|
||||||
edition = "2018"
|
|
||||||
name = "solana-bench-exchange"
|
|
||||||
version = "1.8.0"
|
|
||||||
repository = "https://github.com/solana-labs/solana"
|
|
||||||
license = "Apache-2.0"
|
|
||||||
homepage = "https://solana.com/"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = "2.33.1"
|
|
||||||
itertools = "0.10.1"
|
|
||||||
log = "0.4.14"
|
|
||||||
num-derive = "0.3"
|
|
||||||
num-traits = "0.2"
|
|
||||||
rand = "0.7.0"
|
|
||||||
rayon = "1.5.1"
|
|
||||||
serde_json = "1.0.64"
|
|
||||||
serde_yaml = "0.8.17"
|
|
||||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.0" }
|
|
||||||
solana-core = { path = "../core", version = "=1.8.0" }
|
|
||||||
solana-genesis = { path = "../genesis", version = "=1.8.0" }
|
|
||||||
solana-client = { path = "../client", version = "=1.8.0" }
|
|
||||||
solana-exchange-program = { path = "../programs/exchange", version = "=1.8.0" }
|
|
||||||
solana-faucet = { path = "../faucet", version = "=1.8.0" }
|
|
||||||
solana-gossip = { path = "../gossip", version = "=1.8.0" }
|
|
||||||
solana-logger = { path = "../logger", version = "=1.8.0" }
|
|
||||||
solana-metrics = { path = "../metrics", version = "=1.8.0" }
|
|
||||||
solana-net-utils = { path = "../net-utils", version = "=1.8.0" }
|
|
||||||
solana-runtime = { path = "../runtime", version = "=1.8.0" }
|
|
||||||
solana-sdk = { path = "../sdk", version = "=1.8.0" }
|
|
||||||
solana-version = { path = "../version", version = "=1.8.0" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
solana-local-cluster = { path = "../local-cluster", version = "=1.8.0" }
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
targets = ["x86_64-unknown-linux-gnu"]
|
|
|
@ -1,479 +0,0 @@
|
||||||
# token-exchange
|
|
||||||
Solana Token Exchange Bench
|
|
||||||
|
|
||||||
If you can't wait; jump to [Running the exchange](#Running-the-exchange) to
|
|
||||||
learn how to start and interact with the exchange.
|
|
||||||
|
|
||||||
### Table of Contents
|
|
||||||
[Overview](#Overview)<br>
|
|
||||||
[Premise](#Premise)<br>
|
|
||||||
[Exchange startup](#Exchange-startup)<br>
|
|
||||||
[Order Requests](#Trade-requests)<br>
|
|
||||||
[Order Cancellations](#Trade-cancellations)<br>
|
|
||||||
[Trade swap](#Trade-swap)<br>
|
|
||||||
[Exchange program operations](#Exchange-program-operations)<br>
|
|
||||||
[Quotes and OHLCV](#Quotes-and-OHLCV)<br>
|
|
||||||
[Investor strategies](#Investor-strategies)<br>
|
|
||||||
[Running the exchange](#Running-the-exchange)<br>
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
An exchange is a marketplace where one asset can be traded for another. This
|
|
||||||
demo demonstrates one way to host an exchange on the Solana blockchain by
|
|
||||||
emulating a currency exchange.
|
|
||||||
|
|
||||||
The assets are virtual tokens held by investors who may post order requests to
|
|
||||||
the exchange. A Matcher monitors the exchange and posts swap requests for
|
|
||||||
matching orders. All the transactions can execute concurrently.
|
|
||||||
|
|
||||||
## Premise
|
|
||||||
|
|
||||||
- Exchange
|
|
||||||
- An exchange is a marketplace where one asset can be traded for another.
|
|
||||||
The exchange in this demo is the on-chain program that implements the
|
|
||||||
tokens and the policies for trading those tokens.
|
|
||||||
- Token
|
|
||||||
- A virtual asset that can be owned, traded, and holds virtual intrinsic value
|
|
||||||
compared to other assets. There are four types of tokens in this demo, A,
|
|
||||||
B, C, D. Each one may be traded for another.
|
|
||||||
- Token account
|
|
||||||
- An account owned by the exchange that holds a quantity of one type of token.
|
|
||||||
- Account request
|
|
||||||
- A request to create a token account
|
|
||||||
- Token request
|
|
||||||
- A request to deposit tokens of a particular type into a token account.
|
|
||||||
- Asset pair
|
|
||||||
- A struct with fields Base and Quote, representing the two assets which make up a
|
|
||||||
trading pair, which themselves are Tokens. The Base or 'primary' asset is the
|
|
||||||
numerator and the Quote is the denominator for pricing purposes.
|
|
||||||
- Order side
|
|
||||||
- Describes which side of the market an investor wants to place a trade on. Options
|
|
||||||
are "Bid" or "Ask", where a bid represents an offer to purchase the Base asset of
|
|
||||||
the AssetPair for a sum of the Quote Asset and an Ask is an offer to sell Base asset
|
|
||||||
for the Quote asset.
|
|
||||||
- Price ratio
|
|
||||||
- An expression of the relative prices of two tokens. Calculated with the Base
|
|
||||||
Asset as the numerator and the Quote Asset as the denominator. Ratios are
|
|
||||||
represented as fixed point numbers. The fixed point scaler is defined in
|
|
||||||
[exchange_state.rs](https://github.com/solana-labs/solana/blob/c2fdd1362a029dcf89c8907c562d2079d977df11/programs/exchange_api/src/exchange_state.rs#L7)
|
|
||||||
- Order request
|
|
||||||
- A Solana transaction sent by a trader to the exchange to submit an order.
|
|
||||||
Order requests are made up of the token pair, the order side (bid or ask),
|
|
||||||
quantity of the primary token, the price ratio, and the two token accounts
|
|
||||||
to be credited/deducted. An example trade request looks like "T AB 5 2"
|
|
||||||
which reads "Exchange 5 A tokens to B tokens at a price ratio of 1:2" A fulfilled trade would result in 5 A tokens
|
|
||||||
deducted and 10 B tokens credited to the trade initiator's token accounts.
|
|
||||||
Successful order requests result in an order.
|
|
||||||
- Order
|
|
||||||
- The result of a successful order request. orders are stored in
|
|
||||||
accounts owned by the submitter of the order request. They can only be
|
|
||||||
canceled by their owner but can be used by anyone in a trade swap. They
|
|
||||||
contain the same information as the order request.
|
|
||||||
- Price spread
|
|
||||||
- The difference between the two matching orders. The spread is the
|
|
||||||
profit of the Matcher initiating the swap request.
|
|
||||||
- Match requirements
|
|
||||||
- Policies that result in a successful trade swap.
|
|
||||||
- Match request
|
|
||||||
- A request to fill two complementary orders (bid/ask), resulting if successful,
|
|
||||||
in a trade being created.
|
|
||||||
- Trade
|
|
||||||
- A successful trade is created from two matching orders that meet
|
|
||||||
swap requirements which are submitted in a Match Request by a Matcher and
|
|
||||||
executed by the exchange. A trade may not wholly satisfy one or both of the
|
|
||||||
orders in which case the orders are adjusted appropriately. Upon execution,
|
|
||||||
tokens are distributed to the traders' accounts and any overlap or
|
|
||||||
"negative spread" between orders is deposited into the Matcher's profit
|
|
||||||
account. All successful trades are recorded in the data of a new solana
|
|
||||||
account for posterity.
|
|
||||||
- Investor
|
|
||||||
- Individual investors who hold a number of tokens and wish to trade them on
|
|
||||||
the exchange. Investors operate as Solana thin clients who own a set of
|
|
||||||
accounts containing tokens and/or order requests. Investors post
|
|
||||||
transactions to the exchange in order to request tokens and post or cancel
|
|
||||||
order requests.
|
|
||||||
- Matcher
|
|
||||||
- An agent who facilitates trading between investors. Matchers operate as
|
|
||||||
Solana thin clients who monitor all the orders looking for a trade
|
|
||||||
match. Once found, the Matcher issues a swap request to the exchange.
|
|
||||||
Matchers are the engine of the exchange and are rewarded for their efforts by
|
|
||||||
accumulating the price spreads of the swaps they initiate. Matchers also
|
|
||||||
provide current bid/ask price and OHLCV (Open, High, Low, Close, Volume)
|
|
||||||
information on demand via a public network port.
|
|
||||||
- Transaction fees
|
|
||||||
- Solana transaction fees are paid for by the transaction submitters who are
|
|
||||||
the Investors and Matchers.
|
|
||||||
|
|
||||||
## Exchange startup
|
|
||||||
|
|
||||||
The exchange is up and running when it reaches a state where it can take
|
|
||||||
investors' trades and Matchers' match requests. To achieve this state the
|
|
||||||
following must occur in order:
|
|
||||||
|
|
||||||
- Start the Solana blockchain
|
|
||||||
- Start the thin-client
|
|
||||||
- The Matcher subscribes to change notifications for all the accounts owned by
|
|
||||||
the exchange program id. The subscription is managed via Solana's JSON RPC
|
|
||||||
interface.
|
|
||||||
- The Matcher starts responding to queries for bid/ask price and OHLCV
|
|
||||||
|
|
||||||
The Matcher responding successfully to price and OHLCV requests is the signal to
|
|
||||||
the investors that trades submitted after that point will be analyzed. <!--This
|
|
||||||
is not ideal, and instead investors should be able to submit trades at any time,
|
|
||||||
and the Matcher could come and go without missing a trade. One way to achieve
|
|
||||||
this is for the Matcher to read the current state of all accounts looking for all
|
|
||||||
open orders.-->
|
|
||||||
|
|
||||||
Investors will initially query the exchange to discover their current balance
|
|
||||||
for each type of token. If the investor does not already have an account for
|
|
||||||
each type of token, they will submit account requests. Matcher as well will
|
|
||||||
request accounts to hold the tokens they earn by initiating trade swaps.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Supported token types
|
|
||||||
pub enum Token {
|
|
||||||
A,
|
|
||||||
B,
|
|
||||||
C,
|
|
||||||
D,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Supported token pairs
|
|
||||||
pub enum TokenPair {
|
|
||||||
AB,
|
|
||||||
AC,
|
|
||||||
AD,
|
|
||||||
BC,
|
|
||||||
BD,
|
|
||||||
CD,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// New token account
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - New token account
|
|
||||||
AccountRequest,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Token accounts are populated with this structure
|
|
||||||
pub struct TokenAccountInfo {
|
|
||||||
/// Investor who owns this account
|
|
||||||
pub owner: Pubkey,
|
|
||||||
/// Current number of tokens this account holds
|
|
||||||
pub tokens: Tokens,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For this demo investors or Matcher can request more tokens from the exchange at
|
|
||||||
any time by submitting token requests. In non-demos, an exchange of this type
|
|
||||||
would provide another way to exchange a 3rd party asset into tokens.
|
|
||||||
|
|
||||||
To request tokens, investors submit transfer requests:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// 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 Requests
|
|
||||||
|
|
||||||
When an investor decides to exchange a token of one type for another, they
|
|
||||||
submit a transaction to the Solana Blockchain containing an order request, which,
|
|
||||||
if successful, is turned into an order. orders do not expire but are
|
|
||||||
cancellable. <!-- orders should have a timestamp to enable trade
|
|
||||||
expiration --> When an order is created, tokens are deducted from a token
|
|
||||||
account and the order acts as an escrow. The tokens are held until the
|
|
||||||
order is fulfilled or canceled. If the direction is `To`, then the number
|
|
||||||
of `tokens` are deducted from the primary account, if `From` then `tokens`
|
|
||||||
multiplied by `price` are deducted from the secondary account. orders are
|
|
||||||
no longer valid when the number of `tokens` goes to zero, at which point they
|
|
||||||
can no longer be used. <!-- Could support refilling orders, so order
|
|
||||||
accounts are refilled rather than accumulating -->
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Direction of the exchange between two tokens in a pair
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct OrderRequestInfo {
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// order request
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - Account in which to record the swap
|
|
||||||
/// key 2 - Token account associated with this trade
|
|
||||||
TradeRequest(TradeRequestInfo),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trade accounts are populated with this structure
|
|
||||||
pub struct TradeOrderInfo {
|
|
||||||
/// Owner of the 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 canceled.
|
|
||||||
pub src_account: Pubkey,
|
|
||||||
/// account which the tokens the tokens will be deposited into on a successful trade
|
|
||||||
pub dst_account: Pubkey,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Order cancellations
|
|
||||||
|
|
||||||
An investor may cancel a trade at anytime, but only trades they own. If the
|
|
||||||
cancellation is successful, any tokens held in escrow are returned to the
|
|
||||||
account from which they came.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// order cancellation
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 -order to cancel
|
|
||||||
TradeCancellation,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Trade swaps
|
|
||||||
|
|
||||||
The Matcher is monitoring the accounts assigned to the exchange program and
|
|
||||||
building a trade-order table. The order table is used to identify
|
|
||||||
matching orders which could be fulfilled. When a match is found the
|
|
||||||
Matcher should issue a swap request. Swap requests may not satisfy the entirety
|
|
||||||
of either order, but the exchange will greedily fulfill it. Any leftover tokens
|
|
||||||
in either account will keep the order valid for further swap requests in
|
|
||||||
the future.
|
|
||||||
|
|
||||||
Matching orders are defined by the following swap requirements:
|
|
||||||
|
|
||||||
- Opposite polarity (one `To` and one `From`)
|
|
||||||
- Operate on the same token pair
|
|
||||||
- The price ratio of the `From` order is greater than or equal to the `To` order
|
|
||||||
- There are sufficient tokens to perform the trade
|
|
||||||
|
|
||||||
Orders can be written in the following format:
|
|
||||||
|
|
||||||
`investor direction pair quantity price-ratio`
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
- `1 T AB 2 1`
|
|
||||||
- Investor 1 wishes to exchange 2 A tokens to B tokens at a ratio of 1 A to 1
|
|
||||||
B
|
|
||||||
- `2 F AC 6 1.2`
|
|
||||||
- Investor 2 wishes to exchange A tokens from 6 B tokens at a ratio of 1 A
|
|
||||||
from 1.2 B
|
|
||||||
|
|
||||||
An order table could look something like the following. Notice how the columns
|
|
||||||
are sorted low to high and high to low, respectively. Prices are dramatic and
|
|
||||||
whole for clarity.
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 2 4 | 2 F AB 2 8 |
|
|
||||||
| 2 | 1 T AB 1 4 | 2 F AB 2 8 |
|
|
||||||
| 3 | 1 T AB 6 6 | 2 F AB 2 7 |
|
|
||||||
| 4 | 1 T AB 2 8 | 2 F AB 3 6 |
|
|
||||||
| 5 | 1 T AB 2 10 | 2 F AB 1 5 |
|
|
||||||
|
|
||||||
As part of a successful swap request, the exchange will credit tokens to the
|
|
||||||
Matcher's account equal to the difference in the price ratios or the two orders.
|
|
||||||
These tokens are considered the Matcher's profit for initiating the trade.
|
|
||||||
|
|
||||||
The Matcher would initiate the following swap on the order table above:
|
|
||||||
|
|
||||||
- Row 1, To: Investor 1 trades 2 A tokens to 8 B tokens
|
|
||||||
- Row 1, From: Investor 2 trades 2 A tokens from 8 B tokens
|
|
||||||
- Matcher takes 8 B tokens as profit
|
|
||||||
|
|
||||||
Both row 1 trades are fully realized, table becomes:
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 1 4 | 2 F AB 2 8 |
|
|
||||||
| 2 | 1 T AB 6 6 | 2 F AB 2 7 |
|
|
||||||
| 3 | 1 T AB 2 8 | 2 F AB 3 6 |
|
|
||||||
| 4 | 1 T AB 2 10 | 2 F AB 1 5 |
|
|
||||||
|
|
||||||
The Matcher would initiate the following swap:
|
|
||||||
|
|
||||||
- Row 1, To: Investor 1 trades 1 A token to 4 B tokens
|
|
||||||
- Row 1, From: Investor 2 trades 1 A token from 4 B tokens
|
|
||||||
- Matcher takes 4 B tokens as profit
|
|
||||||
|
|
||||||
Row 1 From is not fully realized, table becomes:
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 6 6 | 2 F AB 1 8 |
|
|
||||||
| 2 | 1 T AB 2 8 | 2 F AB 2 7 |
|
|
||||||
| 3 | 1 T AB 2 10 | 2 F AB 3 6 |
|
|
||||||
| 4 | | 2 F AB 1 5 |
|
|
||||||
|
|
||||||
The Matcher would initiate the following swap:
|
|
||||||
|
|
||||||
- Row 1, To: Investor 1 trades 1 A token to 6 B tokens
|
|
||||||
- Row 1, From: Investor 2 trades 1 A token from 6 B tokens
|
|
||||||
- Matcher takes 2 B tokens as profit
|
|
||||||
|
|
||||||
Row 1 To is now fully realized, table becomes:
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 5 6 | 2 F AB 2 7 |
|
|
||||||
| 2 | 1 T AB 2 8 | 2 F AB 3 5 |
|
|
||||||
| 3 | 1 T AB 2 10 | 2 F AB 1 5 |
|
|
||||||
|
|
||||||
The Matcher would initiate the following last swap:
|
|
||||||
|
|
||||||
- Row 1, To: Investor 1 trades 2 A token to 12 B tokens
|
|
||||||
- Row 1, From: Investor 2 trades 2 A token from 12 B tokens
|
|
||||||
- Matcher takes 2 B tokens as profit
|
|
||||||
|
|
||||||
Table becomes:
|
|
||||||
|
|
||||||
|Row| To | From |
|
|
||||||
|---|-------------|------------|
|
|
||||||
| 1 | 1 T AB 3 6 | 2 F AB 3 5 |
|
|
||||||
| 2 | 1 T AB 2 8 | 2 F AB 1 5 |
|
|
||||||
| 3 | 1 T AB 2 10 | |
|
|
||||||
|
|
||||||
At this point the lowest To's price is larger than the largest From's price so
|
|
||||||
no more swaps would be initiated until new orders came in.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub enum ExchangeInstruction {
|
|
||||||
/// Trade swap request
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - Account in which to record the swap
|
|
||||||
/// key 2 - 'To' order
|
|
||||||
/// key 3 - `From` 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 Matcher profit from the swap.
|
|
||||||
SwapRequest,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Swap accounts are populated with this structure
|
|
||||||
pub struct TradeSwapInfo {
|
|
||||||
/// Pair swapped
|
|
||||||
pub pair: TokenPair,
|
|
||||||
/// `To` order
|
|
||||||
pub to_trade_order: Pubkey,
|
|
||||||
/// `From` 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,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Exchange program operations
|
|
||||||
|
|
||||||
Putting all the commands together from above, the following operations will be
|
|
||||||
supported by the on-chain exchange program:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
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 swap
|
|
||||||
/// key 2 - Token account associated with this trade
|
|
||||||
TradeRequest(TradeRequestInfo),
|
|
||||||
|
|
||||||
/// order cancellation
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 -order to cancel
|
|
||||||
TradeCancellation,
|
|
||||||
|
|
||||||
/// Trade swap request
|
|
||||||
/// key 0 - Signer
|
|
||||||
/// key 1 - Account in which to record the swap
|
|
||||||
/// key 2 - 'To' order
|
|
||||||
/// key 3 - `From` 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 Matcher profit from the swap.
|
|
||||||
SwapRequest,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quotes and OHLCV
|
|
||||||
|
|
||||||
The Matcher will provide current bid/ask price quotes based on trade actively and
|
|
||||||
also provide OHLCV based on some time window. The details of how the bid/ask
|
|
||||||
price quotes are calculated are yet to be decided.
|
|
||||||
|
|
||||||
## Investor strategies
|
|
||||||
|
|
||||||
To make a compelling demo, the investors needs to provide interesting trade
|
|
||||||
behavior. Something as simple as a randomly twiddled baseline would be a
|
|
||||||
minimum starting point.
|
|
||||||
|
|
||||||
## Running the exchange
|
|
||||||
|
|
||||||
The exchange bench posts trades and swaps matches as fast as it can.
|
|
||||||
|
|
||||||
You might want to bump the duration up
|
|
||||||
to 60 seconds and the batch size to 1000 for better numbers. You can modify those
|
|
||||||
in client_demo/src/demo.rs::test_exchange_local_cluster.
|
|
||||||
|
|
||||||
The following command runs the bench:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ RUST_LOG=solana_bench_exchange=info cargo test --release -- --nocapture test_exchange_local_cluster
|
|
||||||
```
|
|
||||||
|
|
||||||
To also see the cluster messages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ RUST_LOG=solana_bench_exchange=info,solana=info cargo test --release -- --nocapture test_exchange_local_cluster
|
|
||||||
```
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,221 +0,0 @@
|
||||||
use clap::{crate_description, crate_name, value_t, App, Arg, ArgMatches};
|
|
||||||
use solana_core::gen_keys::GenKeys;
|
|
||||||
use solana_faucet::faucet::FAUCET_PORT;
|
|
||||||
use solana_sdk::signature::{read_keypair_file, Keypair};
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub struct Config {
|
|
||||||
pub entrypoint_addr: SocketAddr,
|
|
||||||
pub faucet_addr: SocketAddr,
|
|
||||||
pub identity: Keypair,
|
|
||||||
pub threads: usize,
|
|
||||||
pub num_nodes: usize,
|
|
||||||
pub duration: Duration,
|
|
||||||
pub transfer_delay: u64,
|
|
||||||
pub fund_amount: u64,
|
|
||||||
pub batch_size: usize,
|
|
||||||
pub chunk_size: usize,
|
|
||||||
pub account_groups: usize,
|
|
||||||
pub client_ids_and_stake_file: String,
|
|
||||||
pub write_to_client_file: bool,
|
|
||||||
pub read_from_client_file: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
|
||||||
faucet_addr: SocketAddr::from(([127, 0, 0, 1], FAUCET_PORT)),
|
|
||||||
identity: Keypair::new(),
|
|
||||||
num_nodes: 1,
|
|
||||||
threads: 4,
|
|
||||||
duration: Duration::new(u64::max_value(), 0),
|
|
||||||
transfer_delay: 0,
|
|
||||||
fund_amount: 100_000,
|
|
||||||
batch_size: 100,
|
|
||||||
chunk_size: 100,
|
|
||||||
account_groups: 100,
|
|
||||||
client_ids_and_stake_file: String::new(),
|
|
||||||
write_to_client_file: false,
|
|
||||||
read_from_client_file: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
|
||||||
App::new(crate_name!())
|
|
||||||
.about(crate_description!())
|
|
||||||
.version(version)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("entrypoint")
|
|
||||||
.short("n")
|
|
||||||
.long("entrypoint")
|
|
||||||
.value_name("HOST:PORT")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("127.0.0.1:8001")
|
|
||||||
.help("Cluster entry point; defaults to 127.0.0.1:8001"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("faucet")
|
|
||||||
.short("d")
|
|
||||||
.long("faucet")
|
|
||||||
.value_name("HOST:PORT")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("127.0.0.1:9900")
|
|
||||||
.help("Location of the faucet; defaults to 127.0.0.1:9900"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("identity")
|
|
||||||
.short("i")
|
|
||||||
.long("identity")
|
|
||||||
.value_name("PATH")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("File containing a client identity (keypair)"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("threads")
|
|
||||||
.long("threads")
|
|
||||||
.value_name("<threads>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("1")
|
|
||||||
.help("Number of threads submitting transactions"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("num-nodes")
|
|
||||||
.long("num-nodes")
|
|
||||||
.value_name("NUM")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("1")
|
|
||||||
.help("Wait for NUM nodes to converge"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("duration")
|
|
||||||
.long("duration")
|
|
||||||
.value_name("SECS")
|
|
||||||
.takes_value(true)
|
|
||||||
.default_value("60")
|
|
||||||
.help("Seconds to run benchmark, then exit; default is forever"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("transfer-delay")
|
|
||||||
.long("transfer-delay")
|
|
||||||
.value_name("<delay>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("0")
|
|
||||||
.help("Delay between each chunk"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("fund-amount")
|
|
||||||
.long("fund-amount")
|
|
||||||
.value_name("<fund>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("100000")
|
|
||||||
.help("Number of lamports to fund to each signer"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("batch-size")
|
|
||||||
.long("batch-size")
|
|
||||||
.value_name("<batch>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("1000")
|
|
||||||
.help("Number of transactions before the signer rolls over"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("chunk-size")
|
|
||||||
.long("chunk-size")
|
|
||||||
.value_name("<cunk>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("500")
|
|
||||||
.help("Number of transactions to generate and send at a time"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("account-groups")
|
|
||||||
.long("account-groups")
|
|
||||||
.value_name("<groups>")
|
|
||||||
.takes_value(true)
|
|
||||||
.required(false)
|
|
||||||
.default_value("10")
|
|
||||||
.help("Number of account groups to cycle for each batch"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("write-client-keys")
|
|
||||||
.long("write-client-keys")
|
|
||||||
.value_name("FILENAME")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Generate client keys and stakes and write the list to YAML file"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("read-client-keys")
|
|
||||||
.long("read-client-keys")
|
|
||||||
.value_name("FILENAME")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Read client keys and stakes from the YAML file"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::field_reassign_with_default)]
|
|
||||||
pub fn extract_args(matches: &ArgMatches) -> Config {
|
|
||||||
let mut args = Config::default();
|
|
||||||
|
|
||||||
args.entrypoint_addr = solana_net_utils::parse_host_port(
|
|
||||||
matches.value_of("entrypoint").unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
eprintln!("failed to parse entrypoint address: {}", e);
|
|
||||||
exit(1)
|
|
||||||
});
|
|
||||||
|
|
||||||
args.faucet_addr = solana_net_utils::parse_host_port(matches.value_of("faucet").unwrap())
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
eprintln!("failed to parse faucet address: {}", e);
|
|
||||||
exit(1)
|
|
||||||
});
|
|
||||||
|
|
||||||
if matches.is_present("identity") {
|
|
||||||
args.identity = read_keypair_file(matches.value_of("identity").unwrap())
|
|
||||||
.expect("can't read client identity");
|
|
||||||
} else {
|
|
||||||
args.identity = {
|
|
||||||
let seed = [42_u8; 32];
|
|
||||||
let mut rnd = GenKeys::new(seed);
|
|
||||||
rnd.gen_keypair()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
args.threads = value_t!(matches.value_of("threads"), usize).expect("Failed to parse threads");
|
|
||||||
args.num_nodes =
|
|
||||||
value_t!(matches.value_of("num-nodes"), usize).expect("Failed to parse num-nodes");
|
|
||||||
let duration = value_t!(matches.value_of("duration"), u64).expect("Failed to parse duration");
|
|
||||||
args.duration = Duration::from_secs(duration);
|
|
||||||
args.transfer_delay =
|
|
||||||
value_t!(matches.value_of("transfer-delay"), u64).expect("Failed to parse transfer-delay");
|
|
||||||
args.fund_amount =
|
|
||||||
value_t!(matches.value_of("fund-amount"), u64).expect("Failed to parse fund-amount");
|
|
||||||
args.batch_size =
|
|
||||||
value_t!(matches.value_of("batch-size"), usize).expect("Failed to parse batch-size");
|
|
||||||
args.chunk_size =
|
|
||||||
value_t!(matches.value_of("chunk-size"), usize).expect("Failed to parse chunk-size");
|
|
||||||
args.account_groups = value_t!(matches.value_of("account-groups"), usize)
|
|
||||||
.expect("Failed to parse account-groups");
|
|
||||||
|
|
||||||
if let Some(s) = matches.value_of("write-client-keys") {
|
|
||||||
args.write_to_client_file = true;
|
|
||||||
args.client_ids_and_stake_file = s.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(s) = matches.value_of("read-client-keys") {
|
|
||||||
assert!(!args.write_to_client_file);
|
|
||||||
args.read_from_client_file = true;
|
|
||||||
args.client_ids_and_stake_file = s.to_string();
|
|
||||||
}
|
|
||||||
args
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod bench;
|
|
||||||
pub mod cli;
|
|
||||||
mod order_book;
|
|
|
@ -1,83 +0,0 @@
|
||||||
#![allow(clippy::integer_arithmetic)]
|
|
||||||
pub mod bench;
|
|
||||||
mod cli;
|
|
||||||
pub mod order_book;
|
|
||||||
|
|
||||||
use crate::bench::{airdrop_lamports, create_client_accounts_file, do_bench_exchange, Config};
|
|
||||||
use log::*;
|
|
||||||
use solana_gossip::gossip_service::{discover_cluster, get_multi_client};
|
|
||||||
use solana_sdk::signature::Signer;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
solana_logger::setup();
|
|
||||||
solana_metrics::set_panic_hook("bench-exchange");
|
|
||||||
|
|
||||||
let matches = cli::build_args(solana_version::version!()).get_matches();
|
|
||||||
let cli_config = cli::extract_args(&matches);
|
|
||||||
|
|
||||||
let cli::Config {
|
|
||||||
entrypoint_addr,
|
|
||||||
faucet_addr,
|
|
||||||
identity,
|
|
||||||
threads,
|
|
||||||
num_nodes,
|
|
||||||
duration,
|
|
||||||
transfer_delay,
|
|
||||||
fund_amount,
|
|
||||||
batch_size,
|
|
||||||
chunk_size,
|
|
||||||
account_groups,
|
|
||||||
client_ids_and_stake_file,
|
|
||||||
write_to_client_file,
|
|
||||||
read_from_client_file,
|
|
||||||
..
|
|
||||||
} = cli_config;
|
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
identity,
|
|
||||||
threads,
|
|
||||||
duration,
|
|
||||||
transfer_delay,
|
|
||||||
fund_amount,
|
|
||||||
batch_size,
|
|
||||||
chunk_size,
|
|
||||||
account_groups,
|
|
||||||
client_ids_and_stake_file,
|
|
||||||
read_from_client_file,
|
|
||||||
};
|
|
||||||
|
|
||||||
if write_to_client_file {
|
|
||||||
create_client_accounts_file(
|
|
||||||
&config.client_ids_and_stake_file,
|
|
||||||
config.batch_size,
|
|
||||||
config.account_groups,
|
|
||||||
config.fund_amount,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
info!("Connecting to the cluster");
|
|
||||||
let nodes = discover_cluster(&entrypoint_addr, num_nodes).unwrap_or_else(|_| {
|
|
||||||
panic!("Failed to discover nodes");
|
|
||||||
});
|
|
||||||
|
|
||||||
let (client, num_clients) = get_multi_client(&nodes);
|
|
||||||
|
|
||||||
info!("{} nodes found", num_clients);
|
|
||||||
if num_clients < num_nodes {
|
|
||||||
panic!("Error: Insufficient nodes discovered");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !read_from_client_file {
|
|
||||||
info!("Funding keypair: {}", config.identity.pubkey());
|
|
||||||
|
|
||||||
let accounts_in_groups = batch_size * account_groups;
|
|
||||||
const NUM_SIGNERS: u64 = 2;
|
|
||||||
airdrop_lamports(
|
|
||||||
&client,
|
|
||||||
&faucet_addr,
|
|
||||||
&config.identity,
|
|
||||||
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
do_bench_exchange(vec![client], config);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
use itertools::EitherOrBoth::{Both, Left, Right};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use log::*;
|
|
||||||
use solana_exchange_program::exchange_state::*;
|
|
||||||
use solana_sdk::pubkey::Pubkey;
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
use std::collections::BinaryHeap;
|
|
||||||
use std::{error, fmt};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct ToOrder {
|
|
||||||
pub pubkey: Pubkey,
|
|
||||||
pub info: OrderInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for ToOrder {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
other.info.price.cmp(&self.info.price)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialOrd for ToOrder {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct FromOrder {
|
|
||||||
pub pubkey: Pubkey,
|
|
||||||
pub info: OrderInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for FromOrder {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
self.info.price.cmp(&other.info.price)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl PartialOrd for FromOrder {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct OrderBook {
|
|
||||||
// TODO scale to x token types
|
|
||||||
to_ab: BinaryHeap<ToOrder>,
|
|
||||||
from_ab: BinaryHeap<FromOrder>,
|
|
||||||
}
|
|
||||||
impl fmt::Display for OrderBook {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"+-Order Book--------------------------+-------------------------------------+"
|
|
||||||
)?;
|
|
||||||
for (i, it) in self
|
|
||||||
.to_ab
|
|
||||||
.iter()
|
|
||||||
.zip_longest(self.from_ab.iter())
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
match it {
|
|
||||||
Both(to, from) => writeln!(
|
|
||||||
f,
|
|
||||||
"| T AB {:8} for {:8}/{:8} | F AB {:8} for {:8}/{:8} |{}",
|
|
||||||
to.info.tokens,
|
|
||||||
SCALER,
|
|
||||||
to.info.price,
|
|
||||||
from.info.tokens,
|
|
||||||
SCALER,
|
|
||||||
from.info.price,
|
|
||||||
i
|
|
||||||
)?,
|
|
||||||
Left(to) => writeln!(
|
|
||||||
f,
|
|
||||||
"| T AB {:8} for {:8}/{:8} | |{}",
|
|
||||||
to.info.tokens, SCALER, to.info.price, i
|
|
||||||
)?,
|
|
||||||
Right(from) => writeln!(
|
|
||||||
f,
|
|
||||||
"| | F AB {:8} for {:8}/{:8} |{}",
|
|
||||||
from.info.tokens, SCALER, from.info.price, i
|
|
||||||
)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"+-------------------------------------+-------------------------------------+"
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OrderBook {
|
|
||||||
// TODO
|
|
||||||
// pub fn cancel(&mut self, pubkey: Pubkey) -> Result<(), Box<dyn error::Error>> {
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
pub fn push(&mut self, pubkey: Pubkey, info: OrderInfo) -> Result<(), Box<dyn error::Error>> {
|
|
||||||
check_trade(info.side, info.tokens, info.price)?;
|
|
||||||
match info.side {
|
|
||||||
OrderSide::Ask => {
|
|
||||||
self.to_ab.push(ToOrder { pubkey, info });
|
|
||||||
}
|
|
||||||
OrderSide::Bid => {
|
|
||||||
self.from_ab.push(FromOrder { pubkey, info });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn pop(&mut self) -> Option<(ToOrder, FromOrder)> {
|
|
||||||
if let Some(pair) = Self::pop_pair(&mut self.to_ab, &mut self.from_ab) {
|
|
||||||
return Some(pair);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pub fn get_num_outstanding(&self) -> (usize, usize) {
|
|
||||||
(self.to_ab.len(), self.from_ab.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pop_pair(
|
|
||||||
to_ab: &mut BinaryHeap<ToOrder>,
|
|
||||||
from_ab: &mut BinaryHeap<FromOrder>,
|
|
||||||
) -> Option<(ToOrder, FromOrder)> {
|
|
||||||
let to = to_ab.peek()?;
|
|
||||||
let from = from_ab.peek()?;
|
|
||||||
if from.info.price < to.info.price {
|
|
||||||
debug!("Trade not viable");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let to = to_ab.pop()?;
|
|
||||||
let from = from_ab.pop()?;
|
|
||||||
Some((to, from))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
use log::*;
|
|
||||||
use solana_bench_exchange::bench::{airdrop_lamports, do_bench_exchange, Config};
|
|
||||||
use solana_core::validator::ValidatorConfig;
|
|
||||||
use solana_exchange_program::{
|
|
||||||
exchange_processor::process_instruction, id, solana_exchange_program,
|
|
||||||
};
|
|
||||||
use solana_faucet::faucet::run_local_faucet_with_port;
|
|
||||||
use solana_gossip::gossip_service::{discover_cluster, get_multi_client};
|
|
||||||
use solana_local_cluster::{
|
|
||||||
local_cluster::{ClusterConfig, LocalCluster},
|
|
||||||
validator_configs::make_identical_validator_configs,
|
|
||||||
};
|
|
||||||
use solana_runtime::{bank::Bank, bank_client::BankClient};
|
|
||||||
use solana_sdk::{
|
|
||||||
genesis_config::create_genesis_config,
|
|
||||||
signature::{Keypair, Signer},
|
|
||||||
};
|
|
||||||
use std::{process::exit, sync::mpsc::channel, time::Duration};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn test_exchange_local_cluster() {
|
|
||||||
solana_logger::setup();
|
|
||||||
|
|
||||||
const NUM_NODES: usize = 1;
|
|
||||||
|
|
||||||
let config = Config {
|
|
||||||
identity: Keypair::new(),
|
|
||||||
duration: Duration::from_secs(1),
|
|
||||||
fund_amount: 100_000,
|
|
||||||
threads: 1,
|
|
||||||
transfer_delay: 20, // 15
|
|
||||||
batch_size: 100, // 1000
|
|
||||||
chunk_size: 10, // 200
|
|
||||||
account_groups: 1, // 10
|
|
||||||
..Config::default()
|
|
||||||
};
|
|
||||||
let Config {
|
|
||||||
fund_amount,
|
|
||||||
batch_size,
|
|
||||||
account_groups,
|
|
||||||
..
|
|
||||||
} = config;
|
|
||||||
let accounts_in_groups = batch_size * account_groups;
|
|
||||||
|
|
||||||
let cluster = LocalCluster::new(&mut ClusterConfig {
|
|
||||||
node_stakes: vec![100_000; NUM_NODES],
|
|
||||||
cluster_lamports: 100_000_000_000_000,
|
|
||||||
validator_configs: make_identical_validator_configs(&ValidatorConfig::default(), NUM_NODES),
|
|
||||||
native_instruction_processors: [solana_exchange_program!()].to_vec(),
|
|
||||||
..ClusterConfig::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
let faucet_keypair = Keypair::new();
|
|
||||||
cluster.transfer(
|
|
||||||
&cluster.funding_keypair,
|
|
||||||
&faucet_keypair.pubkey(),
|
|
||||||
2_000_000_000_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
let (addr_sender, addr_receiver) = channel();
|
|
||||||
run_local_faucet_with_port(faucet_keypair, addr_sender, Some(1_000_000_000_000), 0);
|
|
||||||
let faucet_addr = addr_receiver
|
|
||||||
.recv_timeout(Duration::from_secs(2))
|
|
||||||
.expect("run_local_faucet")
|
|
||||||
.expect("faucet_addr");
|
|
||||||
|
|
||||||
info!("Connecting to the cluster");
|
|
||||||
let nodes =
|
|
||||||
discover_cluster(&cluster.entry_point_info.gossip, NUM_NODES).unwrap_or_else(|err| {
|
|
||||||
error!("Failed to discover {} nodes: {:?}", NUM_NODES, err);
|
|
||||||
exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
let (client, num_clients) = get_multi_client(&nodes);
|
|
||||||
|
|
||||||
info!("clients: {}", num_clients);
|
|
||||||
assert!(num_clients >= NUM_NODES);
|
|
||||||
|
|
||||||
const NUM_SIGNERS: u64 = 2;
|
|
||||||
airdrop_lamports(
|
|
||||||
&client,
|
|
||||||
&faucet_addr,
|
|
||||||
&config.identity,
|
|
||||||
fund_amount * (accounts_in_groups + 1) as u64 * NUM_SIGNERS,
|
|
||||||
);
|
|
||||||
|
|
||||||
do_bench_exchange(vec![client], config);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_exchange_bank_client() {
|
|
||||||
solana_logger::setup();
|
|
||||||
let (genesis_config, identity) = create_genesis_config(100_000_000_000_000);
|
|
||||||
let mut bank = Bank::new(&genesis_config);
|
|
||||||
bank.add_builtin("exchange_program", id(), process_instruction);
|
|
||||||
let clients = vec![BankClient::new(bank)];
|
|
||||||
|
|
||||||
do_bench_exchange(
|
|
||||||
clients,
|
|
||||||
Config {
|
|
||||||
identity,
|
|
||||||
duration: Duration::from_secs(1),
|
|
||||||
fund_amount: 100_000,
|
|
||||||
threads: 1,
|
|
||||||
transfer_delay: 20, // 0;
|
|
||||||
batch_size: 100, // 1500;
|
|
||||||
chunk_size: 10, // 1500;
|
|
||||||
account_groups: 1, // 50;
|
|
||||||
..Config::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -18,7 +18,6 @@ serde_json = "1.0.64"
|
||||||
serde_yaml = "0.8.17"
|
serde_yaml = "0.8.17"
|
||||||
solana-clap-utils = { path = "../clap-utils", version = "=1.8.0" }
|
solana-clap-utils = { path = "../clap-utils", version = "=1.8.0" }
|
||||||
solana-cli-config = { path = "../cli-config", version = "=1.8.0" }
|
solana-cli-config = { path = "../cli-config", version = "=1.8.0" }
|
||||||
solana-exchange-program = { path = "../programs/exchange", version = "=1.8.0" }
|
|
||||||
solana-ledger = { path = "../ledger", version = "=1.8.0" }
|
solana-ledger = { path = "../ledger", version = "=1.8.0" }
|
||||||
solana-logger = { path = "../logger", version = "=1.8.0" }
|
solana-logger = { path = "../logger", version = "=1.8.0" }
|
||||||
solana-runtime = { path = "../runtime", version = "=1.8.0" }
|
solana-runtime = { path = "../runtime", version = "=1.8.0" }
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
//! A command-line executable for generating the chain's genesis config.
|
//! A command-line executable for generating the chain's genesis config.
|
||||||
#![allow(clippy::integer_arithmetic)]
|
#![allow(clippy::integer_arithmetic)]
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
extern crate solana_exchange_program;
|
|
||||||
|
|
||||||
use clap::{crate_description, crate_name, value_t, value_t_or_exit, App, Arg, ArgMatches};
|
use clap::{crate_description, crate_name, value_t, value_t_or_exit, App, Arg, ArgMatches};
|
||||||
use solana_clap_utils::{
|
use solana_clap_utils::{
|
||||||
input_parsers::{cluster_type_of, pubkey_of, pubkeys_of, unix_timestamp_from_rfc3339_datetime},
|
input_parsers::{cluster_type_of, pubkey_of, pubkeys_of, unix_timestamp_from_rfc3339_datetime},
|
||||||
|
@ -490,14 +487,8 @@ fn main() -> Result<(), Box<dyn error::Error>> {
|
||||||
matches.is_present("enable_warmup_epochs"),
|
matches.is_present("enable_warmup_epochs"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let native_instruction_processors = if cluster_type == ClusterType::Development {
|
|
||||||
vec![solana_exchange_program!()]
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut genesis_config = GenesisConfig {
|
let mut genesis_config = GenesisConfig {
|
||||||
native_instruction_processors,
|
native_instruction_processors: vec![],
|
||||||
ticks_per_slot,
|
ticks_per_slot,
|
||||||
poh_config,
|
poh_config,
|
||||||
fee_rate_governor,
|
fee_rate_governor,
|
||||||
|
|
|
@ -21,7 +21,6 @@ solana-config-program = { path = "../programs/config", version = "=1.8.0" }
|
||||||
solana-core = { path = "../core", version = "=1.8.0" }
|
solana-core = { path = "../core", version = "=1.8.0" }
|
||||||
solana-client = { path = "../client", version = "=1.8.0" }
|
solana-client = { path = "../client", version = "=1.8.0" }
|
||||||
solana-download-utils = { path = "../download-utils", version = "=1.8.0" }
|
solana-download-utils = { path = "../download-utils", version = "=1.8.0" }
|
||||||
solana-exchange-program = { path = "../programs/exchange", version = "=1.8.0" }
|
|
||||||
solana-faucet = { path = "../faucet", version = "=1.8.0" }
|
solana-faucet = { path = "../faucet", version = "=1.8.0" }
|
||||||
solana-gossip = { path = "../gossip", version = "=1.8.0" }
|
solana-gossip = { path = "../gossip", version = "=1.8.0" }
|
||||||
solana-ledger = { path = "../ledger", version = "=1.8.0" }
|
solana-ledger = { path = "../ledger", version = "=1.8.0" }
|
||||||
|
|
15
net/net.sh
15
net/net.sh
|
@ -20,7 +20,6 @@ usage() {
|
||||||
Valid client types are:
|
Valid client types are:
|
||||||
idle
|
idle
|
||||||
bench-tps
|
bench-tps
|
||||||
bench-exchange
|
|
||||||
User can optionally provide extraArgs that are transparently
|
User can optionally provide extraArgs that are transparently
|
||||||
supplied to the client program as command line parameters.
|
supplied to the client program as command line parameters.
|
||||||
For example,
|
For example,
|
||||||
|
@ -307,7 +306,6 @@ startBootstrapLeader() {
|
||||||
\"$internalNodesLamports\" \
|
\"$internalNodesLamports\" \
|
||||||
$nodeIndex \
|
$nodeIndex \
|
||||||
${#clientIpList[@]} \"$benchTpsExtraArgs\" \
|
${#clientIpList[@]} \"$benchTpsExtraArgs\" \
|
||||||
${#clientIpList[@]} \"$benchExchangeExtraArgs\" \
|
|
||||||
\"$genesisOptions\" \
|
\"$genesisOptions\" \
|
||||||
\"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority\" \
|
\"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority\" \
|
||||||
\"$gpuMode\" \
|
\"$gpuMode\" \
|
||||||
|
@ -379,7 +377,6 @@ startNode() {
|
||||||
\"$internalNodesLamports\" \
|
\"$internalNodesLamports\" \
|
||||||
$nodeIndex \
|
$nodeIndex \
|
||||||
${#clientIpList[@]} \"$benchTpsExtraArgs\" \
|
${#clientIpList[@]} \"$benchTpsExtraArgs\" \
|
||||||
${#clientIpList[@]} \"$benchExchangeExtraArgs\" \
|
|
||||||
\"$genesisOptions\" \
|
\"$genesisOptions\" \
|
||||||
\"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority\" \
|
\"$maybeNoSnapshot $maybeSkipLedgerVerify $maybeLimitLedgerSize $maybeWaitForSupermajority\" \
|
||||||
\"$gpuMode\" \
|
\"$gpuMode\" \
|
||||||
|
@ -409,7 +406,7 @@ startClient() {
|
||||||
startCommon "$ipAddress"
|
startCommon "$ipAddress"
|
||||||
ssh "${sshOptions[@]}" -f "$ipAddress" \
|
ssh "${sshOptions[@]}" -f "$ipAddress" \
|
||||||
"./solana/net/remote/remote-client.sh $deployMethod $entrypointIp \
|
"./solana/net/remote/remote-client.sh $deployMethod $entrypointIp \
|
||||||
$clientToRun \"$RUST_LOG\" \"$benchTpsExtraArgs\" \"$benchExchangeExtraArgs\" $clientIndex"
|
$clientToRun \"$RUST_LOG\" \"$benchTpsExtraArgs\" $clientIndex"
|
||||||
) >> "$logFile" 2>&1 || {
|
) >> "$logFile" 2>&1 || {
|
||||||
cat "$logFile"
|
cat "$logFile"
|
||||||
echo "^^^ +++"
|
echo "^^^ +++"
|
||||||
|
@ -421,8 +418,6 @@ startClients() {
|
||||||
for ((i=0; i < "$numClients" && i < "$numClientsRequested"; i++)) do
|
for ((i=0; i < "$numClients" && i < "$numClientsRequested"; i++)) do
|
||||||
if [[ $i -lt "$numBenchTpsClients" ]]; then
|
if [[ $i -lt "$numBenchTpsClients" ]]; then
|
||||||
startClient "${clientIpList[$i]}" "solana-bench-tps" "$i"
|
startClient "${clientIpList[$i]}" "solana-bench-tps" "$i"
|
||||||
elif [[ $i -lt $((numBenchTpsClients + numBenchExchangeClients)) ]]; then
|
|
||||||
startClient "${clientIpList[$i]}" "solana-bench-exchange" $((i-numBenchTpsClients))
|
|
||||||
else
|
else
|
||||||
startClient "${clientIpList[$i]}" "idle"
|
startClient "${clientIpList[$i]}" "idle"
|
||||||
fi
|
fi
|
||||||
|
@ -767,9 +762,7 @@ updatePlatforms=
|
||||||
nodeAddress=
|
nodeAddress=
|
||||||
numIdleClients=0
|
numIdleClients=0
|
||||||
numBenchTpsClients=0
|
numBenchTpsClients=0
|
||||||
numBenchExchangeClients=0
|
|
||||||
benchTpsExtraArgs=
|
benchTpsExtraArgs=
|
||||||
benchExchangeExtraArgs=
|
|
||||||
failOnValidatorBootupFailure=true
|
failOnValidatorBootupFailure=true
|
||||||
genesisOptions=
|
genesisOptions=
|
||||||
numValidatorsRequested=
|
numValidatorsRequested=
|
||||||
|
@ -977,10 +970,6 @@ while getopts "h?T:t:o:f:rc:Fn:i:d" opt "${shortArgs[@]}"; do
|
||||||
numBenchTpsClients=$numClients
|
numBenchTpsClients=$numClients
|
||||||
benchTpsExtraArgs=$extraArgs
|
benchTpsExtraArgs=$extraArgs
|
||||||
;;
|
;;
|
||||||
bench-exchange)
|
|
||||||
numBenchExchangeClients=$numClients
|
|
||||||
benchExchangeExtraArgs=$extraArgs
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "Unknown client type: $clientType"
|
echo "Unknown client type: $clientType"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -1013,7 +1002,7 @@ if [[ -n $numValidatorsRequested ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
numClients=${#clientIpList[@]}
|
numClients=${#clientIpList[@]}
|
||||||
numClientsRequested=$((numBenchTpsClients + numBenchExchangeClients + numIdleClients))
|
numClientsRequested=$((numBenchTpsClients + numIdleClients))
|
||||||
if [[ "$numClientsRequested" -eq 0 ]]; then
|
if [[ "$numClientsRequested" -eq 0 ]]; then
|
||||||
numBenchTpsClients=$numClients
|
numBenchTpsClients=$numClients
|
||||||
numClientsRequested=$numClients
|
numClientsRequested=$numClients
|
||||||
|
|
|
@ -10,8 +10,7 @@ if [[ -n $4 ]]; then
|
||||||
export RUST_LOG="$4"
|
export RUST_LOG="$4"
|
||||||
fi
|
fi
|
||||||
benchTpsExtraArgs="$5"
|
benchTpsExtraArgs="$5"
|
||||||
benchExchangeExtraArgs="$6"
|
clientIndex="$6"
|
||||||
clientIndex="$7"
|
|
||||||
|
|
||||||
missing() {
|
missing() {
|
||||||
echo "Error: $1 not specified"
|
echo "Error: $1 not specified"
|
||||||
|
@ -57,23 +56,6 @@ solana-bench-tps)
|
||||||
--read-client-keys ./client-accounts.yml \
|
--read-client-keys ./client-accounts.yml \
|
||||||
"
|
"
|
||||||
;;
|
;;
|
||||||
solana-bench-exchange)
|
|
||||||
solana-keygen new --no-passphrase -fso bench.keypair
|
|
||||||
net/scripts/rsync-retry.sh -vPrc \
|
|
||||||
"$entrypointIp":~/solana/config/bench-exchange"$clientIndex".yml ./client-accounts.yml
|
|
||||||
clientCommand="\
|
|
||||||
solana-bench-exchange \
|
|
||||||
--entrypoint $entrypointIp:8001 \
|
|
||||||
--faucet $entrypointIp:9900 \
|
|
||||||
--threads $threadCount \
|
|
||||||
--batch-size 1000 \
|
|
||||||
--fund-amount 20000 \
|
|
||||||
--duration 7500 \
|
|
||||||
--identity bench.keypair \
|
|
||||||
$benchExchangeExtraArgs \
|
|
||||||
--read-client-keys ./client-accounts.yml \
|
|
||||||
"
|
|
||||||
;;
|
|
||||||
idle)
|
idle)
|
||||||
# Add the faucet keypair to idle clients for convenience
|
# Add the faucet keypair to idle clients for convenience
|
||||||
net/scripts/rsync-retry.sh -vPrc \
|
net/scripts/rsync-retry.sh -vPrc \
|
||||||
|
|
|
@ -20,15 +20,13 @@ internalNodesLamports="${11}"
|
||||||
nodeIndex="${12}"
|
nodeIndex="${12}"
|
||||||
numBenchTpsClients="${13}"
|
numBenchTpsClients="${13}"
|
||||||
benchTpsExtraArgs="${14}"
|
benchTpsExtraArgs="${14}"
|
||||||
numBenchExchangeClients="${15}"
|
genesisOptions="${15}"
|
||||||
benchExchangeExtraArgs="${16}"
|
extraNodeArgs="${16}"
|
||||||
genesisOptions="${17}"
|
gpuMode="${17:-auto}"
|
||||||
extraNodeArgs="${18}"
|
maybeWarpSlot="${18}"
|
||||||
gpuMode="${19:-auto}"
|
waitForNodeInit="${19}"
|
||||||
maybeWarpSlot="${20}"
|
extraPrimordialStakes="${20:=0}"
|
||||||
waitForNodeInit="${21}"
|
tmpfsAccounts="${21:false}"
|
||||||
extraPrimordialStakes="${22:=0}"
|
|
||||||
tmpfsAccounts="${23:false}"
|
|
||||||
set +x
|
set +x
|
||||||
|
|
||||||
missing() {
|
missing() {
|
||||||
|
@ -194,13 +192,6 @@ EOF
|
||||||
tail -n +2 -q config/bench-tps"$i".yml >> config/client-accounts.yml
|
tail -n +2 -q config/bench-tps"$i".yml >> config/client-accounts.yml
|
||||||
echo "" >> config/client-accounts.yml
|
echo "" >> config/client-accounts.yml
|
||||||
done
|
done
|
||||||
for i in $(seq 0 $((numBenchExchangeClients-1))); do
|
|
||||||
# shellcheck disable=SC2086 # Do not want to quote $benchExchangeExtraArgs
|
|
||||||
solana-bench-exchange --batch-size 1000 --fund-amount 20000 \
|
|
||||||
--write-client-keys config/bench-exchange"$i".yml $benchExchangeExtraArgs
|
|
||||||
tail -n +2 -q config/bench-exchange"$i".yml >> config/client-accounts.yml
|
|
||||||
echo "" >> config/client-accounts.yml
|
|
||||||
done
|
|
||||||
if [[ -f $externalPrimordialAccountsFile ]]; then
|
if [[ -f $externalPrimordialAccountsFile ]]; then
|
||||||
cat "$externalPrimordialAccountsFile" >> config/validator-balances.yml
|
cat "$externalPrimordialAccountsFile" >> config/validator-balances.yml
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -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"]
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
|
@ -84,7 +84,6 @@ else
|
||||||
|
|
||||||
BINS=(
|
BINS=(
|
||||||
solana
|
solana
|
||||||
solana-bench-exchange
|
|
||||||
solana-bench-tps
|
solana-bench-tps
|
||||||
solana-faucet
|
solana-faucet
|
||||||
solana-gossip
|
solana-gossip
|
||||||
|
|
|
@ -106,7 +106,6 @@ find target/cov -type f -name '*.gcda' -newer target/cov/before-test ! -newer ta
|
||||||
--ignore bench-tps\*
|
--ignore bench-tps\*
|
||||||
--ignore upload-perf\*
|
--ignore upload-perf\*
|
||||||
--ignore bench-streamer\*
|
--ignore bench-streamer\*
|
||||||
--ignore bench-exchange\*
|
|
||||||
--ignore local-cluster\*
|
--ignore local-cluster\*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue