├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── phoenix-cli-install.sh
└── src
├── command.rs
├── lib
├── helpers
│ ├── devnet_helpers.rs
│ ├── market_helpers.rs
│ ├── mod.rs
│ └── print_helpers.rs
├── lib.rs
└── processor
│ ├── mod.rs
│ ├── process_claim_seat.rs
│ ├── process_evict_seat.rs
│ ├── process_get_all_markets.rs
│ ├── process_get_book_levels.rs
│ ├── process_get_full_book.rs
│ ├── process_get_market.rs
│ ├── process_get_market_status.rs
│ ├── process_get_open_orders.rs
│ ├── process_get_seat_info.rs
│ ├── process_get_seat_manager_info.rs
│ ├── process_get_top_of_book.rs
│ ├── process_get_traders_for_market.rs
│ ├── process_get_transaction.rs
│ ├── process_mint_tokens.rs
│ ├── process_mint_tokens_for_market.rs
│ └── process_request_seat.rs
└── main.rs
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*' # Trigger when a commit is tagged with a new version. TODO: Use a filter pattern for major releases only.
7 |
8 | jobs:
9 | create_release:
10 | name: Create Release
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Create Release
14 | id: create_release
15 | uses: softprops/action-gh-release@v1
16 | with:
17 | name: ${{ github.ref_name }}
18 | draft: false
19 | prerelease: false
20 | generate_release_notes: true
21 |
22 | build_release:
23 | name: Build Release
24 | needs: create_release
25 | runs-on: ${{ matrix.os }}
26 | strategy:
27 | matrix:
28 | name: [
29 | linux,
30 | macos
31 | ]
32 |
33 | include:
34 | - name: linux
35 | os: ubuntu-latest
36 | artifact_path: target/release/phoenix-cli
37 | asset_name: phoenix-cli-linux
38 | - name: macos
39 | os: macos-latest
40 | artifact_path: target/release/phoenix-cli
41 | asset_name: phoenix-cli-macos
42 | steps:
43 | - name: Checkout code
44 | uses: actions/checkout@v1
45 |
46 | - name: Use Rust toolchain
47 | uses: actions-rs/toolchain@v1
48 | with:
49 | profile: minimal
50 | toolchain: stable
51 |
52 | - name: Build
53 | run: cargo build --release
54 |
55 | - name: Rename executable based on OS
56 | env:
57 | ASSET_NAME: ${{matrix.asset_name}}
58 | EXEC_PATH: ${{matrix.artifact_path}}
59 | run: |
60 | echo "asset name: ${ASSET_NAME} executable path: ${EXEC_PATH}"
61 | mv ${EXEC_PATH} ${ASSET_NAME}
62 |
63 | - name: Upload binaries to release
64 | uses: softprops/action-gh-release@v1
65 | with:
66 | files: ${{matrix.asset_name}}
67 | fail_on_unmatched_files: true
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 |
3 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
4 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
5 | Cargo.lock
6 |
7 | **/*.rs.bk
8 |
9 | registry.txt
10 |
11 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "phoenix-cli"
3 | version = "0.3.10"
4 | description = "CLI and associated library for interacting with the Phoenix program from the command line"
5 | edition = "2021"
6 | license = "MIT"
7 |
8 | [[bin]]
9 | name = "phoenix-cli"
10 | path = "src/main.rs"
11 |
12 | [lib]
13 | name = "phoenix_cli_processor"
14 | path = "src/lib/lib.rs"
15 |
16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
17 |
18 | [dependencies]
19 | anyhow = "1.0.66"
20 | clap = { version = "4.0.26", features = ["derive"] }
21 | shellexpand = "2.1.2"
22 | solana-sdk = "1.10.32"
23 | ellipsis-client = "1.0.1"
24 | solana-client = "1.10.32"
25 | solana-account-decoder = "1.14.7"
26 | solana-cli-config = "1.14.7"
27 | borsh = "0.9.3"
28 | tokio = { version = "1.8.4", features = ["full"] }
29 | rand = "0.7.3"
30 | itertools = "0.10.5"
31 | colored = "2.0.0"
32 | spl-token = { version = "3.2.0", features = ["no-entrypoint"] }
33 | serde = { version = "1.0", features = ["derive"] }
34 | serde_json = "1.0"
35 | spl-associated-token-account = { version = "2.2.0", features = [ "no-entrypoint" ] }
36 | phoenix-common = { version = "0.2.1", features = ["no-entrypoint"] }
37 | phoenix-sdk = "0.8.0"
38 | bytemuck = "1.13.0"
39 | reqwest = "0.11.14"
40 | bincode = "1.3.3"
41 | phoenix-seat-manager-common = "0.1.1"
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Ellipsis Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # phoenix-cli
2 | CLI for interacting with the Phoenix DEX
3 |
4 | ## Program Deployments
5 |
6 | | Program | Devnet |
7 | | ----------- | ---------------------------------------------- |
8 | | Phoenix Dex | `PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY` |
9 |
10 | | Program | Mainnet |
11 | | ----------- | ---------------------------------------------- |
12 | | Phoenix Dex | `PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY` |
13 |
14 | ## Installation
15 |
16 | You will need to install Cargo if you don't already have it.
17 |
18 | Run the following command in your shell to install it (or visit https://rustup.rs/):
19 | ```
20 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
21 | ```
22 |
23 | To install the Phoenix CLI, run the following in your shell:
24 |
25 | ```
26 | cargo install phoenix-cli
27 | ```
28 |
29 | ## Running the CLI
30 |
31 | To view a list of all available commands, run `phoenix-cli --help`
32 |
33 |
34 |
35 | To zoom in on a specific command, run `phoenix-cli --help`
36 |
37 | Optionally include the following parameters when running the cli:
38 | * `-u, --url` Include your RPC endpoint. Use "local", "dev", and "main" for the respective default endpoints. Defaults to your Solana CLI config settings - if the config isn't found, defaults to mainnet.
39 | * `-k, --keypair-path` Include the path to the keypair you wish to use. Defaults to your Solana CLI config settings - if the config isn't found, defaults to `.config/solana/id.json`
40 | * `-c, --commitment` Include a commitment level for the RPC. Defaults to your Solana CLI config settings - if the config isn't found, defaults to Confirmed
41 |
42 | ## Commands
43 |
44 |
45 | ### get-all-markets
46 | Returns summary information on all markets that exist on Phoenix. Summary information includes market key, base and quote token keys, and authority key. Recommended to use the no-gpa flag to read from a static config file and avoiding making an expensive network call.
47 |
48 | `$ phoenix-cli -u main get-all-markets --no-gpa`
49 | ```
50 | Found 2 market(s)
51 | --------------------------------------------
52 | Market: 14CAwu3LiBBk5fcHGdTsFyVxDwvpgFiSfDwgPJxECcE5
53 | Base Token: 7Z6Kczxo8ViRpfnsVvVaATB5fQ8bN2CQpxP8DHfd1vz5
54 | Quote Token: 5zUmtDCDeR17UYjvKKqvYp3S9pqcZA69cDoYPtojseJ4
55 | Authority: 9odqiJyK4zCMNfPi6AUE6gi9tomqZKPFYcDiokMXYRzS
56 | --------------------------------------------
57 | Market: 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg
58 | Base Token: So11111111111111111111111111111111111111112
59 | Quote Token: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
60 | Authority: 9odqiJyK4zCMNfPi6AUE6gi9tomqZKPFYcDiokMXYRzS
61 | ```
62 |
63 | ### get-market
64 | Returns detailed information on a specific market. Information includes market balance's of the base and quote tokens, base and quote token keys, base lot size, quote lot size, tick size, and taker fees in basis points.
65 |
66 | `$ phoenix-cli -u main get-market 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg`
67 | ```
68 | Market: 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg
69 | Status: Active
70 | Authority: 9odqiJyK4zCMNfPi6AUE6gi9tomqZKPFYcDiokMXYRzS
71 | Sequence number: 696709
72 | Base Vault balance: 0.000
73 | Quote Vault balance: 10.485
74 | Base Token: So11111111111111111111111111111111111111112
75 | Quote Token: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
76 | Base vault key: 8g4Z9d6PqGkgH31tMW6FwxGhwYJrXpxZHQrkikpLJKrG
77 | Quote vault key: 3HSYXeGc3LjEPCuzoNDjQN37F1ebsSiR4CqXVqQCdekZ
78 | Base Lot Size, in whole units: 0.001
79 | Quote Lot Size, in whole units: 0.000001
80 | Tick size in quote atoms per base unit: 0.001
81 | Taker fees in basis points: 2
82 | Fee destination pubkey: 6pwvUFHxtwNrcMqb12V3ni2FXcMnvTWvBWX5DXmPpg1Y
83 | Raw base units per base unit: 1
84 | Market Size Params: MarketSizeParams { bids_size: 4096, asks_size: 4096, num_seats: 8321 }
85 | Successor pubkey: 9odqiJyK4zCMNfPi6AUE6gi9tomqZKPFYcDiokMXYRzS
86 | Uncollected fees, in quote units: 10.48482
87 | Collected fees, in quote units: 0.0
88 | ```
89 |
90 | ### get-traders-for-market
91 | Returns all trader keys that have an approved seat on a given market.
92 |
93 | `$ phoenix-cli -u main get-traders-for-market 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg`
94 | ```
95 | Found 3 trader(s). Printing traders with locked or free lots
96 | --------------------------------
97 | Trader pubkey: 3HBWHuyxWv4uN8U8SeukocrWPfLZJqrtj9DgDHsGo2HR
98 | Base token locked: 116.873
99 | Base token free: 6.666
100 | Quote token locked: 2647.022716
101 | Quote token free: 1222.250847
102 | ```
103 |
104 | ### get-top-of-book
105 | Returns the best bid and best ask on a given market.
106 |
107 | `$ phoenix-cli -u main get-top-of-book 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg`
108 | ```
109 | 22.990 5.838
110 | 5.843 22.980
111 | ```
112 |
113 | ### get-book-levels
114 | Returns the top N levels of a market's orderbook. N is by default set to 10.
115 |
116 | `$ phoenix-cli -u main get-book-levels 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg -l 5`
117 | ```
118 | 23.030 109.725
119 | 23.015 66.583
120 | 23.005 29.987
121 | 22.995 15.006
122 | 22.990 4.838
123 | 5.843 22.980
124 | 15.031 22.975
125 | 30.087 22.965
126 | 66.917 22.955
127 | 110.552 22.940
128 | ```
129 | ### get-full-book
130 | Returns the full orderbook for a given market.
131 |
132 | `$ phoenix-cli -u main get-full-book 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg`
133 | ```
134 | 23.210 409.500
135 | 23.025 166.320
136 | 23.005 109.954
137 | 22.990 96.747
138 | 22.980 15.025
139 | 22.975 5.845
140 | 5.850 22.965
141 | 15.050 22.960
142 | 30.125 22.950
143 | 67.057 22.930
144 | 110.784 22.915
145 | 168.137 22.895
146 | 426.985 22.710
147 | ```
148 |
149 | ### get-transaction
150 | Returns a summary of the market events that occured (Place, Fill, Reduce/Cancel) in a given transaction signature.
151 |
152 | `$ phoenix-cli -u main get-transaction 4gw6UDWsDCWrh2eqYxvVzbVyywfPVo24V2qMTSVGJJAdxvv9Tx4pBrqE1cLTgomP2QkZ7wigbjoN3GpibhJY8PFV`
153 | ```
154 | market: 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg, event_type: Fill, timestamp: 1677629539, signature: 4gw6UDWsDCWrh2eqYxvVzbVyywfPVo24V2qMTSVGJJAdxvv9Tx4pBrqE1cLTgomP2QkZ7wigbjoN3GpibhJY8PFV, slot: 180067446, sequence_number: 680904, event_index: 0, maker: 3HBWHuyxWv4uN8U8SeukocrWPfLZJqrtj9DgDHsGo2HR, taker: CcoiNhaTR88CSkEdsdeJpEMWnfCNqMf4HGGzXjwnvZF, price: 21.815, side: Bid, quantity: 2.288
155 | market: 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg, event_type: Fill, timestamp: 1677629539, signature: 4gw6UDWsDCWrh2eqYxvVzbVyywfPVo24V2qMTSVGJJAdxvv9Tx4pBrqE1cLTgomP2QkZ7wigbjoN3GpibhJY8PFV, slot: 180067446, sequence_number: 680904, event_index: 1, maker: 3HBWHuyxWv4uN8U8SeukocrWPfLZJqrtj9DgDHsGo2HR, taker: CcoiNhaTR88CSkEdsdeJpEMWnfCNqMf4HGGzXjwnvZF, price: 21.811, side: Bid, quantity: 27.459
156 | market: 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg, event_type: Fill, timestamp: 1677629539, signature: 4gw6UDWsDCWrh2eqYxvVzbVyywfPVo24V2qMTSVGJJAdxvv9Tx4pBrqE1cLTgomP2QkZ7wigbjoN3GpibhJY8PFV, slot: 180067446, sequence_number: 680904, event_index: 2, maker: 3HBWHuyxWv4uN8U8SeukocrWPfLZJqrtj9DgDHsGo2HR, taker: CcoiNhaTR88CSkEdsdeJpEMWnfCNqMf4HGGzXjwnvZF, price: 21.806, side: Bid, quantity: 17.066
157 | Total quote token fees paid: 0.204193
158 | ```
159 |
160 | ### get-market-status
161 | Returns the status of a given market. Markets can be in the following states: Active, PostOnly, Paused, Closed, Uninitialized, Tombstoned.
162 |
163 | `$ phoenix-cli -u main get-market-status 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg`
164 | ```
165 | Market status: Active
166 | ```
167 |
168 | ### get-seat-info
169 | Returns the status and address of a trader's seat. By default, returns the payer's seat info. Seats can be in the following states: Approved, NotApproved, Retired
170 |
171 | `$ phoenix-cli -u main get-seat-info 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg -t 3HBWHuyxWv4uN8U8SeukocrWPfLZJqrtj9DgDHsGo2HR`
172 | ```
173 | Seat address: GGyZqgoqnKsvMTsmSSkTrDjtdSFUsEoioKz9Yr2vEnZa
174 | Seat status: Approved
175 | ```
176 |
177 | ### get-open-orders
178 | Returns all open orders on a given market for a trader. By default, returns the payer's open orders. Returns the side, orderID, price in ticks, price, and size for each order.
179 |
180 | `$ phoenix-cli -u main get-open-orders 14CAwu3LiBBk5fcHGdTsFyVxDwvpgFiSfDwgPJxECcE5 -t mkrc4jMLEPRoKLUnNL7Ctnwb7uJykbwiYvFjB4sw9Z9`
181 | ```
182 | Open Bids
183 | ID | Price (ticks) | Price | Quantity
184 | 18446744073707873235 | 4466 | 22.330 | 3.134
185 | 18446744073707873233 | 4465 | 22.325 | 8.062
186 | 18446744073707873231 | 4462 | 22.310 | 16.136
187 | 18446744073707873237 | 4461 | 22.305 | 35.866
188 | 18446744073707873247 | 4457 | 22.285 | 89.746
189 | 18446744073707873229 | 4457 | 22.285 | 59.232
190 | 18446744073707873245 | 4420 | 22.100 | 226.244
191 |
192 | Open Asks
193 | ID | Price (ticks) | Price | Quantity
194 | 1678379 | 4468 | 22.340 | 3.133
195 | 1678381 | 4469 | 22.345 | 8.055
196 | 1678383 | 4470 | 22.350 | 16.107
197 | 1678377 | 4473 | 22.365 | 35.770
198 | 1678385 | 4475 | 22.375 | 58.994
199 | 1678367 | 4483 | 22.415 | 89.225
200 | 1678369 | 4520 | 22.600 | 221.238
201 | ```
202 |
203 | ### request-seat
204 | Send a transaction on chain to allocate a seat for the payer on the given market. This will cost ~.0018 SOL for rent. Note that the seat will have to then be approved by the market authority in order to place limit orders.
205 |
206 | `$ phoenix-cli -u main request-seat 4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg`
207 | ```
208 | Requested seat, transaction signature: 3Qq7MZQ8XoLeT8fSfeFBTxRy8zFPvCFPbvwU2Zhu16gKT3o8tHo8HRxvHfyb75dvuJjDqo3sTpvfGL9v3tco8nAN
209 | ```
210 |
211 | ### mint-tokens
212 | Mints tokens of the ticker_string (example: SOL) to the given pubkey. Default amount is 100_000_000_000. This command is only relevant for tokens associated with the ellipsis token faucet. On mainnet, this will only apply to the BASE/QUOTE market at address `14CAwu3LiBBk5fcHGdTsFyVxDwvpgFiSfDwgPJxECcE5`
213 |
214 | `$ phoenix-cli -u main mint-tokens BASE aChXgDyJn7g5BCkjccisGc78LrQZKEmNgt5sz8Tdkzn -a 100000`
215 | ```
216 | Creating ATA
217 | 100000 Tokens minted! Mint pubkey: 7Z6Kczxo8ViRpfnsVvVaATB5fQ8bN2CQpxP8DHfd1vz5, Recipient address: aChXgDyJn7g5BCkjccisGc78LrQZKEmNgt5sz8Tdkzn
218 | ```
219 |
220 | ### mint-tokens-for-market
221 | Mints the base and quote tokens of the given market to the given pubkey. Default amounts are 100_000_000_000 for base and 100_000_000 for quote. This command is only relevant for tokens associated with the ellipsis token faucet. On mainnet, this will only apply to the BASE/QUOTE market at address `14CAwu3LiBBk5fcHGdTsFyVxDwvpgFiSfDwgPJxECcE5`
222 |
223 | `$ phoenix-cli -u main mint-tokens-for-market 14CAwu3LiBBk5fcHGdTsFyVxDwvpgFiSfDwgPJxECcE5 aChXgDyJn7g5BCkjccisGc78LrQZKEmNgt5sz8Tdkzn`
224 | ```
225 | Creating ATA for base token
226 | Creating ATA for quote token
227 | Tokens minted! Signature: 2mN6o7gBB41UFEboQuCMaeG1t5qQ1uRAvTDoXUhsk1yBoKXQtrXsHVtkQAT9R3oRUSPbhDkZjCQtNtjcYP4TqwVV
228 | ```
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
--------------------------------------------------------------------------------
/phoenix-cli-install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Phoenix CLI binary installation script
4 | #
5 | # The purpose of this script is to automate the download and installation
6 | # of Phoenix CLI binary.
7 | #
8 | # Currently the supported platforms are macOS, Linux
9 | #
10 |
11 | RED() { echo $'\e[1;31m'$1$'\e[0m'; }
12 | GRN() { echo $'\e[1;32m'$1$'\e[0m'; }
13 | CYN() { echo $'\e[1;36m'$1$'\e[0m'; }
14 |
15 | abort_on_error() {
16 | if [ ! $1 -eq 0 ]; then
17 | RED "Aborting: operation failed"
18 | exit 1
19 | fi
20 | }
21 |
22 | CYN "Phoenix CLI installation script"
23 | echo "---------------------------------------"
24 | echo ""
25 |
26 | OS_FLAVOUR="$(uname -s)"
27 | PROCESSOR="$(uname -m)"
28 |
29 |
30 | case "$PROCESSOR" in
31 | arm* | aarch* | ppc* )
32 | if [ "$OS_FLAVOUR" != Darwin ]; then
33 | echo "Binary for $PROCESSOR architecture is not currently supported using this installer."
34 | exit 1
35 | fi
36 | ;;
37 |
38 | *)
39 | # good to go
40 | ;;
41 | esac
42 |
43 | BIN="phoenix-cli"
44 | SUFFIX="linux"
45 |
46 | if [ "$OS_FLAVOUR" = Darwin ]; then
47 | SUFFIX="macos"
48 | fi
49 |
50 | if ["$OS_FLAVOUR" = Windows ]; then
51 | echo "Windows is not currently supported using this installer."
52 | exit 1
53 | fi
54 |
55 | DIST="phoenix-cli-$SUFFIX"
56 |
57 | # creates a temporary directory to save the distribution file
58 | SOURCE="$(mktemp -d)"
59 |
60 | echo "$(CYN "1.") 🖥 $(CYN "Downloading distribution")"
61 | echo ""
62 |
63 | # downloads the distribution file
64 | REMOTE="https://github.com/Ellipsis-Labs/phoenix-cli/releases/latest/download/"
65 | echo " => downloading from: $(CYN $REMOTE$DIST)"
66 | curl -L $REMOTE$DIST --output "$SOURCE/$DIST"
67 | abort_on_error $?
68 |
69 | SIZE=$(wc -c "$SOURCE/$DIST" | grep -oE "[0-9]+" | head -n 1)
70 |
71 | if [ $SIZE -eq 0 ]; then
72 | RED "Aborting: could not download Phoenix distribution"
73 | exit 1
74 | fi
75 |
76 | # makes sure the binary will be executable
77 | chmod u+x "$SOURCE/$DIST"
78 | abort_on_error $?
79 |
80 | echo ""
81 | echo "$(CYN "2.") 📤 $(CYN "Moving binary into place")"
82 | echo ""
83 |
84 | if [ ! "$(command -v $BIN)" = "" ]; then
85 | # binary already found on system, ask if we should
86 | # replace it
87 | EXISTING="$(which $BIN)"
88 |
89 | echo "Phoenix binary was found at:"
90 | echo " => $(CYN $EXISTING)"
91 | echo ""
92 | echo -n "$(CYN "Replace it? [y/n]") (default 'n'): "
93 | read REPLACE
94 |
95 | if [ "$REPLACE" = y ]; then
96 | echo ""
97 | echo "'$BIN' binary will be moved to '$(dirname "$EXISTING")'."
98 |
99 | mv "$SOURCE/$DIST" "$EXISTING"
100 | abort_on_error $?
101 | else
102 | # nothing else to do, replacement was cancelled
103 | RED "Aborting: replacement cancelled"
104 | exit 1
105 | fi
106 | else
107 | # determines a suitable directory for the binary - preference:
108 | # 1) ~/.cargo/bin if exists
109 | # 2) ~/bin otherwise
110 | TARGET="$HOME/.cargo/bin"
111 |
112 | if [ ! -d "$TARGET" ]; then
113 | TARGET="$HOME/bin"
114 |
115 | if [ ! -d "$TARGET" ]; then
116 | mkdir $TARGET
117 | fi
118 | fi
119 |
120 | echo "'$BIN' binary will be moved to '$TARGET'."
121 |
122 | mv "$SOURCE/$DIST" "$TARGET/$BIN"
123 | abort_on_error $?
124 |
125 | if [ "$(command -v $BIN)" = "" ]; then
126 | ENV_FILE="$HOME/.$(basename $SHELL)rc"
127 |
128 | if [ -f "$ENV_FILE" ]; then
129 | echo " => adding '$TARGET' to 'PATH' variable in '$ENV_FILE'"
130 | echo "export PATH=\"$HOME/bin:\$PATH\"" >> "$ENV_FILE"
131 | else
132 | echo " => adding '$TARGET' to 'PATH' variable to execute 'phoenix-cli' from any directory."
133 | echo " - file '$(CYN $ENV_FILE)' was not found"
134 | echo ""
135 | echo -n "$(CYN "Would you like to create '$ENV_FILE'? [y/n]") (default 'n'): "
136 | read CREATE
137 |
138 | if [ "$CREATE" = y ]; then
139 | echo " => adding '$TARGET' to 'PATH' variable in '$ENV_FILE'"
140 | echo "export PATH=\"$HOME/bin:\$PATH\"" >> "$ENV_FILE"
141 | else
142 | echo ""
143 | echo " $(RED "[File creation cancelled]")"
144 | echo ""
145 | echo " - to manually add '$TARGET' to 'PATH' you will need to:"
146 | echo ""
147 | echo " 1. create a file named '$(basename $ENV_FILE)' in your directory '$(dirname $ENV_FILE)'"
148 | echo " 2. add the following line to the file:"
149 | echo ""
150 | echo " export PATH=\"$HOME/bin:\$PATH\""
151 | fi
152 | fi
153 | fi
154 | fi
155 |
156 | echo ""
157 | # sanity check
158 | if [ "$(command -v $BIN)" = "" ]; then
159 | # installation was completed, but phoenix-cli is not in the PATH
160 | echo "✅ $(GRN "Installation complete:") restart your shell to update 'PATH' variable or type '$TARGET/$BIN' to start using it."
161 | else
162 | # success
163 | echo "✅ $(GRN "Installation successful:") type '$BIN' to start using it."
164 | fi
165 |
--------------------------------------------------------------------------------
/src/command.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 | use solana_sdk::pubkey::Pubkey;
3 | use solana_sdk::signature::Signature;
4 |
5 | // #[clap(author, version, about)]
6 | #[derive(Debug, Clone, Parser)]
7 | pub enum PhoenixCLICommand {
8 | /// Get summary information on all markets
9 | GetAllMarkets {
10 | /// Optionally skip the GetProgramAccounts network call. This will read a static list of markets in a config file instead.
11 | /// Highly recommended to use this flag as GetProgramAccounts is an expensive call.
12 | #[clap(short, long, required = false)]
13 | no_gpa: bool,
14 | },
15 | /// Get detailed information on a specific market
16 | GetMarket { market_pubkey: Pubkey },
17 | /// Get active traders for a given market
18 | GetTradersForMarket { market_pubkey: Pubkey },
19 | /// Get the best bid and ask price for a given market
20 | GetTopOfBook { market_pubkey: Pubkey },
21 | /// Get the first N levels of the order book for a given market.
22 | /// Default is 10 levels
23 | GetBookLevels {
24 | market_pubkey: Pubkey,
25 | #[clap(short, long, required = false, default_value = "10")]
26 | levels: u64,
27 | },
28 | /// Get the full order book for a given market
29 | GetFullBook { market_pubkey: Pubkey },
30 | /// Get the market events that occured in a given transaction signature
31 | GetTransaction { signature: Signature },
32 | /// Get the current status of a market
33 | GetMarketStatus { market_pubkey: Pubkey },
34 | /// Get the status and address of a seat for a given market and trader
35 | GetSeatInfo {
36 | market_pubkey: Pubkey,
37 | /// Pubkey of the trader associated with the seat. Defaults to the current payer
38 | #[clap(short, long, required = false)]
39 | trader_pubkey: Option,
40 | },
41 | /// Get all open orders on a given market for a trader
42 | GetOpenOrders {
43 | market_pubkey: Pubkey,
44 | /// Pubkey of the trader for whom to get open orders. Defaults to the current payer
45 | #[clap(short, long, required = false)]
46 | trader_pubkey: Option,
47 | },
48 | /// Send a transaction on chain to allocate a seat for the payer on the given market. This will cost ~.0018 SOL for rent.
49 | /// Note that the seat will have to then be approved by the market authority. Only relevant for permissioned markets.
50 | /// For permissionless markets (with an automated seat manager), you can claim a seat with the claim-seat CLI command.
51 | RequestSeat { market_pubkey: Pubkey },
52 | /// Mint tokens to a recipient for a given ticker string (for example SOL or USDC). Default amount is 100_000_000_000.
53 | /// This is only for markets associated with the ellipsis token faucet.
54 | MintTokens {
55 | /// Ticker string, example: SOL
56 | mint_ticker: String,
57 | /// Pubkey of the recipient of the tokens
58 | recipient_pubkey: Pubkey,
59 | /// Amount in atoms (1 * 10*(-decimals))
60 | #[clap(short, long, required = false, default_value = "100000000000")]
61 | amount: u64,
62 | },
63 | /// Mint both base and quote tokens to a recipient for a given market. Default amounts are 100_000_000_000 for base and 100_000_000 for quote.
64 | /// This is only for markets associated with the ellipsis token faucet.
65 | MintTokensForMarket {
66 | market_pubkey: Pubkey,
67 | /// Pubkey of the recipient of the tokens
68 | recipient_pubkey: Pubkey,
69 | /// Amount in atoms (1 * 10*(-decimals))
70 | #[clap(short, long, required = false, default_value = "100000000000")]
71 | base_amount: u64,
72 | /// Amount in atoms (1 * 10*(-decimals))
73 | #[clap(short, long, required = false, default_value = "100000000")]
74 | quote_amount: u64,
75 | },
76 | /// For the given market, get the seat manager data fields, including authority, successor, and designated market makers.
77 | GetSeatManagerInfo { market_pubkey: Pubkey },
78 | /// On the given market, claim a maker seat for the public key of the keypair at the indicated file path.
79 | /// Indicate a different keypair file to use by specifying the file path with flag `-k`.
80 | ClaimSeat { market_pubkey: Pubkey },
81 | /// Evict a trader from the given market if that market's trader state is at capacity.
82 | /// If no trader is given, this function will greedily find a trader to evict.
83 | /// Note that eviction will not work if the market's trader state is not at capacity.
84 | EvictSeat {
85 | market_pubkey: Pubkey,
86 | trader_to_evict: Option,
87 | },
88 | }
89 |
--------------------------------------------------------------------------------
/src/lib/helpers/devnet_helpers.rs:
--------------------------------------------------------------------------------
1 | use ellipsis_client::EllipsisClient;
2 | use solana_sdk::pubkey::Pubkey;
3 | use solana_sdk::signer::Signer;
4 |
5 | pub mod devnet_token_faucet {
6 | use borsh::{BorshDeserialize, BorshSerialize};
7 | use solana_sdk::{
8 | declare_id,
9 | instruction::{AccountMeta, Instruction},
10 | pubkey::Pubkey,
11 | };
12 |
13 | const CREAT_MINT_DISCRIMINATOR: [u8; 8] = [69, 44, 215, 132, 253, 214, 41, 45];
14 | const AIRDROP_SPL_DISCRIMINATOR: [u8; 8] = [133, 44, 125, 96, 172, 219, 228, 51];
15 |
16 | declare_id!("FF2UnZt7Lce3S65tW5cMVKz8iVAPoCS8ETavmUhsWLJB");
17 |
18 | #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
19 | pub struct CreateMint {
20 | pub ticker: String,
21 | pub decimals: u8,
22 | }
23 |
24 | pub fn get_mint_address(ticker: &str) -> Pubkey {
25 | Pubkey::find_program_address(&["mint".as_bytes(), ticker.to_lowercase().as_ref()], &ID).0
26 | }
27 |
28 | pub fn get_mint_authority_address(ticker: &str) -> Pubkey {
29 | Pubkey::find_program_address(
30 | &["mint-authority".as_bytes(), ticker.to_lowercase().as_ref()],
31 | &ID,
32 | )
33 | .0
34 | }
35 |
36 | pub fn create_mint_ix(
37 | program_id: Pubkey,
38 | payer: Pubkey,
39 | ticker: String,
40 | decimals: u8,
41 | ) -> Instruction {
42 | let (mint, _) = Pubkey::find_program_address(
43 | &["mint".as_bytes(), ticker.to_lowercase().as_ref()],
44 | &program_id,
45 | );
46 |
47 | let (mint_authority, _) = Pubkey::find_program_address(
48 | &["mint-authority".as_bytes(), ticker.to_lowercase().as_ref()],
49 | &program_id,
50 | );
51 |
52 | let accounts_vec: Vec = vec![
53 | AccountMeta::new(mint, false),
54 | AccountMeta::new(mint_authority, false),
55 | AccountMeta::new(payer, true),
56 | AccountMeta::new_readonly(spl_token::id(), false),
57 | AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
58 | AccountMeta::new_readonly(solana_sdk::sysvar::rent::id(), false),
59 | ];
60 |
61 | let create_mint_data = CreateMint { ticker, decimals };
62 |
63 | let ix_data: Vec = [
64 | &CREAT_MINT_DISCRIMINATOR,
65 | create_mint_data.try_to_vec().unwrap().as_slice(),
66 | ]
67 | .concat();
68 |
69 | Instruction {
70 | program_id,
71 | accounts: accounts_vec,
72 | data: ix_data,
73 | }
74 | }
75 |
76 | #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
77 | pub struct AirdropSpl {
78 | pub amount: u64,
79 | }
80 |
81 | pub fn airdrop_spl_with_ticker_ix(
82 | program_id: &Pubkey,
83 | ticker: String,
84 | payer: &Pubkey,
85 | amount: u64,
86 | ) -> Instruction {
87 | let mint = get_mint_address(&ticker);
88 | let mint_authority = get_mint_authority_address(&ticker);
89 |
90 | let destination = spl_associated_token_account::get_associated_token_address(payer, &mint);
91 |
92 | let accounts_vec: Vec = vec![
93 | AccountMeta::new(mint, false),
94 | AccountMeta::new(mint_authority, false),
95 | AccountMeta::new(destination, false),
96 | AccountMeta::new_readonly(spl_token::id(), false),
97 | ];
98 |
99 | let airdrop_spl_data = AirdropSpl { amount };
100 |
101 | let ix_data: Vec = [
102 | AIRDROP_SPL_DISCRIMINATOR,
103 | airdrop_spl_data.try_to_vec().unwrap().try_into().unwrap(),
104 | ]
105 | .concat();
106 |
107 | Instruction {
108 | program_id: *program_id,
109 | accounts: accounts_vec,
110 | data: ix_data,
111 | }
112 | }
113 |
114 | pub fn airdrop_spl_with_mint_pdas_ix(
115 | program_id: &Pubkey,
116 | mint: &Pubkey,
117 | mint_authority: &Pubkey,
118 | payer: &Pubkey,
119 | amount: u64,
120 | ) -> Instruction {
121 | let destination = spl_associated_token_account::get_associated_token_address(payer, mint);
122 |
123 | let accounts_vec: Vec = vec![
124 | AccountMeta::new(*mint, false),
125 | AccountMeta::new(*mint_authority, false),
126 | AccountMeta::new(destination, false),
127 | AccountMeta::new_readonly(spl_token::id(), false),
128 | ];
129 |
130 | let airdrop_spl_data = AirdropSpl { amount };
131 |
132 | let ix_data: Vec = [
133 | AIRDROP_SPL_DISCRIMINATOR,
134 | airdrop_spl_data.try_to_vec().unwrap().try_into().unwrap(),
135 | ]
136 | .concat();
137 |
138 | Instruction {
139 | program_id: *program_id,
140 | accounts: accounts_vec,
141 | data: ix_data,
142 | }
143 | }
144 | }
145 |
146 | // Create_mint on devnet, utilizing devnet-token-faucet
147 | pub async fn find_or_create_devnet_mint(
148 | client: &EllipsisClient,
149 | ticker: &str,
150 | decimals: u8,
151 | ) -> anyhow::Result {
152 | let (mint, _) = Pubkey::find_program_address(
153 | &["mint".as_bytes(), ticker.to_lowercase().as_ref()],
154 | &devnet_token_faucet::ID,
155 | );
156 | if client.get_account(&mint).await.is_err() {
157 | let mint_ix = devnet_token_faucet::create_mint_ix(
158 | devnet_token_faucet::ID,
159 | client.payer.pubkey(),
160 | ticker.to_string(),
161 | decimals,
162 | );
163 | client
164 | .sign_send_instructions(vec![mint_ix], vec![&client.payer])
165 | .await?;
166 | }
167 | Ok(mint)
168 | }
169 |
--------------------------------------------------------------------------------
/src/lib/helpers/market_helpers.rs:
--------------------------------------------------------------------------------
1 | use borsh::BorshDeserialize;
2 | use borsh::BorshSerialize;
3 | use ellipsis_client::EllipsisClient;
4 | use phoenix::program::{load_with_dispatch, status::SeatApprovalStatus, MarketHeader};
5 | use phoenix::state::markets::FIFOOrderId;
6 | use phoenix::state::markets::FIFORestingOrder;
7 | use phoenix::state::markets::{Ladder, Market};
8 | use phoenix::state::OrderPacket;
9 |
10 | use phoenix_sdk::sdk_client::*;
11 | use phoenix_seat_manager::get_seat_manager_address;
12 | use phoenix_seat_manager::seat_manager::SeatManager;
13 | use solana_account_decoder::UiAccountEncoding;
14 | use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
15 | use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType};
16 | use solana_sdk::account::Account;
17 | use solana_sdk::clock::Clock;
18 | use solana_sdk::commitment_config::CommitmentConfig;
19 | use solana_sdk::keccak;
20 | use solana_sdk::pubkey::Pubkey;
21 | use solana_sdk::sysvar;
22 | use std::collections::BTreeMap;
23 | use std::mem::size_of;
24 |
25 | pub fn get_discriminant(type_name: &str) -> anyhow::Result {
26 | Ok(u64::from_le_bytes(
27 | keccak::hashv(&[phoenix::id().as_ref(), type_name.as_bytes()]).as_ref()[..8].try_into()?,
28 | ))
29 | }
30 |
31 | pub async fn get_seat_status(
32 | sdk: &SDKClient,
33 | seat_key: &Pubkey,
34 | ) -> anyhow::Result {
35 | // Get seat account and deserialize
36 | let seat_acc = sdk.client.get_account(seat_key).await?;
37 | let mut seat_acc_data = seat_acc.data.to_vec();
38 | let (_, seat_approval_bytes) = seat_acc_data.split_at_mut(72);
39 | let status_as_u64 = u64::try_from_slice(&seat_approval_bytes[0..8])?;
40 | let seat_status = SeatApprovalStatus::from(status_as_u64);
41 | Ok(seat_status)
42 | }
43 |
44 | pub async fn get_all_markets(client: &EllipsisClient) -> anyhow::Result> {
45 | // Get discriminant for market account
46 | let market_account_discriminant = get_discriminant("phoenix::program::accounts::MarketHeader")?;
47 |
48 | // Get Program Accounts, filtering for the market account discriminant
49 | #[allow(deprecated)]
50 | //Allow deprecated because solana_client struct Memcmp.encoding since 1.11.2. Upgrade workspace package
51 | let memcmp = RpcFilterType::Memcmp(Memcmp {
52 | offset: 0,
53 | bytes: MemcmpEncodedBytes::Bytes(market_account_discriminant.to_le_bytes().to_vec()),
54 | encoding: None,
55 | });
56 |
57 | let config = RpcProgramAccountsConfig {
58 | filters: Some(vec![memcmp]),
59 | account_config: RpcAccountInfoConfig {
60 | encoding: Some(UiAccountEncoding::Base64),
61 | commitment: Some(CommitmentConfig::confirmed()),
62 | ..RpcAccountInfoConfig::default()
63 | },
64 | ..RpcProgramAccountsConfig::default()
65 | };
66 |
67 | let accounts = client
68 | .get_program_accounts_with_config(&phoenix::id(), config)
69 | .await?;
70 | Ok(accounts)
71 | }
72 |
73 | // Get each trader's trader index and map to the trader's pubkey
74 | pub fn get_all_registered_traders(
75 | market: &dyn Market,
76 | ) -> BTreeMap {
77 | let mut trader_index_to_pubkey = BTreeMap::new();
78 | market
79 | .get_registered_traders()
80 | .iter()
81 | .map(|(trader, _)| *trader)
82 | .for_each(|trader| {
83 | trader_index_to_pubkey.insert(market.get_trader_index(&trader).unwrap() as u64, trader);
84 | });
85 | trader_index_to_pubkey
86 | }
87 |
88 | pub async fn get_all_seats(client: &EllipsisClient) -> anyhow::Result> {
89 | // Get discriminant for seat account
90 | let seat_account_discriminant = get_discriminant("phoenix::program::accounts::Seat")?;
91 |
92 | #[allow(deprecated)]
93 | let memcmp = RpcFilterType::Memcmp(Memcmp {
94 | offset: 0,
95 | bytes: MemcmpEncodedBytes::Bytes(seat_account_discriminant.to_le_bytes().to_vec()),
96 | encoding: None,
97 | });
98 |
99 | let config = RpcProgramAccountsConfig {
100 | filters: Some(vec![memcmp]),
101 | account_config: RpcAccountInfoConfig {
102 | encoding: Some(UiAccountEncoding::Base64),
103 | commitment: Some(CommitmentConfig::confirmed()),
104 | ..RpcAccountInfoConfig::default()
105 | },
106 | ..RpcProgramAccountsConfig::default()
107 | };
108 |
109 | let accounts = client
110 | .get_program_accounts_with_config(&phoenix::id(), config)
111 | .await?;
112 |
113 | Ok(accounts)
114 | }
115 |
116 | pub async fn get_book_levels(
117 | market_pubkey: &Pubkey,
118 | client: &EllipsisClient,
119 | levels: u64,
120 | ) -> anyhow::Result {
121 | // Get market account
122 | let mut market_and_clock = client
123 | .get_multiple_accounts_with_commitment(
124 | &[*market_pubkey, sysvar::clock::id()],
125 | CommitmentConfig::confirmed(),
126 | )
127 | .await?
128 | .value;
129 |
130 | let market_account_data = market_and_clock
131 | .remove(0)
132 | .ok_or_else(|| anyhow::Error::msg("Market account not found"))?
133 | .data;
134 |
135 | let clock_account_data = market_and_clock
136 | .remove(0)
137 | .ok_or_else(|| anyhow::Error::msg("Clock account not found"))?
138 | .data;
139 |
140 | let clock: Clock = bincode::deserialize(&clock_account_data)
141 | .map_err(|_| anyhow::Error::msg("Error deserializing clock"))?;
142 |
143 | let (header_bytes, market_bytes) = market_account_data.split_at(size_of::());
144 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
145 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
146 |
147 | // Derserialize data and load into correct type
148 | let market = load_with_dispatch(&header.market_size_params, market_bytes)?.inner;
149 |
150 | Ok(market.get_ladder_with_expiration(
151 | levels,
152 | Some(clock.slot),
153 | Some(clock.unix_timestamp as u64),
154 | ))
155 | }
156 |
157 | pub async fn get_all_approved_seats_for_market(
158 | sdk: &SDKClient,
159 | market: &Pubkey,
160 | ) -> anyhow::Result> {
161 | // Get discriminant for seat account
162 | let seat_account_discriminant = get_discriminant("phoenix::program::accounts::Seat")?;
163 |
164 | // Get Program Accounts, filtering for the market account discriminant
165 | let memcmp = RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
166 | 0,
167 | [
168 | seat_account_discriminant.to_le_bytes().to_vec(),
169 | market.to_bytes().to_vec(),
170 | ]
171 | .concat(),
172 | ));
173 |
174 | let approved = RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
175 | 72,
176 | SeatApprovalStatus::Approved.try_to_vec()?,
177 | ));
178 |
179 | let config = RpcProgramAccountsConfig {
180 | filters: Some(vec![memcmp, approved]),
181 | account_config: RpcAccountInfoConfig {
182 | encoding: Some(UiAccountEncoding::Base64),
183 | commitment: Some(CommitmentConfig::confirmed()),
184 | ..RpcAccountInfoConfig::default()
185 | },
186 | ..RpcProgramAccountsConfig::default()
187 | };
188 |
189 | let accounts = sdk
190 | .client
191 | .get_program_accounts_with_config(&phoenix::id(), config)
192 | .await?;
193 | Ok(accounts)
194 | }
195 |
196 | pub async fn get_market_header(
197 | sdk: &SDKClient,
198 | market_pubkey: &Pubkey,
199 | ) -> anyhow::Result {
200 | let market_account_data = sdk.client.get_account_data(market_pubkey).await?;
201 | let (header_bytes, _market_bytes) = market_account_data.split_at(size_of::());
202 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
203 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
204 |
205 | Ok(*header)
206 | }
207 |
208 | pub async fn get_seat_manager_data_with_market(
209 | client: &EllipsisClient,
210 | market: &Pubkey,
211 | ) -> anyhow::Result {
212 | let seat_manager_address = get_seat_manager_address(market).0;
213 | get_seat_manager_data_with_pubkey(client, &seat_manager_address).await
214 | }
215 |
216 | pub async fn get_seat_manager_data_with_pubkey(
217 | client: &EllipsisClient,
218 | seat_manager_pubkey: &Pubkey,
219 | ) -> anyhow::Result {
220 | let seat_manager_account = client.get_account(seat_manager_pubkey).await?;
221 | let seat_manager_data = SeatManager::load(&seat_manager_account.data)?;
222 |
223 | Ok(*seat_manager_data)
224 | }
225 |
--------------------------------------------------------------------------------
/src/lib/helpers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod devnet_helpers;
2 | pub mod market_helpers;
3 | pub mod print_helpers;
4 |
--------------------------------------------------------------------------------
/src/lib/helpers/print_helpers.rs:
--------------------------------------------------------------------------------
1 | use std::mem::size_of;
2 |
3 | use colored::Colorize;
4 | use phoenix::program::status::MarketStatus;
5 | use phoenix::program::MarketHeader;
6 | use phoenix::program::{get_vault_address, load_with_dispatch};
7 | use phoenix::quantities::WrapperU64;
8 | use phoenix::state::{markets::Ladder, Side, TraderState};
9 | use phoenix_sdk::sdk_client::*;
10 | use solana_sdk::program_pack::Pack;
11 | use solana_sdk::pubkey::Pubkey;
12 |
13 | pub fn print_book(sdk: &SDKClient, market: &Pubkey, book: &Ladder) -> anyhow::Result<()> {
14 | let meta = sdk.get_market_metadata_from_cache(market)?;
15 | let raw_base_units_per_base_lot =
16 | meta.base_atoms_per_base_lot as f64 / meta.base_atoms_per_raw_base_unit as f64;
17 |
18 | let asks = book.asks.iter().filter_map(|lvl| {
19 | Some((
20 | sdk.ticks_to_float_price(market, lvl.price_in_ticks).ok()?,
21 | lvl.size_in_base_lots as f64 * raw_base_units_per_base_lot,
22 | ))
23 | });
24 |
25 | let bids = book.bids.iter().filter_map(|lvl| {
26 | Some((
27 | sdk.ticks_to_float_price(market, lvl.price_in_ticks).ok()?,
28 | lvl.size_in_base_lots as f64 * raw_base_units_per_base_lot,
29 | ))
30 | });
31 | let price_precision: usize = get_precision(
32 | 10_u64.pow(meta.quote_decimals) * meta.raw_base_units_per_base_unit as u64
33 | / meta.tick_size_in_quote_atoms_per_base_unit,
34 | );
35 | let size_precision: usize =
36 | get_precision(meta.num_base_lots_per_base_unit / meta.raw_base_units_per_base_unit as u64);
37 | let bid_strings = bids
38 | .into_iter()
39 | .map(|(price, size)| {
40 | let p = format_float(price, price_precision);
41 | let s = format_float(size, size_precision).green();
42 | (s, p)
43 | })
44 | .collect::>();
45 |
46 | let bid_width = bid_strings.iter().map(|(s, _)| s.len()).max().unwrap_or(0) + 1;
47 |
48 | let ask_strings = asks
49 | .into_iter()
50 | .rev()
51 | .map(|(price, size)| {
52 | let p = format_float(price, price_precision);
53 | let s = format_float(size, size_precision).red();
54 | (p, s)
55 | })
56 | .collect::>();
57 |
58 | let price_width = bid_strings
59 | .iter()
60 | .zip(ask_strings.iter())
61 | .map(|(a, b)| a.0.len().max(b.1.len()))
62 | .max()
63 | .unwrap_or(0);
64 |
65 | let ask_width = ask_strings.iter().map(|(_, s)| s.len()).max().unwrap_or(0) + 1;
66 |
67 | for (price, size) in ask_strings {
68 | let str = format!(
69 | "{:bid_width$} {:>price_width$} {:>ask_width$}",
70 | "", price, size
71 | );
72 | println!("{}", str);
73 | }
74 | for (size, price) in bid_strings {
75 | let str = format!(
76 | "{:>bid_width$} {:>price_width$} {:ask_width$}",
77 | size, price, ""
78 | );
79 | println!("{}", str);
80 | }
81 | Ok(())
82 | }
83 |
84 | pub struct LadderLevelEntry {
85 | pub tick: u64,
86 | pub lots: u64,
87 | pub trader_present: bool,
88 | }
89 |
90 | pub fn print_book_with_trader(
91 | sdk: &SDKClient,
92 | market: &Pubkey,
93 | bid_entries: &[LadderLevelEntry],
94 | ask_entries: &[LadderLevelEntry],
95 | ) -> anyhow::Result<()> {
96 | let meta = sdk.get_market_metadata_from_cache(market)?;
97 | let raw_base_units_per_base_lot =
98 | meta.base_atoms_per_base_lot as f64 / meta.base_atoms_per_raw_base_unit as f64;
99 |
100 | let asks = ask_entries.iter().filter_map(|lvl| {
101 | Some((
102 | sdk.ticks_to_float_price(market, lvl.tick).ok()?,
103 | lvl.lots as f64 * raw_base_units_per_base_lot,
104 | lvl.trader_present,
105 | ))
106 | });
107 |
108 | let bids = bid_entries.iter().filter_map(|lvl| {
109 | Some((
110 | sdk.ticks_to_float_price(market, lvl.tick).ok()?,
111 | lvl.lots as f64 * raw_base_units_per_base_lot,
112 | lvl.trader_present,
113 | ))
114 | });
115 | let price_precision: usize = get_precision(
116 | 10_u64.pow(meta.quote_decimals) * meta.raw_base_units_per_base_unit as u64
117 | / meta.tick_size_in_quote_atoms_per_base_unit,
118 | );
119 | let size_precision: usize =
120 | get_precision(meta.num_base_lots_per_base_unit / meta.raw_base_units_per_base_unit as u64);
121 | let bid_strings = bids
122 | .into_iter()
123 | .map(|(price, size, present)| {
124 | let p = format_float(price, price_precision);
125 | let s = format_float(size, size_precision).green();
126 | let m = if present { "→".green() } else { " ".green() };
127 |
128 | (m, s, p)
129 | })
130 | .collect::>();
131 |
132 | let bid_width = bid_strings
133 | .iter()
134 | .map(|(_, s, _)| s.len())
135 | .max()
136 | .unwrap_or(0)
137 | + 1;
138 |
139 | let ask_strings = asks
140 | .into_iter()
141 | .rev()
142 | .map(|(price, size, present)| {
143 | let p = format_float(price, price_precision);
144 | let s = format_float(size, size_precision).red();
145 | let m = if present { "←".red() } else { " ".red() };
146 |
147 | (p, s, m)
148 | })
149 | .collect::>();
150 |
151 | let price_width = bid_strings
152 | .iter()
153 | .zip(ask_strings.iter())
154 | .map(|(a, b)| a.0.len().max(b.1.len()))
155 | .max()
156 | .unwrap_or(0);
157 |
158 | let ask_width = ask_strings
159 | .iter()
160 | .map(|(_, s, _)| s.len())
161 | .max()
162 | .unwrap_or(0)
163 | + 1;
164 |
165 | for (price, size, marker) in ask_strings {
166 | let str = format!(
167 | " {:bid_width$} {:>price_width$} {:>ask_width$} {marker}",
168 | "", price, size
169 | );
170 | println!("{}", str);
171 | }
172 | for (marker, size, price) in bid_strings {
173 | let str = format!(
174 | "{marker} {:>bid_width$} {:>price_width$} {:ask_width$} ",
175 | size, price, ""
176 | );
177 | println!("{}", str);
178 | }
179 | Ok(())
180 | }
181 |
182 | pub fn get_precision(mut target: u64) -> usize {
183 | let mut fives = 0;
184 | let mut twos = 0;
185 | let initial = target;
186 | while target > 0 && target % 5 == 0 {
187 | target /= 5;
188 | fives += 1;
189 | }
190 | while target > 0 && target % 2 == 0 {
191 | target /= 2;
192 | twos += 1;
193 | }
194 | let precision = twos.max(fives);
195 | if precision == 0 && initial != 0 {
196 | // In the off chance that the target does not have 2 or 5 as a prime factor,
197 | // we'll just return a precision of 3 decimals.
198 | 3
199 | } else {
200 | precision
201 | }
202 | }
203 |
204 | pub fn format_float(float: f64, precision: usize) -> String {
205 | if precision > 3 && float.abs() < 1.0 {
206 | // Use scientific notation for small numbers
207 | format!("{:.1$e}", float, 3)
208 | } else if float > 1e9 {
209 | let prefix = format_float(float / 1e9, 3);
210 | format!("{}B", prefix)
211 | } else if float > 1e6 {
212 | let prefix = format_float(float / 1e6, 3);
213 | format!("{}M", prefix)
214 | } else {
215 | format!("{:.1$}", float, precision)
216 | }
217 | }
218 |
219 | pub fn print_market_summary_data(
220 | market_pubkey: &Pubkey,
221 | header: &MarketHeader,
222 | base_mint_symbol: Option,
223 | quote_mint_symbol: Option,
224 | ) {
225 | let base_pubkey = header.base_params.mint_key;
226 | let quote_pubkey = header.quote_params.mint_key;
227 |
228 | println!("--------------------------------------------");
229 | if let (Some(base), Some(quote)) = (base_mint_symbol, quote_mint_symbol) {
230 | println!("Market: {}/{}", base, quote);
231 | }
232 | println!("Market Address: {:?}", market_pubkey);
233 | println!("Base Token: {:?}", base_pubkey);
234 | println!("Quote Token: {:?}", quote_pubkey);
235 | println!("Authority: {:?}", header.authority);
236 | }
237 |
238 | pub async fn print_market_details(
239 | sdk: &SDKClient,
240 | market_pubkey: &Pubkey,
241 | market_metadata: &MarketMetadata,
242 | market_header: &MarketHeader,
243 | taker_fees: u64,
244 | base_mint_symbol: Option,
245 | quote_mint_symbol: Option,
246 | ) -> anyhow::Result<()> {
247 | let base_pubkey = market_metadata.base_mint;
248 | let quote_pubkey = market_metadata.quote_mint;
249 |
250 | let meta = sdk.get_market_metadata_from_cache(market_pubkey)?;
251 |
252 | let base_vault = get_vault_address(market_pubkey, &base_pubkey).0;
253 | let quote_vault = get_vault_address(market_pubkey, "e_pubkey).0;
254 |
255 | let base_vault_acct =
256 | spl_token::state::Account::unpack(&sdk.client.get_account(&base_vault).await?.data)?;
257 |
258 | let quote_vault_acct =
259 | spl_token::state::Account::unpack(&sdk.client.get_account("e_vault).await?.data)?;
260 |
261 | // Get market account
262 | let mut market_account_data = sdk.client.get_account_data(market_pubkey).await?;
263 | let (header_bytes, market_bytes) = market_account_data.split_at_mut(size_of::());
264 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
265 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
266 |
267 | // Derserialize data and load into correct type
268 | let market = load_with_dispatch(&header.market_size_params, market_bytes)?.inner;
269 |
270 | println!("--------------------------------------------");
271 | if let (Some(base), Some(quote)) = (base_mint_symbol, quote_mint_symbol) {
272 | println!("Market: {}/{}", base, quote);
273 | }
274 | println!("Market Address: {}", market_pubkey);
275 | println!("Status: {}", MarketStatus::from(market_header.status));
276 | println!("Authority: {}", market_header.authority);
277 | println!("Sequence number: {}", market_header.market_sequence_number);
278 |
279 | println!(
280 | "Base Vault balance: {:.3}",
281 | get_decimal_string(base_vault_acct.amount, meta.base_decimals).parse::()?
282 | );
283 |
284 | println!(
285 | "Quote Vault balance: {:.3}",
286 | get_decimal_string(quote_vault_acct.amount, meta.quote_decimals).parse::()?
287 | );
288 |
289 | println!("Base Token: {}", base_pubkey);
290 | println!("Quote Token: {}", quote_pubkey);
291 |
292 | println!("Base vault key: {}", market_header.base_params.vault_key);
293 | println!("Quote vault key: {}", market_header.quote_params.vault_key);
294 |
295 | println!(
296 | "Raw base units per base lot: {}",
297 | get_decimal_string(
298 | market_metadata.base_atoms_per_base_lot,
299 | market_metadata.base_decimals
300 | ),
301 | );
302 | println!(
303 | "Quote units per quote lot: {}",
304 | get_decimal_string(
305 | market_metadata.quote_atoms_per_quote_lot,
306 | market_metadata.quote_decimals
307 | )
308 | );
309 | println!(
310 | "Tick size in quote units per base unit: {}",
311 | get_decimal_string(
312 | market_metadata.tick_size_in_quote_atoms_per_base_unit,
313 | market_metadata.quote_decimals
314 | )
315 | );
316 | println!(
317 | "Num base lots per base unit: {}",
318 | market_metadata.num_base_lots_per_base_unit,
319 | );
320 | println!(
321 | "Tick size in quote atoms per base unit: {}",
322 | market_metadata.tick_size_in_quote_atoms_per_base_unit,
323 | );
324 | println!("Taker fees in basis points: {}", taker_fees);
325 | println!("Fee destination pubkey: {:?}", market_header.fee_recipient);
326 | println!(
327 | "Raw base units per base unit: {}",
328 | market_metadata.raw_base_units_per_base_unit
329 | );
330 | println!("Market Size Params: {:?}", market_header.market_size_params);
331 | println!("Successor pubkey: {:?}", market_header.successor);
332 |
333 | println!(
334 | "Uncollected fees, in quote units: {}",
335 | get_decimal_string(
336 | sdk.quote_lots_to_quote_atoms(
337 | market_pubkey,
338 | market.get_uncollected_fee_amount().as_u64()
339 | )?,
340 | market_metadata.quote_decimals
341 | )
342 | );
343 | println!(
344 | "Collected fees, in quote units: {}",
345 | get_decimal_string(
346 | sdk.quote_lots_to_quote_atoms(
347 | market_pubkey,
348 | market.get_collected_fee_amount().as_u64()
349 | )?,
350 | market_metadata.quote_decimals
351 | )
352 | );
353 |
354 | Ok(())
355 | }
356 |
357 | pub fn print_trader_state(
358 | sdk: &SDKClient,
359 | market_pubkey: &Pubkey,
360 | pubkey: &Pubkey,
361 | state: &TraderState,
362 | ) -> anyhow::Result<()> {
363 | let meta = sdk.get_market_metadata_from_cache(market_pubkey)?;
364 | if state.base_lots_locked == 0
365 | && state.base_lots_free == 0
366 | && state.quote_lots_locked == 0
367 | && state.quote_lots_free == 0
368 | {
369 | return Ok(());
370 | }
371 | println!("--------------------------------");
372 | println!("Trader pubkey: {:?}", pubkey);
373 | println!(
374 | "Base token locked: {}",
375 | get_decimal_string(
376 | sdk.base_lots_to_base_atoms(market_pubkey, state.base_lots_locked.into())?,
377 | meta.base_decimals
378 | )
379 | );
380 | println!(
381 | "Base token free: {}",
382 | get_decimal_string(
383 | sdk.base_lots_to_base_atoms(market_pubkey, state.base_lots_free.into())?,
384 | meta.base_decimals
385 | )
386 | );
387 | println!(
388 | "Quote token locked: {}",
389 | get_decimal_string(
390 | sdk.quote_lots_to_quote_atoms(market_pubkey, state.quote_lots_locked.into())?,
391 | meta.quote_decimals
392 | )
393 | );
394 | println!(
395 | "Quote token free: {}",
396 | get_decimal_string(
397 | sdk.quote_lots_to_quote_atoms(market_pubkey, state.quote_lots_free.into())?,
398 | meta.quote_decimals
399 | )
400 | );
401 | Ok(())
402 | }
403 |
404 | pub async fn log_market_events(
405 | sdk: &mut SDKClient,
406 | market_events: Vec,
407 | ) -> anyhow::Result<()> {
408 | for event in market_events {
409 | let market_pubkey = event.market;
410 | if !sdk.markets.contains_key(&market_pubkey) {
411 | sdk.add_market(&market_pubkey).await?;
412 | }
413 | let metadata = sdk.get_market_metadata_from_cache(&market_pubkey)?;
414 | match event.details {
415 | MarketEventDetails::Fill(fill) => {
416 | let Fill {
417 | maker,
418 | taker,
419 | price_in_ticks,
420 | base_lots_filled,
421 | side_filled,
422 | ..
423 | } = fill;
424 | let keys = initialize_log(&event, "Fill".to_string());
425 | let fill_data = vec![
426 | maker.to_string(),
427 | taker.to_string(),
428 | (sdk.ticks_to_float_price(&market_pubkey, price_in_ticks))?.to_string(),
429 | format!("{:?}", side_filled),
430 | get_decimal_string(
431 | sdk.base_lots_to_base_atoms(&market_pubkey, base_lots_filled)?,
432 | metadata.base_decimals,
433 | ),
434 | ];
435 | println!("{}", finalize_log(keys, fill_data));
436 | }
437 | MarketEventDetails::Place(place) => {
438 | let Place {
439 | order_sequence_number,
440 | client_order_id: _,
441 | maker,
442 | price_in_ticks,
443 | base_lots_placed,
444 | } = place;
445 | let side = Side::from_order_sequence_number(order_sequence_number);
446 | let keys = initialize_log(&event, "Place".to_string());
447 | let place_data = vec![
448 | maker.to_string(),
449 | "".to_string(),
450 | sdk.ticks_to_float_price(&market_pubkey, price_in_ticks)?
451 | .to_string(),
452 | format!("{:?}", side),
453 | get_decimal_string(
454 | sdk.base_lots_to_base_atoms(&market_pubkey, base_lots_placed)?,
455 | metadata.base_decimals,
456 | ),
457 | ];
458 |
459 | println!("{}", finalize_log(keys, place_data));
460 | }
461 | MarketEventDetails::Reduce(reduce) => {
462 | let Reduce {
463 | order_sequence_number,
464 | maker,
465 | price_in_ticks,
466 | base_lots_removed,
467 | ..
468 | } = reduce;
469 | let side = Side::from_order_sequence_number(order_sequence_number);
470 | let keys = initialize_log(&event, "Reduce".to_string());
471 |
472 | let reduce_data = vec![
473 | maker.to_string(),
474 | "".to_string(),
475 | sdk.ticks_to_float_price(&market_pubkey, price_in_ticks)?
476 | .to_string(),
477 | format!("{:?}", side),
478 | get_decimal_string(
479 | sdk.base_lots_to_base_atoms(&market_pubkey, base_lots_removed)?,
480 | metadata.base_decimals,
481 | ),
482 | ];
483 | println!("{}", finalize_log(keys, reduce_data));
484 | }
485 | MarketEventDetails::FillSummary(fill_summary) => {
486 | let FillSummary {
487 | total_quote_fees, ..
488 | } = fill_summary;
489 | println!(
490 | "Total quote token fees paid: {}",
491 | sdk.quote_atoms_to_quote_units_as_float(&market_pubkey, total_quote_fees)?
492 | );
493 | }
494 | _ => {
495 | continue;
496 | }
497 | }
498 | }
499 | Ok(())
500 | }
501 | pub fn initialize_log(event: &PhoenixEvent, event_type: String) -> Vec {
502 | let base_schema: Vec = vec![
503 | "market".to_string(),
504 | "event_type".to_string(),
505 | "timestamp".to_string(),
506 | "signature".to_string(),
507 | "slot".to_string(),
508 | "sequence_number".to_string(),
509 | "event_index".to_string(),
510 | ];
511 | let base = vec![
512 | event.market.to_string(),
513 | event_type,
514 | event.timestamp.to_string(),
515 | event.signature.to_string(),
516 | event.slot.to_string(),
517 | event.sequence_number.to_string(),
518 | event.event_index.to_string(),
519 | ];
520 | base_schema
521 | .iter()
522 | .zip(base.iter())
523 | .map(|(a, b)| format!("{}: {}", a, b))
524 | .collect::>()
525 | }
526 |
527 | pub fn finalize_log(mut log: Vec, data: Vec) -> String {
528 | let event_schema: Vec = vec![
529 | "maker".to_string(),
530 | "taker".to_string(),
531 | "price".to_string(),
532 | "side".to_string(),
533 | "quantity".to_string(),
534 | ];
535 | log.extend_from_slice(
536 | &event_schema
537 | .iter()
538 | .zip(data.iter())
539 | .map(|(a, b)| format!("{}: {}", a, b))
540 | .collect::>(),
541 | );
542 | log.join(", ")
543 | }
544 |
--------------------------------------------------------------------------------
/src/lib/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod helpers;
2 | pub mod processor;
3 |
--------------------------------------------------------------------------------
/src/lib/processor/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod process_claim_seat;
2 | pub mod process_evict_seat;
3 | pub mod process_get_all_markets;
4 | pub mod process_get_book_levels;
5 | pub mod process_get_full_book;
6 | pub mod process_get_market;
7 | pub mod process_get_market_status;
8 | pub mod process_get_open_orders;
9 | pub mod process_get_seat_info;
10 | pub mod process_get_seat_manager_info;
11 | pub mod process_get_top_of_book;
12 | pub mod process_get_traders_for_market;
13 | pub mod process_get_transaction;
14 | pub mod process_mint_tokens;
15 | pub mod process_mint_tokens_for_market;
16 | pub mod process_request_seat;
17 |
--------------------------------------------------------------------------------
/src/lib/processor/process_claim_seat.rs:
--------------------------------------------------------------------------------
1 | use ellipsis_client::EllipsisClient;
2 | use phoenix_sdk::utils::create_claim_seat_ix_if_needed;
3 | use solana_sdk::{pubkey::Pubkey, signer::Signer};
4 |
5 | pub async fn process_claim_seat(
6 | client: &EllipsisClient,
7 | market_pubkey: &Pubkey,
8 | ) -> anyhow::Result<()> {
9 | let claim_seat_ix =
10 | create_claim_seat_ix_if_needed(client, market_pubkey, &client.payer.pubkey()).await?;
11 | println!("Claiming seat for pubkey: {}", client.payer.pubkey());
12 |
13 | if !claim_seat_ix.is_empty() {
14 | let tx = client.sign_send_instructions(claim_seat_ix, vec![]).await?;
15 | println!("Claim seat transaction: {}", tx);
16 | } else {
17 | println!("Seat already created for pubkey: {}", client.payer.pubkey());
18 | }
19 |
20 | Ok(())
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/processor/process_evict_seat.rs:
--------------------------------------------------------------------------------
1 | use std::mem::size_of;
2 |
3 | use ellipsis_client::EllipsisClient;
4 | use phoenix::program::MarketHeader;
5 | use phoenix_sdk::utils::get_evictable_trader_ix;
6 | use phoenix_seat_manager::instruction_builders::{
7 | create_evict_seat_instruction, EvictTraderAccountBackup,
8 | };
9 | use solana_sdk::pubkey::Pubkey;
10 |
11 | pub async fn process_evict_seat(
12 | client: &EllipsisClient,
13 | market_pubkey: &Pubkey,
14 | trader_to_evict: &Option,
15 | ) -> anyhow::Result<()> {
16 | let market_bytes = client.get_account_data(market_pubkey).await?;
17 | let (header_bytes, _market_bytes) = market_bytes.split_at(size_of::());
18 | let market_header = bytemuck::try_from_bytes::(header_bytes)
19 | .map_err(|e| anyhow::anyhow!("Error deserializing market header. Error: {:?}", e))?;
20 |
21 | let maybe_evict_trader_ix = if let Some(trader_pubkey) = trader_to_evict {
22 | let evict_trader_state = EvictTraderAccountBackup {
23 | trader_pubkey: *trader_pubkey,
24 | base_token_account_backup: None,
25 | quote_token_account_backup: None,
26 | };
27 | Some(create_evict_seat_instruction(
28 | market_pubkey,
29 | &market_header.base_params.mint_key,
30 | &market_header.quote_params.mint_key,
31 | trader_pubkey,
32 | vec![evict_trader_state],
33 | ))
34 | } else {
35 | get_evictable_trader_ix(client, market_pubkey).await?
36 | };
37 |
38 | if let Some(evict_trader_ix) = maybe_evict_trader_ix {
39 | println!("Evicting trader: {}", evict_trader_ix.accounts[13].pubkey);
40 | let tx = client
41 | .sign_send_instructions(vec![evict_trader_ix], vec![])
42 | .await?;
43 | println!("Evict trader tx: {}", tx);
44 | } else {
45 | println!("Cannot evict a trader when the market's trader state is not full.");
46 | return Ok(());
47 | }
48 |
49 | Ok(())
50 | }
51 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_all_markets.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::{market_helpers::get_all_markets, print_helpers::print_market_summary_data};
2 | use anyhow::anyhow;
3 | use ellipsis_client::EllipsisClient;
4 | use phoenix::program::MarketHeader;
5 | use phoenix_sdk::sdk_client::SDKClient;
6 | use serde::{Deserialize, Serialize};
7 | use solana_sdk::pubkey::Pubkey;
8 | use std::collections::HashMap;
9 | use std::{mem::size_of, str::FromStr};
10 |
11 | pub async fn process_get_all_markets(client: &EllipsisClient) -> anyhow::Result<()> {
12 | let config = get_phoenix_config(client).await?;
13 | let accounts = get_all_markets(client).await?;
14 |
15 | println!("Found {} market(s)", accounts.len());
16 |
17 | //Deserialize market accounts and print summary information
18 | for (market_pubkey, mut market_account) in accounts {
19 | let (header_bytes, _market_bytes) =
20 | market_account.data.split_at_mut(size_of::());
21 |
22 | let header = bytemuck::try_from_bytes::(header_bytes)
23 | .map_err(|e| anyhow!("Error getting market header. Error: {:?}", e))?;
24 |
25 | let (base_mint_symbol, quote_mint_symbol) = get_base_and_quote_symbols(&config, header);
26 | print_market_summary_data(&market_pubkey, header, base_mint_symbol, quote_mint_symbol);
27 | }
28 | Ok(())
29 | }
30 |
31 | pub fn get_base_and_quote_symbols(
32 | config: &MasterConfig,
33 | header: &MarketHeader,
34 | ) -> (Option, Option) {
35 | let base_mint = header.base_params.mint_key;
36 | let quote_mint = header.quote_params.mint_key;
37 | (
38 | config
39 | .tokens
40 | .iter()
41 | .find(|t| t.mint == base_mint.to_string())
42 | .map(|t| t.symbol.clone()),
43 | config
44 | .tokens
45 | .iter()
46 | .find(|t| t.mint == quote_mint.to_string())
47 | .map(|t| t.symbol.clone()),
48 | )
49 | }
50 |
51 | pub async fn process_get_all_markets_no_gpa(
52 | client: &EllipsisClient,
53 | network_url: &str,
54 | ) -> anyhow::Result<()> {
55 | let config = get_phoenix_config(client).await?;
56 | let markets = config
57 | .markets
58 | .iter()
59 | .map(|m| m.market.clone())
60 | .collect::>()
61 | .clone();
62 |
63 | println!("Found {} market(s)", markets.len());
64 |
65 | for market in markets {
66 | let market_pubkey = Pubkey::from_str(&market)?;
67 | let sdk = SDKClient::new(&client.payer, network_url).await?;
68 |
69 | let market_account_data = sdk.client.get_account_data(&market_pubkey).await?;
70 | let (header_bytes, _market_bytes) = market_account_data.split_at(size_of::());
71 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
72 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
73 |
74 | let (base_mint_symbol, quote_mint_symbol) = get_base_and_quote_symbols(&config, header);
75 | print_market_summary_data(&market_pubkey, header, base_mint_symbol, quote_mint_symbol);
76 | }
77 | Ok(())
78 | }
79 |
80 | #[derive(Serialize, Deserialize, Clone, Debug)]
81 | pub struct MasterConfig {
82 | pub tokens: Vec,
83 | pub markets: Vec,
84 | }
85 |
86 | #[derive(Serialize, Deserialize, Clone, Debug)]
87 | #[serde(rename_all = "camelCase")]
88 | pub struct TokenConfig {
89 | pub name: String,
90 | pub symbol: String,
91 | pub mint: String,
92 | pub logo_uri: String,
93 | }
94 |
95 | #[derive(Serialize, Deserialize, Clone, Debug)]
96 | #[serde(rename_all = "camelCase")]
97 | pub struct MarketConfig {
98 | pub market: String,
99 | pub base_mint: String,
100 | pub quote_mint: String,
101 | }
102 |
103 | pub async fn get_phoenix_config(client: &EllipsisClient) -> anyhow::Result {
104 | let genesis: solana_sdk::hash::Hash = client.get_genesis_hash().await?;
105 |
106 | //hardcoded in the genesis hashes for mainnet and devnet
107 | let cluster = match genesis.to_string().as_str() {
108 | "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d" => "mainnet-beta",
109 | "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG" => "devnet",
110 | _ => "localhost",
111 | };
112 |
113 | if cluster == "localhost" {
114 | return Ok(MasterConfig {
115 | tokens: vec![],
116 | markets: vec![],
117 | });
118 | }
119 |
120 | let body = reqwest::get(
121 | "https://raw.githubusercontent.com/Ellipsis-Labs/phoenix-sdk/master/master_config.json",
122 | )
123 | .await?
124 | .text()
125 | .await?;
126 |
127 | let config: HashMap = serde_json::from_str(&body)?;
128 |
129 | Ok(config
130 | .get(cluster)
131 | .ok_or_else(|| anyhow!("Failed to find market config"))?
132 | .clone())
133 | }
134 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_book_levels.rs:
--------------------------------------------------------------------------------
1 | use std::mem::size_of;
2 |
3 | use phoenix::{
4 | program::{load_with_dispatch, MarketHeader},
5 | quantities::WrapperU64,
6 | state::{markets::RestingOrder, Side},
7 | };
8 | use phoenix_sdk::sdk_client::*;
9 | use solana_sdk::{clock::Clock, commitment_config::CommitmentConfig, pubkey::Pubkey, sysvar};
10 |
11 | use crate::helpers::print_helpers::{print_book_with_trader, LadderLevelEntry};
12 |
13 | pub async fn process_get_book_levels(
14 | market_pubkey: &Pubkey,
15 | sdk: &SDKClient,
16 | levels: u64,
17 | ) -> anyhow::Result<()> {
18 | let mut ask_entries: Vec = Vec::with_capacity(levels as usize);
19 | let mut bid_entries: Vec = Vec::with_capacity(levels as usize);
20 |
21 | // Get market account
22 | let mut market_and_clock = sdk
23 | .client
24 | .get_multiple_accounts_with_commitment(
25 | &[*market_pubkey, sysvar::clock::id()],
26 | CommitmentConfig::confirmed(),
27 | )
28 | .await?
29 | .value;
30 |
31 | let market_account_data = market_and_clock
32 | .remove(0)
33 | .ok_or_else(|| anyhow::Error::msg("Market account not found"))?
34 | .data;
35 |
36 | let clock_account_data = market_and_clock
37 | .remove(0)
38 | .ok_or_else(|| anyhow::Error::msg("Clock account not found"))?
39 | .data;
40 |
41 | let clock: Clock = bincode::deserialize(&clock_account_data)
42 | .map_err(|_| anyhow::Error::msg("Error deserializing clock"))?;
43 |
44 | let (header_bytes, market_bytes) = market_account_data.split_at(size_of::());
45 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
46 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
47 |
48 | // Derserialize data and load into correct type
49 | let market = load_with_dispatch(&header.market_size_params, market_bytes)?.inner;
50 |
51 | // If not present, use u32::MAX instead of aborting.
52 | // This will simply not print any markers.
53 | let trader_index = market.get_trader_index(&sdk.trader).unwrap_or(u32::MAX);
54 | let book_bids = market.get_book(Side::Bid);
55 | let book_asks = market.get_book(Side::Ask);
56 |
57 | let mut open_bids = vec![];
58 | open_bids.push(format!(
59 | "{0: <20} | {1: <20} | {2: <10} | {3: <10} | {4: <15} | {5: <15} ",
60 | "ID", "Price (ticks)", "Price", "Quantity", "Slots Remaining", "Seconds Remaining"
61 | ));
62 |
63 | for (order_id, order) in book_bids.iter() {
64 | // Check if order is expired
65 | if order.is_expired(clock.slot, clock.unix_timestamp as u64) {
66 | continue;
67 | }
68 |
69 | // Check if entry is present
70 | if let Some(ref mut entry) = bid_entries
71 | .iter_mut()
72 | .find(|entry| entry.tick == order_id.price_in_ticks)
73 | {
74 | // If entry is present, add to amount
75 | entry.lots += order.num_base_lots.as_u64();
76 |
77 | // Flag trader if present
78 | entry.trader_present |= order.trader_index == trader_index as u64;
79 | }
80 |
81 | // Otherwise, check length before attempting to add entry
82 | if bid_entries.len() < levels as usize {
83 | bid_entries.push(LadderLevelEntry {
84 | tick: order_id.price_in_ticks.as_u64(),
85 | lots: order.num_base_lots.as_u64(),
86 | trader_present: order.trader_index == trader_index as u64,
87 | })
88 | } else {
89 | break;
90 | }
91 | }
92 |
93 | for (order_id, order) in book_asks.iter() {
94 | // Check if order is expired
95 | if order.is_expired(clock.slot, clock.unix_timestamp as u64) {
96 | continue;
97 | }
98 |
99 | // Check if entry is present
100 | if let Some(ref mut entry) = ask_entries
101 | .iter_mut()
102 | .find(|entry| entry.tick == order_id.price_in_ticks)
103 | {
104 | // If entry is present, add to amount
105 | entry.lots += order.num_base_lots.as_u64();
106 |
107 | // Flag trader if present
108 | entry.trader_present |= order.trader_index == trader_index as u64;
109 | }
110 |
111 | // Otherwise, check length before attempting to add entry
112 | if ask_entries.len() < levels as usize {
113 | ask_entries.push(LadderLevelEntry {
114 | tick: order_id.price_in_ticks.as_u64(),
115 | lots: order.num_base_lots.as_u64(),
116 | trader_present: order.trader_index == trader_index as u64,
117 | })
118 | } else {
119 | break;
120 | }
121 | }
122 |
123 | print_book_with_trader(sdk, market_pubkey, &bid_entries, &ask_entries)?;
124 |
125 | Ok(())
126 | }
127 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_full_book.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::market_helpers::*;
2 | use crate::helpers::print_helpers::print_book;
3 | use phoenix_sdk::sdk_client::*;
4 | use solana_sdk::pubkey::Pubkey;
5 |
6 | pub async fn process_get_full_book(market_pubkey: &Pubkey, sdk: &SDKClient) -> anyhow::Result<()> {
7 | let book = get_book_levels(market_pubkey, &sdk.client, u64::MAX).await?;
8 | if book.bids.is_empty() && book.asks.is_empty() {
9 | println!("Book is empty");
10 | } else {
11 | print_book(sdk, market_pubkey, &book)?;
12 | }
13 | Ok(())
14 | }
15 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_market.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::print_helpers::*;
2 | use phoenix::program::{load_with_dispatch, MarketHeader};
3 | use phoenix_sdk::sdk_client::*;
4 | use solana_sdk::pubkey::Pubkey;
5 | use std::mem::size_of;
6 |
7 | use super::process_get_all_markets::{get_base_and_quote_symbols, get_phoenix_config};
8 |
9 | pub async fn process_get_market(market_pubkey: &Pubkey, sdk: &SDKClient) -> anyhow::Result<()> {
10 | let market_metadata = sdk.get_market_metadata(market_pubkey).await?;
11 | let market_account_data = sdk.client.get_account_data(market_pubkey).await?;
12 | let (header_bytes, market_bytes) = market_account_data.split_at(size_of::());
13 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
14 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
15 |
16 | // Derserialize data and load into correct type
17 | let market = load_with_dispatch(&header.market_size_params, market_bytes)
18 | .map_err(|e| anyhow::anyhow!("Failed to load market. Error {:?}", e))?
19 | .inner;
20 |
21 | let taker_fees = market.get_taker_fee_bps();
22 |
23 | let (base_mint_symbol, quote_mint_symbol) =
24 | if let Ok(config) = get_phoenix_config(&sdk.client).await {
25 | get_base_and_quote_symbols(&config, header)
26 | } else {
27 | (None, None)
28 | };
29 |
30 | print_market_details(
31 | sdk,
32 | market_pubkey,
33 | &market_metadata,
34 | header,
35 | taker_fees,
36 | base_mint_symbol,
37 | quote_mint_symbol,
38 | )
39 | .await
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_market_status.rs:
--------------------------------------------------------------------------------
1 | use phoenix::program::{status::MarketStatus, MarketHeader};
2 | use phoenix_sdk::sdk_client::*;
3 | use solana_sdk::pubkey::Pubkey;
4 | use std::mem::size_of;
5 |
6 | pub async fn process_get_market_status(
7 | market_pubkey: &Pubkey,
8 | sdk: &SDKClient,
9 | ) -> anyhow::Result<()> {
10 | // Get market account
11 | let mut market_account_data = sdk.client.get_account_data(market_pubkey).await?;
12 | let (header_bytes, _) = market_account_data.split_at_mut(size_of::());
13 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
14 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
15 |
16 | let status = MarketStatus::from(header.status);
17 | println!("Market status: {}", status);
18 | Ok(())
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_open_orders.rs:
--------------------------------------------------------------------------------
1 | use phoenix::program::{load_with_dispatch, MarketHeader};
2 | use phoenix::quantities::WrapperU64;
3 | use phoenix::state::markets::{FIFOOrderId, FIFORestingOrder, RestingOrder};
4 | use phoenix::state::Side;
5 | use phoenix_sdk::sdk_client::*;
6 | use solana_sdk::clock::Clock;
7 | use solana_sdk::commitment_config::CommitmentConfig;
8 | use solana_sdk::pubkey::Pubkey;
9 | use solana_sdk::sysvar;
10 | use std::mem::size_of;
11 |
12 | use crate::helpers::print_helpers::get_precision;
13 |
14 | pub async fn process_get_open_orders(
15 | market_pubkey: &Pubkey,
16 | trader_pubkey: &Pubkey,
17 | sdk: &SDKClient,
18 | ) -> anyhow::Result<()> {
19 | let meta = sdk.get_market_metadata(market_pubkey).await?;
20 | // Get market account
21 | let mut market_and_clock = sdk
22 | .client
23 | .get_multiple_accounts_with_commitment(
24 | &[*market_pubkey, sysvar::clock::id()],
25 | CommitmentConfig::confirmed(),
26 | )
27 | .await?
28 | .value;
29 |
30 | let market_account_data = market_and_clock
31 | .remove(0)
32 | .ok_or_else(|| anyhow::Error::msg("Market account not found"))?
33 | .data;
34 |
35 | let clock_account_data = market_and_clock
36 | .remove(0)
37 | .ok_or_else(|| anyhow::Error::msg("Clock account not found"))?
38 | .data;
39 |
40 | let clock: Clock = bincode::deserialize(&clock_account_data)
41 | .map_err(|_| anyhow::Error::msg("Error deserializing clock"))?;
42 |
43 | let (header_bytes, market_bytes) = market_account_data.split_at(size_of::());
44 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
45 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
46 |
47 | // Derserialize data and load into correct type
48 | let market = load_with_dispatch(&header.market_size_params, market_bytes)?.inner;
49 |
50 | let raw_base_units_per_base_lot =
51 | meta.base_atoms_per_base_lot as f64 / meta.base_atoms_per_raw_base_unit as f64;
52 |
53 | let trader_index = market
54 | .get_trader_index(trader_pubkey)
55 | .ok_or_else(|| anyhow::anyhow!("Trader not found"))?;
56 | let book_bids = market.get_book(Side::Bid);
57 | let book_asks = market.get_book(Side::Ask);
58 | let price_precision: usize = get_precision(
59 | 10_u64.pow(meta.quote_decimals) / meta.tick_size_in_quote_atoms_per_base_unit,
60 | );
61 | let size_precision: usize = get_precision(meta.num_base_lots_per_base_unit);
62 |
63 | println!("Open Bids");
64 | let mut open_bids = vec![];
65 | open_bids.push(format!(
66 | "{0: <20} | {1: <20} | {2: <10} | {3: <10} | {4: <15} | {5: <15} ",
67 | "ID", "Price (ticks)", "Price", "Quantity", "Slots Remaining", "Seconds Remaining"
68 | ));
69 | for (order_id, order) in book_bids.iter() {
70 | if order.trader_index as u32 == trader_index {
71 | if order.is_expired(clock.slot, clock.unix_timestamp as u64) {
72 | continue;
73 | }
74 | open_bids.push(format_open_orders(
75 | sdk,
76 | market_pubkey,
77 | order_id,
78 | order,
79 | price_precision,
80 | size_precision,
81 | &clock,
82 | raw_base_units_per_base_lot,
83 | )?);
84 | }
85 | }
86 | open_bids.iter().for_each(|line| println!("{}", line));
87 |
88 | println!();
89 | println!("Open Asks");
90 | let mut open_asks = vec![];
91 | open_asks.push(format!(
92 | "{0: <20} | {1: <20} | {2: <10} | {3: <10} | {4: <15} | {5: <15} ",
93 | "ID", "Price (ticks)", "Price", "Quantity", "Slots Remaining", "Seconds Remaining"
94 | ));
95 | for (order_id, order) in book_asks.iter() {
96 | if order.trader_index as u32 == trader_index {
97 | if order.is_expired(clock.slot, clock.unix_timestamp as u64) {
98 | continue;
99 | }
100 | open_asks.push(format_open_orders(
101 | sdk,
102 | market_pubkey,
103 | order_id,
104 | order,
105 | price_precision,
106 | size_precision,
107 | &clock,
108 | raw_base_units_per_base_lot,
109 | )?);
110 | }
111 | }
112 | open_asks.iter().for_each(|line| println!("{}", line));
113 |
114 | Ok(())
115 | }
116 |
117 | #[allow(clippy::too_many_arguments)]
118 | fn format_open_orders(
119 | sdk: &SDKClient,
120 | market_pubkey: &Pubkey,
121 | order_id: &FIFOOrderId,
122 | order: &FIFORestingOrder,
123 | price_precision: usize,
124 | size_precision: usize,
125 | clock: &Clock,
126 | raw_base_units_per_base_lot: f64,
127 | ) -> anyhow::Result {
128 | Ok(format!(
129 | "{0: <20} | {1: <20} | {2: <10} | {3: <10} | {4: <15} | {5: <15} ",
130 | order_id.order_sequence_number as i64,
131 | order_id.price_in_ticks,
132 | format!(
133 | "{:.1$}",
134 | sdk.ticks_to_float_price(market_pubkey, order_id.price_in_ticks.as_u64())?,
135 | price_precision
136 | ),
137 | format!(
138 | "{:.1$}",
139 | order.num_base_lots.as_u64() as f64 * raw_base_units_per_base_lot,
140 | size_precision,
141 | ),
142 | if order.last_valid_slot >= clock.slot {
143 | (1 + order.last_valid_slot - clock.slot).to_string()
144 | } else {
145 | "∞".to_string()
146 | },
147 | if order.last_valid_unix_timestamp_in_seconds >= clock.unix_timestamp as u64 {
148 | (1 + order.last_valid_unix_timestamp_in_seconds - clock.unix_timestamp as u64)
149 | .to_string()
150 | } else {
151 | "∞".to_string()
152 | }
153 | ))
154 | }
155 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_seat_info.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::market_helpers::*;
2 | use phoenix_sdk::sdk_client::*;
3 | use solana_sdk::pubkey::Pubkey;
4 |
5 | pub async fn process_get_seat_info(
6 | market_pubkey: &Pubkey,
7 | trader_pubkey: &Pubkey,
8 | sdk: &SDKClient,
9 | ) -> anyhow::Result<()> {
10 | let (seat_address, _) = Pubkey::find_program_address(
11 | &[b"seat", market_pubkey.as_ref(), trader_pubkey.as_ref()],
12 | &phoenix::ID,
13 | );
14 | println!("Seat address: {}", seat_address);
15 | let status = get_seat_status(sdk, &seat_address).await;
16 | match status {
17 | Ok(status) => println!("Seat status: {}", status),
18 | _ => println!("Seat status not found"),
19 | }
20 | Ok(())
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_seat_manager_info.rs:
--------------------------------------------------------------------------------
1 | use std::mem::size_of;
2 |
3 | use ellipsis_client::EllipsisClient;
4 | use phoenix::program::MarketHeader;
5 | use phoenix_seat_manager::{get_seat_manager_address, seat_manager::SeatManager};
6 | use solana_sdk::pubkey::Pubkey;
7 |
8 | use crate::helpers::market_helpers::get_seat_manager_data_with_market;
9 |
10 | pub async fn process_get_seat_manager_info(
11 | client: &EllipsisClient,
12 | market_pubkey: &Pubkey,
13 | ) -> anyhow::Result<()> {
14 | let seat_manager_address = get_seat_manager_address(market_pubkey).0;
15 | let market_data = client.get_account_data(market_pubkey).await?;
16 | let market_header =
17 | bytemuck::from_bytes::(market_data.split_at(size_of::()).0);
18 | if market_header.authority != seat_manager_address {
19 | println!(
20 | "Authority for Market {} is not the seat manager.",
21 | market_pubkey
22 | );
23 | println!("Market authority: {}", market_header.authority);
24 | println!("Seat manager address: {}", seat_manager_address);
25 | return Ok(());
26 | }
27 | let seat_manager_info = get_seat_manager_data_with_market(client, market_pubkey).await?;
28 | print_seat_manager_struct(&seat_manager_info, &seat_manager_address);
29 | Ok(())
30 | }
31 |
32 | pub fn print_seat_manager_struct(seat_manager: &SeatManager, seat_manager_pubkey: &Pubkey) {
33 | println!("Seat Manager Address: {}", seat_manager_pubkey);
34 | println!("SM Market: {}", seat_manager.market);
35 | println!("SM Authority: {}", seat_manager.authority);
36 | println!("SM Successor: {}", seat_manager.successor);
37 | println!(
38 | "Number of designated market makers: {}",
39 | seat_manager.num_makers
40 | );
41 |
42 | let dmms: Vec<&Pubkey> = seat_manager
43 | .designated_market_makers
44 | .iter()
45 | .filter(|&&dmm| dmm != Pubkey::default())
46 | .collect();
47 | if !dmms.is_empty() {
48 | println!("DMMs: {:?}", dmms);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_top_of_book.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::market_helpers::*;
2 | use crate::helpers::print_helpers::print_book;
3 | use phoenix_sdk::sdk_client::*;
4 | use solana_sdk::pubkey::Pubkey;
5 |
6 | pub async fn process_get_top_of_book(
7 | market_pubkey: &Pubkey,
8 | sdk: &SDKClient,
9 | ) -> anyhow::Result<()> {
10 | let book = get_book_levels(market_pubkey, &sdk.client, 1).await?;
11 | if book.bids.is_empty() && book.asks.is_empty() {
12 | println!("Book is empty");
13 | } else {
14 | print_book(sdk, market_pubkey, &book)?;
15 | }
16 |
17 | Ok(())
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_traders_for_market.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::print_helpers::*;
2 | use phoenix::program::{load_with_dispatch, MarketHeader};
3 | use phoenix_sdk::sdk_client::*;
4 | use solana_sdk::pubkey::Pubkey;
5 | use std::mem::size_of;
6 |
7 | pub async fn process_get_traders_for_market(
8 | market_pubkey: &Pubkey,
9 | sdk: &SDKClient,
10 | ) -> anyhow::Result<()> {
11 | // Get market account
12 | let mut market_account_data = sdk.client.get_account_data(market_pubkey).await?;
13 | let (header_bytes, market_bytes) = market_account_data.split_at_mut(size_of::());
14 | let header: &MarketHeader = bytemuck::try_from_bytes(header_bytes)
15 | .map_err(|e| anyhow::anyhow!("Error getting market header. Error: {:?}", e))?;
16 |
17 | // Derserialize data and load into correct type
18 | let market = load_with_dispatch(&header.market_size_params, market_bytes)
19 | .map_err(|e| anyhow::anyhow!("Failed to load market. Error {:?}", e))?
20 | .inner;
21 |
22 | println!(
23 | "Found {} trader(s). Printing traders with locked or free lots",
24 | market.get_registered_traders().len()
25 | );
26 |
27 | // Print trader information
28 | for (pubkey, state) in market.get_registered_traders().iter() {
29 | print_trader_state(sdk, market_pubkey, pubkey, state)?;
30 | }
31 |
32 | Ok(())
33 | }
34 |
--------------------------------------------------------------------------------
/src/lib/processor/process_get_transaction.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::print_helpers::*;
2 | use phoenix_sdk::sdk_client::*;
3 | use solana_sdk::signature::Signature;
4 |
5 | pub async fn process_get_transaction(
6 | signature: &Signature,
7 | sdk: &mut SDKClient,
8 | ) -> anyhow::Result<()> {
9 | let events = sdk
10 | .parse_events_from_transaction(signature)
11 | .await
12 | .ok_or_else(|| anyhow::anyhow!("Failed to parse events from transaction"))?;
13 | log_market_events(sdk, events).await?;
14 | Ok(())
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/processor/process_mint_tokens.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::devnet_helpers::*;
2 | use ellipsis_client::EllipsisClient;
3 | use solana_sdk::pubkey::Pubkey;
4 | use solana_sdk::signature::Keypair;
5 | use solana_sdk::signature::Signer;
6 |
7 | // Only valid for sandbox devnet markets
8 | pub async fn process_mint_tokens(
9 | client: &EllipsisClient,
10 | payer: &Keypair,
11 | recipient_pubkey: &Pubkey,
12 | mint_ticker: String,
13 | amount: u64,
14 | ) -> anyhow::Result<()> {
15 | let mut instructions = vec![];
16 |
17 | let mint_pda = find_or_create_devnet_mint(
18 | client,
19 | &mint_ticker,
20 | 9, //Decimals only used in creating mint. No effect if mint already exists
21 | )
22 | .await?;
23 |
24 | // Get or create the ATA for the recipient. If doesn't exist, create token account
25 | let recipient_ata =
26 | spl_associated_token_account::get_associated_token_address(recipient_pubkey, &mint_pda);
27 |
28 | if client.get_account(&recipient_ata).await.is_err() {
29 | println!("Creating ATA");
30 | instructions.push(
31 | spl_associated_token_account::instruction::create_associated_token_account(
32 | &payer.pubkey(),
33 | recipient_pubkey,
34 | &mint_pda,
35 | &spl_token::id(),
36 | ),
37 | )
38 | };
39 |
40 | // Call devnet-token-faucet airdrop spl instruction
41 | instructions.push(devnet_token_faucet::airdrop_spl_with_ticker_ix(
42 | &devnet_token_faucet::id(),
43 | mint_ticker,
44 | recipient_pubkey,
45 | amount,
46 | ));
47 |
48 | client
49 | .sign_send_instructions(instructions, vec![payer])
50 | .await?;
51 |
52 | println!(
53 | "{} Tokens minted! Mint pubkey: {}, Recipient address: {}",
54 | amount, mint_pda, recipient_pubkey
55 | );
56 |
57 | Ok(())
58 | }
59 |
--------------------------------------------------------------------------------
/src/lib/processor/process_mint_tokens_for_market.rs:
--------------------------------------------------------------------------------
1 | use phoenix_sdk::sdk_client::*;
2 | use solana_sdk::program_pack::Pack;
3 | use solana_sdk::pubkey::Pubkey;
4 | use solana_sdk::signature::Signer;
5 | use spl_token::state::Mint;
6 |
7 | use crate::helpers::devnet_helpers::devnet_token_faucet;
8 |
9 | // Only valid for sandbox devnet markets
10 | pub async fn process_mint_tokens_for_market(
11 | sdk: &SDKClient,
12 | market_pubkey: &Pubkey,
13 | recipient_pubkey: &Pubkey,
14 | base_amount: u64,
15 | quote_amount: u64,
16 | ) -> anyhow::Result<()> {
17 | // Get base and quote mints from market metadata
18 | let market_metadata = sdk.get_market_metadata(market_pubkey).await?;
19 | let base_mint = market_metadata.base_mint;
20 | let quote_mint = market_metadata.quote_mint;
21 |
22 | let base_mint_account = Mint::unpack(&sdk.client.get_account_data(&base_mint).await?)?;
23 | let quote_mint_account = Mint::unpack(&sdk.client.get_account_data("e_mint).await?)?;
24 |
25 | let quote_mint_authority = quote_mint_account
26 | .mint_authority
27 | .ok_or_else(|| anyhow::anyhow!("Quote mint authority is not set. Cannot mint tokens"))?;
28 | let base_mint_authority = base_mint_account
29 | .mint_authority
30 | .ok_or_else(|| anyhow::anyhow!("Base mint authority is not set. Cannot mint tokens"))?;
31 |
32 | if sdk.client.get_account("e_mint_authority).await?.owner != devnet_token_faucet::ID {
33 | return Err(anyhow::anyhow!(
34 | "Quote mint authority is not owned by devnet-token-faucet"
35 | ));
36 | }
37 |
38 | if sdk.client.get_account(&base_mint_authority).await?.owner != devnet_token_faucet::ID {
39 | return Err(anyhow::anyhow!(
40 | "Base mint authority is not owned by devnet-token-faucet"
41 | ));
42 | }
43 |
44 | // Get or create the ATA for the recipient. If doesn't exist, create token account
45 | let mut instructions = vec![];
46 |
47 | let recipient_ata_base =
48 | spl_associated_token_account::get_associated_token_address(recipient_pubkey, &base_mint);
49 |
50 | if sdk.client.get_account(&recipient_ata_base).await.is_err() {
51 | println!("Creating ATA for base token");
52 | instructions.push(
53 | spl_associated_token_account::instruction::create_associated_token_account(
54 | &sdk.client.payer.pubkey(),
55 | recipient_pubkey,
56 | &base_mint,
57 | &spl_token::id(),
58 | ),
59 | )
60 | };
61 |
62 | let recipient_ata_quote =
63 | spl_associated_token_account::get_associated_token_address(recipient_pubkey, "e_mint);
64 |
65 | if sdk.client.get_account(&recipient_ata_quote).await.is_err() {
66 | println!("Creating ATA for quote token");
67 | instructions.push(
68 | spl_associated_token_account::instruction::create_associated_token_account(
69 | &sdk.client.payer.pubkey(),
70 | recipient_pubkey,
71 | "e_mint,
72 | &spl_token::id(),
73 | ),
74 | )
75 | };
76 |
77 | instructions.push(devnet_token_faucet::airdrop_spl_with_mint_pdas_ix(
78 | &devnet_token_faucet::id(),
79 | &base_mint,
80 | &base_mint_authority,
81 | recipient_pubkey,
82 | base_amount,
83 | ));
84 |
85 | instructions.push(devnet_token_faucet::airdrop_spl_with_mint_pdas_ix(
86 | &devnet_token_faucet::id(),
87 | "e_mint,
88 | "e_mint_authority,
89 | recipient_pubkey,
90 | quote_amount,
91 | ));
92 | let signature = sdk
93 | .client
94 | .sign_send_instructions(instructions, vec![])
95 | .await?;
96 | println!("Tokens minted! Signature: {}", signature);
97 |
98 | Ok(())
99 | }
100 |
--------------------------------------------------------------------------------
/src/lib/processor/process_request_seat.rs:
--------------------------------------------------------------------------------
1 | use phoenix::program::instruction_builders::create_request_seat_instruction;
2 | use phoenix_sdk::sdk_client::*;
3 | use solana_sdk::pubkey::Pubkey;
4 |
5 | pub async fn process_request_seat(market_pubkey: &Pubkey, sdk: &SDKClient) -> anyhow::Result<()> {
6 | let ix = create_request_seat_instruction(&sdk.core.trader, market_pubkey);
7 | let tx = sdk.client.sign_send_instructions(vec![ix], vec![]).await;
8 |
9 | match tx {
10 | Ok(tx) => println!("Requested seat, transaction signature: {}", tx),
11 | Err(e) => println!("Error requesting seat: {}", e),
12 | }
13 |
14 | Ok(())
15 | }
16 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | mod command;
2 |
3 | use crate::command::PhoenixCLICommand;
4 | use anyhow::anyhow;
5 | use clap::Parser;
6 | use ellipsis_client::EllipsisClient;
7 | use phoenix_cli_processor::processor::process_claim_seat::process_claim_seat;
8 | use phoenix_cli_processor::processor::process_evict_seat::process_evict_seat;
9 | use phoenix_cli_processor::processor::{
10 | process_get_all_markets::*, process_get_book_levels::*, process_get_full_book::*,
11 | process_get_market::*, process_get_market_status::*, process_get_open_orders::*,
12 | process_get_seat_info::*, process_get_seat_manager_info::*, process_get_top_of_book::*,
13 | process_get_traders_for_market::*, process_get_transaction::*, process_mint_tokens::*,
14 | process_mint_tokens_for_market::*, process_request_seat::*,
15 | };
16 | use phoenix_sdk::sdk_client::*;
17 | use solana_cli_config::{Config, ConfigInput, CONFIG_FILE};
18 | use solana_client::nonblocking::rpc_client::RpcClient;
19 | use solana_sdk::signer::keypair::{read_keypair_file, Keypair};
20 | use solana_sdk::signer::Signer;
21 |
22 | #[derive(Parser)]
23 | #[command(author, version, about)]
24 | struct Args {
25 | #[clap(subcommand)]
26 | command: PhoenixCLICommand,
27 | /// Optionally include your RPC endpoint. Use "local", "dev", "main" for default endpoints. Defaults to your Solana CLI config file.
28 | #[clap(global = true, short, long)]
29 | url: Option,
30 | /// Optionally include your keypair path. Defaults to your Solana CLI config file.
31 | #[clap(global = true, short, long)]
32 | keypair_path: Option,
33 | /// Optionally include a commitment level. Defaults to your Solana CLI config file.
34 | #[clap(global = true, short, long)]
35 | commitment: Option,
36 | }
37 |
38 | pub fn get_network(network_str: &str) -> &str {
39 | match network_str {
40 | "devnet" | "dev" | "d" => "https://api.devnet.solana.com",
41 | "mainnet" | "main" | "m" | "mainnet-beta" => "https://api.mainnet-beta.solana.com",
42 | "localnet" | "localhost" | "l" | "local" => "http://localhost:8899",
43 | _ => network_str,
44 | }
45 | }
46 |
47 | pub fn get_payer_keypair_from_path(path: &str) -> anyhow::Result {
48 | read_keypair_file(&*shellexpand::tilde(path)).map_err(|e| anyhow!(e.to_string()))
49 | }
50 |
51 | #[tokio::main]
52 | async fn main() -> anyhow::Result<()> {
53 | let cli = Args::parse();
54 | let config = match CONFIG_FILE.as_ref() {
55 | Some(config_file) => Config::load(config_file).unwrap_or_else(|_| {
56 | println!("Failed to load config file: {}", config_file);
57 | Config::default()
58 | }),
59 | None => Config::default(),
60 | };
61 | let commitment =
62 | ConfigInput::compute_commitment_config("", &cli.commitment.unwrap_or(config.commitment)).1;
63 | let payer = get_payer_keypair_from_path(&cli.keypair_path.unwrap_or(config.keypair_path))
64 | .expect("Keypair file does not exist. Please run `solana-keygen new`");
65 | let network_url = &get_network(&cli.url.unwrap_or(config.json_rpc_url)).to_string();
66 | let client = EllipsisClient::from_rpc(
67 | RpcClient::new_with_commitment(network_url.to_string(), commitment),
68 | &payer,
69 | )?;
70 |
71 | let mut sdk = SDKClient::new(&payer, network_url).await?;
72 |
73 | match cli.command {
74 | PhoenixCLICommand::GetMarket { market_pubkey } => {
75 | sdk.add_market(&market_pubkey).await?;
76 | process_get_market(&market_pubkey, &sdk).await?
77 | }
78 | PhoenixCLICommand::GetAllMarkets { no_gpa } => {
79 | if no_gpa {
80 | process_get_all_markets_no_gpa(&client, network_url).await?
81 | } else {
82 | process_get_all_markets(&client).await?
83 | }
84 | }
85 | PhoenixCLICommand::GetTradersForMarket { market_pubkey } => {
86 | sdk.add_market(&market_pubkey).await?;
87 | process_get_traders_for_market(&market_pubkey, &sdk).await?
88 | }
89 | PhoenixCLICommand::GetTopOfBook { market_pubkey } => {
90 | sdk.add_market(&market_pubkey).await?;
91 | process_get_top_of_book(&market_pubkey, &sdk).await?
92 | }
93 | PhoenixCLICommand::GetBookLevels {
94 | market_pubkey,
95 | levels,
96 | } => {
97 | sdk.add_market(&market_pubkey).await?;
98 | process_get_book_levels(&market_pubkey, &sdk, levels).await?
99 | }
100 | PhoenixCLICommand::GetFullBook { market_pubkey } => {
101 | sdk.add_market(&market_pubkey).await?;
102 | process_get_full_book(&market_pubkey, &sdk).await?
103 | }
104 | PhoenixCLICommand::GetTransaction { signature } => {
105 | process_get_transaction(&signature, &mut sdk).await?
106 | }
107 | PhoenixCLICommand::GetMarketStatus { market_pubkey } => {
108 | sdk.add_market(&market_pubkey).await?;
109 | process_get_market_status(&market_pubkey, &sdk).await?
110 | }
111 | PhoenixCLICommand::GetSeatInfo {
112 | market_pubkey,
113 | trader_pubkey,
114 | } => {
115 | sdk.add_market(&market_pubkey).await?;
116 | process_get_seat_info(
117 | &market_pubkey,
118 | &trader_pubkey.unwrap_or_else(|| payer.pubkey()),
119 | &sdk,
120 | )
121 | .await?
122 | }
123 | PhoenixCLICommand::GetOpenOrders {
124 | market_pubkey,
125 | trader_pubkey,
126 | } => {
127 | sdk.add_market(&market_pubkey).await?;
128 | process_get_open_orders(
129 | &market_pubkey,
130 | &trader_pubkey.unwrap_or_else(|| payer.pubkey()),
131 | &sdk,
132 | )
133 | .await?
134 | }
135 | PhoenixCLICommand::RequestSeat { market_pubkey } => {
136 | sdk.add_market(&market_pubkey).await?;
137 | process_request_seat(&market_pubkey, &sdk).await?
138 | }
139 | PhoenixCLICommand::MintTokens {
140 | mint_ticker,
141 | recipient_pubkey,
142 | amount,
143 | } => process_mint_tokens(&client, &payer, &recipient_pubkey, mint_ticker, amount).await?,
144 | PhoenixCLICommand::MintTokensForMarket {
145 | market_pubkey,
146 | recipient_pubkey,
147 | base_amount,
148 | quote_amount,
149 | } => {
150 | sdk.add_market(&market_pubkey).await?;
151 | process_mint_tokens_for_market(
152 | &sdk,
153 | &market_pubkey,
154 | &recipient_pubkey,
155 | base_amount,
156 | quote_amount,
157 | )
158 | .await?
159 | }
160 | PhoenixCLICommand::GetSeatManagerInfo { market_pubkey } => {
161 | sdk.add_market(&market_pubkey).await?;
162 | process_get_seat_manager_info(&sdk.client, &market_pubkey).await?;
163 | }
164 | PhoenixCLICommand::ClaimSeat { market_pubkey } => {
165 | sdk.add_market(&market_pubkey).await?;
166 | process_claim_seat(&sdk.client, &market_pubkey).await?
167 | }
168 | PhoenixCLICommand::EvictSeat {
169 | market_pubkey,
170 | trader_to_evict,
171 | } => {
172 | sdk.add_market(&market_pubkey).await?;
173 | process_evict_seat(&sdk.client, &market_pubkey, &trader_to_evict).await?
174 | }
175 | }
176 |
177 | Ok(())
178 | }
179 |
--------------------------------------------------------------------------------