├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── jest.config.js ├── package.json ├── shell ├── src ├── fees.ts ├── index.js ├── instructions.js ├── instructions.test.js ├── layout.js ├── market.test.js ├── market.ts ├── markets.json ├── queue.ts ├── slab.test.js ├── slab.ts ├── token-instructions.js ├── token-mints.json └── tokens_and_markets.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | "prettier/@typescript-eslint" 9 | ], 10 | "env": { 11 | "node": true, 12 | "jest": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "no-constant-condition": ["error", { "checkLoops": false }], 20 | "@typescript-eslint/explicit-module-boundary-types": "off", 21 | "@typescript-eslint/ban-ts-comment": "off", 22 | "@typescript-eslint/no-explicit-any": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # builds 5 | build 6 | dist 7 | lib 8 | .rpt2_cache 9 | 10 | # misc 11 | .DS_Store 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | *~ 18 | .idea 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | dist: bionic 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm (scoped)](https://img.shields.io/npm/v/@project-serum/serum)](https://www.npmjs.com/package/@project-serum/serum) 2 | [![Build Status](https://travis-ci.com/project-serum/serum-js.svg?branch=master)](https://travis-ci.com/project-serum/serum-js) 3 | 4 | # Moved 5 | 6 | This repository has been moved to the serum-ts [monorepo](https://github.com/project-serum/serum-ts/tree/master/packages/serum). 7 | 8 | # Serum JS Client Library 9 | 10 | JavaScript client library for interacting with the Project Serum DEX. 11 | 12 | ## Installation 13 | 14 | Using npm: 15 | 16 | ``` 17 | npm install @solana/web3.js @project-serum/serum 18 | ``` 19 | 20 | Using yarn: 21 | 22 | ``` 23 | yarn add @solana/web3.js @project-serum/serum 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```js 29 | import { Account, Connection, PublicKey } from '@solana/web3.js'; 30 | import { Market } from '@project-serum/serum'; 31 | 32 | let connection = new Connection('https://testnet.solana.com'); 33 | let marketAddress = new PublicKey('...'); 34 | let market = await Market.load(connection, marketAddress); 35 | 36 | // Fetching orderbooks 37 | let bids = await market.loadBids(connection); 38 | let asks = await market.loadAsks(connection); 39 | // L2 orderbook data 40 | for (let [price, size] of bids.getL2(20)) { 41 | console.log(price, size); 42 | } 43 | // Full orderbook data 44 | for (let order of asks) { 45 | console.log( 46 | order.orderId, 47 | order.price, 48 | order.size, 49 | order.side, // 'buy' or 'sell' 50 | ); 51 | } 52 | 53 | // Placing orders 54 | let owner = new Account('...'); 55 | let payer = new PublicKey('...'); // spl-token account 56 | await market.placeOrder(connection, { 57 | owner, 58 | payer, 59 | side: 'buy', // 'buy' or 'sell' 60 | price: 123.45, 61 | size: 17.0, 62 | orderType: 'limit', // 'limit', 'ioc', 'postOnly' 63 | }); 64 | 65 | // Retrieving open orders by owner 66 | let myOrders = await market.loadOrdersForOwner(connection, owner.publicKey); 67 | 68 | // Cancelling orders 69 | for (let order of myOrders) { 70 | await market.cancelOrder(connection, owner, order); 71 | } 72 | 73 | // Retrieving fills 74 | for (let fill of await market.loadFills(connection)) { 75 | console.log( 76 | fill.orderId, 77 | fill.price, 78 | fill.size, 79 | fill.side, 80 | ); 81 | } 82 | 83 | // Settle funds 84 | for (let openOrders of await market.findOpenOrdersAccountsForOwner( 85 | connection, 86 | owner.publicKey, 87 | )) { 88 | if (openOrders.baseTokenFree > 0 || openOrders.quoteTokenFree > 0) { 89 | // spl-token accounts to which to send the proceeds from trades 90 | let baseTokenAccount = new PublicKey('...'); 91 | let quoteTokenAccount = new PublicKey('...'); 92 | 93 | await market.settleFunds( 94 | connection, 95 | owner, 96 | openOrders, 97 | baseTokenAccount, 98 | quoteTokenAccount, 99 | ); 100 | } 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-serum/serum", 3 | "version": "0.13.11", 4 | "description": "Library for interacting with the serum dex", 5 | "license": "MIT", 6 | "repository": "project-serum/serum-js", 7 | "main": "lib/index.js", 8 | "source": "src/index.js", 9 | "types": "lib/index.d.ts", 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "scripts": { 14 | "build": "tsc", 15 | "start": "tsc --watch", 16 | "clean": "rm -rf lib", 17 | "prepare": "run-s clean build", 18 | "shell": "node -e \"$(< shell)\" -i --experimental-repl-await", 19 | "test": "run-s test:unit test:lint test:build", 20 | "test:build": "run-s build", 21 | "test:lint": "eslint src", 22 | "test:unit": "jest", 23 | "test:watch": "jest --watch" 24 | }, 25 | "devDependencies": { 26 | "@tsconfig/node12": "^1.0.7", 27 | "@types/bn.js": "^4.11.6", 28 | "@types/jest": "^26.0.9", 29 | "@typescript-eslint/eslint-plugin": "^4.6.0", 30 | "@typescript-eslint/parser": "^4.6.0", 31 | "babel-eslint": "^10.0.3", 32 | "cross-env": "^7.0.2", 33 | "eslint": "^7.6.0", 34 | "eslint-config-prettier": "^6.11.0", 35 | "jest": "^26.4.0", 36 | "npm-run-all": "^4.1.5", 37 | "prettier": "^2.0.5", 38 | "ts-jest": "^26.2.0", 39 | "typescript": "^4.0.5" 40 | }, 41 | "files": [ 42 | "lib" 43 | ], 44 | "prettier": { 45 | "singleQuote": true, 46 | "trailingComma": "all" 47 | }, 48 | "dependencies": { 49 | "@solana/web3.js": "0.86.1", 50 | "bn.js": "^5.1.2", 51 | "buffer-layout": "^1.2.0" 52 | }, 53 | "browserslist": [ 54 | ">0.2%", 55 | "not dead", 56 | "not op_mini all", 57 | "maintained node versions" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /shell: -------------------------------------------------------------------------------- 1 | const lib = require('./lib/index'); 2 | const solana = require('@solana/web3.js'); 3 | const Market = lib.Market; 4 | const Orderbook = lib.Orderbook; 5 | const OpenOrders = lib.OpenOrders; 6 | const DexInstructions = lib.DexInstructions; 7 | const DEX_PROGRAM_ID = lib.DEX_PROGRAM_ID; 8 | const decodeEventQueue = lib.decodeEventQueue; 9 | const decodeRequestQueue = lib.decodeRequestQueue; 10 | const TokenInstructions = lib.TokenInstructions; 11 | const getLayoutVersion = lib.getLayoutVersion; 12 | -------------------------------------------------------------------------------- /src/fees.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { getLayoutVersion } from './tokens_and_markets'; 3 | 4 | export function supportsSrmFeeDiscounts(programId: PublicKey) { 5 | return getLayoutVersion(programId) > 1; 6 | } 7 | 8 | export function getFeeRates(feeTier: number): { taker: number; maker: number } { 9 | if (feeTier === 1) { 10 | // SRM2 11 | return { taker: 0.002, maker: -0.0003 }; 12 | } else if (feeTier === 2) { 13 | // SRM3 14 | return { taker: 0.0018, maker: -0.0003 }; 15 | } else if (feeTier === 3) { 16 | // SRM4 17 | return { taker: 0.0016, maker: -0.0003 }; 18 | } else if (feeTier === 4) { 19 | // SRM5 20 | return { taker: 0.0014, maker: -0.0003 }; 21 | } else if (feeTier === 5) { 22 | // SRM6 23 | return { taker: 0.0012, maker: -0.0003 }; 24 | } else if (feeTier === 6) { 25 | // MSRM 26 | return { taker: 0.001, maker: -0.0005 }; 27 | } 28 | // Base 29 | return { taker: 0.0022, maker: -0.0003 }; 30 | } 31 | 32 | export function getFeeTier(msrmBalance: number, srmBalance: number): number { 33 | if (msrmBalance >= 1) { 34 | return 6; 35 | } else if (srmBalance >= 1_000_000) { 36 | return 5; 37 | } else if (srmBalance >= 100_000) { 38 | return 4; 39 | } else if (srmBalance >= 10_000) { 40 | return 3; 41 | } else if (srmBalance >= 1_000) { 42 | return 2; 43 | } else if (srmBalance >= 100) { 44 | return 1; 45 | } else { 46 | return 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { Market, Orderbook, OpenOrders } from './market'; 2 | export { 3 | DexInstructions, 4 | decodeInstruction, 5 | SETTLE_FUNDS_BASE_WALLET_INDEX, 6 | SETTLE_FUNDS_QUOTE_WALLET_INDEX, 7 | NEW_ORDER_OPEN_ORDERS_INDEX, 8 | NEW_ORDER_OWNER_INDEX, 9 | } from './instructions'; 10 | export { getFeeTier, getFeeRates, supportsSrmFeeDiscounts } from './fees'; 11 | export { TOKEN_MINTS, MARKETS, getLayoutVersion } from './tokens_and_markets'; 12 | export { 13 | decodeEventQueue, 14 | decodeRequestQueue, 15 | REQUEST_QUEUE_LAYOUT, 16 | EVENT_QUEUE_LAYOUT, 17 | } from './queue'; 18 | export * as TokenInstructions from './token-instructions'; 19 | -------------------------------------------------------------------------------- /src/instructions.js: -------------------------------------------------------------------------------- 1 | import { struct, u16, u32, u8, union } from 'buffer-layout'; 2 | import { 3 | orderTypeLayout, 4 | publicKeyLayout, 5 | sideLayout, 6 | u128, 7 | u64, 8 | VersionedLayout, 9 | } from './layout'; 10 | import { SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js'; 11 | import { TOKEN_PROGRAM_ID } from './token-instructions'; 12 | 13 | // NOTE: Update these if the position of arguments for the settleFunds instruction changes 14 | export const SETTLE_FUNDS_BASE_WALLET_INDEX = 5; 15 | export const SETTLE_FUNDS_QUOTE_WALLET_INDEX = 6; 16 | 17 | // NOTE: Update these if the position of arguments for the newOrder instruction changes 18 | export const NEW_ORDER_OPEN_ORDERS_INDEX = 1; 19 | export const NEW_ORDER_OWNER_INDEX = 4; 20 | 21 | export const INSTRUCTION_LAYOUT = new VersionedLayout( 22 | 0, 23 | union(u32('instruction')), 24 | ); 25 | INSTRUCTION_LAYOUT.inner.addVariant( 26 | 0, 27 | struct([ 28 | u64('baseLotSize'), 29 | u64('quoteLotSize'), 30 | u16('feeRateBps'), 31 | u64('vaultSignerNonce'), 32 | u64('quoteDustThreshold'), 33 | ]), 34 | 'initializeMarket', 35 | ); 36 | INSTRUCTION_LAYOUT.inner.addVariant( 37 | 1, 38 | struct([ 39 | sideLayout('side'), 40 | u64('limitPrice'), 41 | u64('maxQuantity'), 42 | orderTypeLayout('orderType'), 43 | u64('clientId'), 44 | ]), 45 | 'newOrder', 46 | ); 47 | INSTRUCTION_LAYOUT.inner.addVariant(2, struct([u16('limit')]), 'matchOrders'); 48 | INSTRUCTION_LAYOUT.inner.addVariant(3, struct([u16('limit')]), 'consumeEvents'); 49 | INSTRUCTION_LAYOUT.inner.addVariant( 50 | 4, 51 | struct([ 52 | sideLayout('side'), 53 | u128('orderId'), 54 | publicKeyLayout('openOrders'), 55 | u8('openOrdersSlot'), 56 | ]), 57 | 'cancelOrder', 58 | ); 59 | INSTRUCTION_LAYOUT.inner.addVariant(5, struct([]), 'settleFunds'); 60 | INSTRUCTION_LAYOUT.inner.addVariant( 61 | 6, 62 | struct([u64('clientId')]), 63 | 'cancelOrderByClientId', 64 | ); 65 | 66 | export function encodeInstruction(instruction) { 67 | const b = Buffer.alloc(100); 68 | return b.slice(0, INSTRUCTION_LAYOUT.encode(instruction, b)); 69 | } 70 | 71 | export function decodeInstruction(message) { 72 | return INSTRUCTION_LAYOUT.decode(message); 73 | } 74 | 75 | export class DexInstructions { 76 | static initializeMarket({ 77 | market, 78 | requestQueue, 79 | eventQueue, 80 | bids, 81 | asks, 82 | baseVault, 83 | quoteVault, 84 | baseMint, 85 | quoteMint, 86 | baseLotSize, 87 | quoteLotSize, 88 | feeRateBps, 89 | vaultSignerNonce, 90 | quoteDustThreshold, 91 | programId, 92 | }) { 93 | return new TransactionInstruction({ 94 | keys: [ 95 | { pubkey: market, isSigner: false, isWritable: true }, 96 | { pubkey: requestQueue, isSigner: false, isWritable: true }, 97 | { pubkey: eventQueue, isSigner: false, isWritable: true }, 98 | { pubkey: bids, isSigner: false, isWritable: true }, 99 | { pubkey: asks, isSigner: false, isWritable: true }, 100 | { pubkey: baseVault, isSigner: false, isWritable: true }, 101 | { pubkey: quoteVault, isSigner: false, isWritable: true }, 102 | { pubkey: baseMint, isSigner: false, isWritable: false }, 103 | { pubkey: quoteMint, isSigner: false, isWritable: false }, 104 | ], 105 | programId, 106 | data: encodeInstruction({ 107 | initializeMarket: { 108 | baseLotSize, 109 | quoteLotSize, 110 | feeRateBps, 111 | vaultSignerNonce, 112 | quoteDustThreshold, 113 | }, 114 | }), 115 | }); 116 | } 117 | 118 | static newOrder({ 119 | market, 120 | openOrders, 121 | payer, 122 | owner, 123 | requestQueue, 124 | baseVault, 125 | quoteVault, 126 | side, 127 | limitPrice, 128 | maxQuantity, 129 | orderType, 130 | clientId, 131 | programId, 132 | feeDiscountPubkey = null, 133 | }) { 134 | const keys = [ 135 | { pubkey: market, isSigner: false, isWritable: true }, 136 | { pubkey: openOrders, isSigner: false, isWritable: true }, 137 | { pubkey: requestQueue, isSigner: false, isWritable: true }, 138 | { pubkey: payer, isSigner: false, isWritable: true }, 139 | { pubkey: owner, isSigner: true, isWritable: false }, 140 | { pubkey: baseVault, isSigner: false, isWritable: true }, 141 | { pubkey: quoteVault, isSigner: false, isWritable: true }, 142 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 143 | { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, 144 | ]; 145 | if (feeDiscountPubkey) { 146 | keys.push({ 147 | pubkey: feeDiscountPubkey, 148 | isSigner: false, 149 | isWritable: false, 150 | }); 151 | } 152 | return new TransactionInstruction({ 153 | keys, 154 | programId, 155 | data: encodeInstruction({ 156 | newOrder: clientId 157 | ? { side, limitPrice, maxQuantity, orderType, clientId } 158 | : { side, limitPrice, maxQuantity, orderType }, 159 | }), 160 | }); 161 | } 162 | 163 | static matchOrders({ 164 | market, 165 | requestQueue, 166 | eventQueue, 167 | bids, 168 | asks, 169 | baseVault, 170 | quoteVault, 171 | limit, 172 | programId, 173 | }) { 174 | return new TransactionInstruction({ 175 | keys: [ 176 | { pubkey: market, isSigner: false, isWritable: true }, 177 | { pubkey: requestQueue, isSigner: false, isWritable: true }, 178 | { pubkey: eventQueue, isSigner: false, isWritable: true }, 179 | { pubkey: bids, isSigner: false, isWritable: true }, 180 | { pubkey: asks, isSigner: false, isWritable: true }, 181 | { pubkey: baseVault, isSigner: false, isWritable: true }, 182 | { pubkey: quoteVault, isSigner: false, isWritable: true }, 183 | ], 184 | programId, 185 | data: encodeInstruction({ matchOrders: { limit } }), 186 | }); 187 | } 188 | 189 | static consumeEvents({ 190 | market, 191 | eventQueue, 192 | openOrdersAccounts, 193 | limit, 194 | programId, 195 | }) { 196 | return new TransactionInstruction({ 197 | keys: [ 198 | ...openOrdersAccounts.map((account) => ({ 199 | pubkey: account, 200 | isSigner: false, 201 | isWritable: true, 202 | })), 203 | { pubkey: market, isSigner: false, isWritable: true }, 204 | { pubkey: eventQueue, isSigner: false, isWritable: true }, 205 | ], 206 | programId, 207 | data: encodeInstruction({ consumeEvents: { limit } }), 208 | }); 209 | } 210 | 211 | static cancelOrder({ 212 | market, 213 | openOrders, 214 | owner, 215 | requestQueue, 216 | side, 217 | orderId, 218 | openOrdersSlot, 219 | programId, 220 | }) { 221 | return new TransactionInstruction({ 222 | keys: [ 223 | { pubkey: market, isSigner: false, isWritable: false }, 224 | { pubkey: openOrders, isSigner: false, isWritable: true }, 225 | { pubkey: requestQueue, isSigner: false, isWritable: true }, 226 | { pubkey: owner, isSigner: true, isWritable: false }, 227 | ], 228 | programId, 229 | data: encodeInstruction({ 230 | cancelOrder: { side, orderId, openOrders, openOrdersSlot }, 231 | }), 232 | }); 233 | } 234 | 235 | static cancelOrderByClientId({ 236 | market, 237 | openOrders, 238 | owner, 239 | requestQueue, 240 | clientId, 241 | programId, 242 | }) { 243 | return new TransactionInstruction({ 244 | keys: [ 245 | { pubkey: market, isSigner: false, isWritable: false }, 246 | { pubkey: openOrders, isSigner: false, isWritable: true }, 247 | { pubkey: requestQueue, isSigner: false, isWritable: true }, 248 | { pubkey: owner, isSigner: true, isWritable: false }, 249 | ], 250 | programId, 251 | data: encodeInstruction({ 252 | cancelOrderByClientId: { clientId }, 253 | }), 254 | }); 255 | } 256 | 257 | static settleFunds({ 258 | market, 259 | openOrders, 260 | owner, 261 | baseVault, 262 | quoteVault, 263 | baseWallet, 264 | quoteWallet, 265 | vaultSigner, 266 | programId, 267 | referrerQuoteWallet = null, 268 | }) { 269 | const keys = [ 270 | { pubkey: market, isSigner: false, isWritable: true }, 271 | { pubkey: openOrders, isSigner: false, isWritable: true }, 272 | { pubkey: owner, isSigner: true, isWritable: false }, 273 | { pubkey: baseVault, isSigner: false, isWritable: true }, 274 | { pubkey: quoteVault, isSigner: false, isWritable: true }, 275 | { pubkey: baseWallet, isSigner: false, isWritable: true }, 276 | { pubkey: quoteWallet, isSigner: false, isWritable: true }, 277 | { pubkey: vaultSigner, isSigner: false, isWritable: false }, 278 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 279 | ]; 280 | if (referrerQuoteWallet) { 281 | keys.push({ 282 | pubkey: referrerQuoteWallet, 283 | isSigner: false, 284 | isWritable: true, 285 | }); 286 | } 287 | return new TransactionInstruction({ 288 | keys, 289 | programId, 290 | data: encodeInstruction({ 291 | settleFunds: {}, 292 | }), 293 | }); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/instructions.test.js: -------------------------------------------------------------------------------- 1 | import { encodeInstruction } from './instructions'; 2 | import BN from 'bn.js'; 3 | 4 | describe('instruction', () => { 5 | it('encodes initialize market', () => { 6 | const b = encodeInstruction({ 7 | initializeMarket: { 8 | baseLotSize: new BN(10), 9 | quoteLotSize: new BN(100000), 10 | feeRateBps: 5, 11 | vaultSignerNonce: new BN(1), 12 | quoteDustThreshold: new BN(10), 13 | }, 14 | }); 15 | expect(b.toString('hex')).toEqual( 16 | '00000000000a00000000000000a086010000000000050001000000000000000a00000000000000', 17 | ); 18 | }); 19 | 20 | it('encodes new order', () => { 21 | const b = encodeInstruction({ 22 | newOrder: { 23 | side: 'sell', 24 | limitPrice: new BN(10), 25 | maxQuantity: new BN(5), 26 | orderType: 'postOnly', 27 | }, 28 | }); 29 | expect(b.toString('hex')).toEqual( 30 | '0001000000010000000a000000000000000500000000000000020000000000000000000000', 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/layout.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { bits, Blob, Layout, u32, UInt } from 'buffer-layout'; 4 | import { PublicKey } from '@solana/web3.js'; 5 | import BN from 'bn.js'; 6 | 7 | class Zeros extends Blob { 8 | decode(b, offset) { 9 | const slice = super.decode(b, offset); 10 | if (!slice.every((v) => v === 0)) { 11 | throw new Error('nonzero padding bytes'); 12 | } 13 | return slice; 14 | } 15 | } 16 | 17 | export function zeros(length) { 18 | return new Zeros(length); 19 | } 20 | 21 | class PublicKeyLayout extends Blob { 22 | constructor(property) { 23 | super(32, property); 24 | } 25 | 26 | decode(b, offset) { 27 | return new PublicKey(super.decode(b, offset)); 28 | } 29 | 30 | encode(src, b, offset) { 31 | return super.encode(src.toBuffer(), b, offset); 32 | } 33 | } 34 | 35 | export function publicKeyLayout(property) { 36 | return new PublicKeyLayout(property); 37 | } 38 | 39 | class BNLayout extends Blob { 40 | decode(b, offset) { 41 | return new BN(super.decode(b, offset), 10, 'le'); 42 | } 43 | 44 | encode(src, b, offset) { 45 | return super.encode(src.toArrayLike(Buffer, 'le', this.span), b, offset); 46 | } 47 | } 48 | 49 | export function u64(property) { 50 | return new BNLayout(8, property); 51 | } 52 | 53 | export function u128(property) { 54 | return new BNLayout(16, property); 55 | } 56 | 57 | export class WideBits extends Layout { 58 | constructor(property) { 59 | super(8, property); 60 | this._lower = bits(u32(), false); 61 | this._upper = bits(u32(), false); 62 | } 63 | 64 | addBoolean(property) { 65 | if (this._lower.fields.length < 32) { 66 | this._lower.addBoolean(property); 67 | } else { 68 | this._upper.addBoolean(property); 69 | } 70 | } 71 | 72 | decode(b, offset = 0) { 73 | const lowerDecoded = this._lower.decode(b, offset); 74 | const upperDecoded = this._upper.decode(b, offset + this._lower.span); 75 | return { ...lowerDecoded, ...upperDecoded }; 76 | } 77 | 78 | encode(src, b, offset = 0) { 79 | return ( 80 | this._lower.encode(src, b, offset) + 81 | this._upper.encode(src, b, offset + this._lower.span) 82 | ); 83 | } 84 | } 85 | 86 | export class VersionedLayout extends Layout { 87 | constructor(version, inner, property) { 88 | super(inner.span > 0 ? inner.span + 1 : inner.span, property); 89 | this.version = version; 90 | this.inner = inner; 91 | } 92 | 93 | decode(b, offset = 0) { 94 | // if (b.readUInt8(offset) !== this._version) { 95 | // throw new Error('invalid version'); 96 | // } 97 | return this.inner.decode(b, offset + 1); 98 | } 99 | 100 | encode(src, b, offset = 0) { 101 | b.writeUInt8(this.version, offset); 102 | return 1 + this.inner.encode(src, b, offset + 1); 103 | } 104 | 105 | getSpan(b, offset = 0) { 106 | return 1 + this.inner.getSpan(b, offset + 1); 107 | } 108 | } 109 | 110 | class EnumLayout extends UInt { 111 | constructor(values, span, property) { 112 | super(span, property); 113 | this.values = values; 114 | } 115 | 116 | encode(src, b, offset) { 117 | if (this.values[src] !== undefined) { 118 | return super.encode(this.values[src], b, offset); 119 | } 120 | throw new Error('Invalid ' + this.property); 121 | } 122 | 123 | decode(b, offset) { 124 | const decodedValue = super.decode(b, offset); 125 | const entry = Object.entries(this.values).find( 126 | ([, value]) => value === decodedValue, 127 | ); 128 | if (entry) { 129 | return entry[0]; 130 | } 131 | throw new Error('Invalid ' + this.property); 132 | } 133 | } 134 | 135 | export function sideLayout(property) { 136 | return new EnumLayout({ buy: 0, sell: 1 }, 4, property); 137 | } 138 | 139 | export function orderTypeLayout(property) { 140 | return new EnumLayout({ limit: 0, ioc: 1, postOnly: 2 }, 4, property); 141 | } 142 | 143 | const ACCOUNT_FLAGS_LAYOUT = new WideBits(); 144 | ACCOUNT_FLAGS_LAYOUT.addBoolean('initialized'); 145 | ACCOUNT_FLAGS_LAYOUT.addBoolean('market'); 146 | ACCOUNT_FLAGS_LAYOUT.addBoolean('openOrders'); 147 | ACCOUNT_FLAGS_LAYOUT.addBoolean('requestQueue'); 148 | ACCOUNT_FLAGS_LAYOUT.addBoolean('eventQueue'); 149 | ACCOUNT_FLAGS_LAYOUT.addBoolean('bids'); 150 | ACCOUNT_FLAGS_LAYOUT.addBoolean('asks'); 151 | 152 | export function accountFlagsLayout(property = 'accountFlags') { 153 | return ACCOUNT_FLAGS_LAYOUT.replicate(property); 154 | } 155 | 156 | export function setLayoutDecoder(layout, decoder) { 157 | const originalDecode = layout.decode; 158 | layout.decode = function decode(b, offset = 0) { 159 | return decoder(originalDecode.call(this, b, offset)); 160 | }; 161 | } 162 | 163 | export function setLayoutEncoder(layout, encoder) { 164 | const originalEncode = layout.encode; 165 | layout.encode = function encode(src, b, offset) { 166 | return originalEncode.call(this, encoder(src), b, offset); 167 | }; 168 | return layout; 169 | } 170 | -------------------------------------------------------------------------------- /src/market.test.js: -------------------------------------------------------------------------------- 1 | import { accountFlagsLayout } from './layout'; 2 | 3 | describe('accountFlags', () => { 4 | const layout = accountFlagsLayout(); 5 | it('parses', () => { 6 | const b = Buffer.from('0000000000000000', 'hex'); 7 | expect(layout.getSpan(b)).toBe(8); 8 | expect(layout.decode(b).initialized).toBe(false); 9 | expect(layout.decode(Buffer.from('0000000000000000', 'hex'))).toMatchObject( 10 | { 11 | initialized: false, 12 | market: false, 13 | openOrders: false, 14 | requestQueue: false, 15 | eventQueue: false, 16 | bids: false, 17 | asks: false, 18 | }, 19 | ); 20 | expect(layout.decode(Buffer.from('0300000000000000', 'hex'))).toMatchObject( 21 | { 22 | initialized: true, 23 | market: true, 24 | openOrders: false, 25 | requestQueue: false, 26 | eventQueue: false, 27 | bids: false, 28 | asks: false, 29 | }, 30 | ); 31 | expect(layout.decode(Buffer.from('0500000000000000', 'hex'))).toMatchObject( 32 | { 33 | initialized: true, 34 | market: false, 35 | openOrders: true, 36 | requestQueue: false, 37 | eventQueue: false, 38 | bids: false, 39 | asks: false, 40 | }, 41 | ); 42 | }); 43 | 44 | it('serializes', () => { 45 | const b = Buffer.alloc(8); 46 | expect( 47 | layout.encode( 48 | { 49 | initialized: true, 50 | market: false, 51 | openOrders: false, 52 | requestQueue: false, 53 | eventQueue: false, 54 | bids: false, 55 | asks: true, 56 | }, 57 | b, 58 | ), 59 | ).toBe(8); 60 | expect(b.toString('hex')).toEqual('4100000000000000'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/market.ts: -------------------------------------------------------------------------------- 1 | import { blob, seq, struct, u8 } from 'buffer-layout'; 2 | import { accountFlagsLayout, publicKeyLayout, u128, u64 } from './layout'; 3 | import { Slab, SLAB_LAYOUT } from './slab'; 4 | import { DexInstructions } from './instructions'; 5 | import BN from 'bn.js'; 6 | import { 7 | Account, 8 | AccountInfo, 9 | Commitment, 10 | Connection, 11 | LAMPORTS_PER_SOL, 12 | PublicKey, 13 | SystemProgram, 14 | Transaction, 15 | TransactionInstruction, 16 | TransactionSignature, 17 | } from '@solana/web3.js'; 18 | import { decodeEventQueue, decodeRequestQueue } from './queue'; 19 | import { Buffer } from 'buffer'; 20 | import { getFeeTier, supportsSrmFeeDiscounts } from './fees'; 21 | import { 22 | closeAccount, 23 | initializeAccount, 24 | MSRM_DECIMALS, 25 | MSRM_MINT, 26 | SRM_DECIMALS, 27 | SRM_MINT, 28 | TOKEN_PROGRAM_ID, 29 | WRAPPED_SOL_MINT, 30 | } from './token-instructions'; 31 | import { getLayoutVersion } from './tokens_and_markets'; 32 | 33 | export const _MARKET_STAT_LAYOUT_V1 = struct([ 34 | blob(5), 35 | 36 | accountFlagsLayout('accountFlags'), 37 | 38 | publicKeyLayout('ownAddress'), 39 | 40 | u64('vaultSignerNonce'), 41 | 42 | publicKeyLayout('baseMint'), 43 | publicKeyLayout('quoteMint'), 44 | 45 | publicKeyLayout('baseVault'), 46 | u64('baseDepositsTotal'), 47 | u64('baseFeesAccrued'), 48 | 49 | publicKeyLayout('quoteVault'), 50 | u64('quoteDepositsTotal'), 51 | u64('quoteFeesAccrued'), 52 | 53 | u64('quoteDustThreshold'), 54 | 55 | publicKeyLayout('requestQueue'), 56 | publicKeyLayout('eventQueue'), 57 | 58 | publicKeyLayout('bids'), 59 | publicKeyLayout('asks'), 60 | 61 | u64('baseLotSize'), 62 | u64('quoteLotSize'), 63 | 64 | u64('feeRateBps'), 65 | 66 | blob(7), 67 | ]); 68 | 69 | export const _MARKET_STATE_LAYOUT_V2 = struct([ 70 | blob(5), 71 | 72 | accountFlagsLayout('accountFlags'), 73 | 74 | publicKeyLayout('ownAddress'), 75 | 76 | u64('vaultSignerNonce'), 77 | 78 | publicKeyLayout('baseMint'), 79 | publicKeyLayout('quoteMint'), 80 | 81 | publicKeyLayout('baseVault'), 82 | u64('baseDepositsTotal'), 83 | u64('baseFeesAccrued'), 84 | 85 | publicKeyLayout('quoteVault'), 86 | u64('quoteDepositsTotal'), 87 | u64('quoteFeesAccrued'), 88 | 89 | u64('quoteDustThreshold'), 90 | 91 | publicKeyLayout('requestQueue'), 92 | publicKeyLayout('eventQueue'), 93 | 94 | publicKeyLayout('bids'), 95 | publicKeyLayout('asks'), 96 | 97 | u64('baseLotSize'), 98 | u64('quoteLotSize'), 99 | 100 | u64('feeRateBps'), 101 | 102 | u64('referrerRebatesAccrued'), 103 | 104 | blob(7), 105 | ]); 106 | 107 | export class Market { 108 | private _decoded: any; 109 | private _baseSplTokenDecimals: number; 110 | private _quoteSplTokenDecimals: number; 111 | private _skipPreflight: boolean; 112 | private _commitment: Commitment; 113 | private _programId: PublicKey; 114 | private _openOrdersAccountsCache: { 115 | [publickKey: string]: { accounts: OpenOrders[]; ts: number }; 116 | }; 117 | 118 | private _feeDiscountKeysCache: { 119 | [publicKey: string]: { 120 | accounts: Array<{ 121 | balance: number; 122 | mint: PublicKey; 123 | pubkey: PublicKey; 124 | feeTier: number; 125 | }>; 126 | ts: number; 127 | }; 128 | }; 129 | 130 | constructor( 131 | decoded, 132 | baseMintDecimals: number, 133 | quoteMintDecimals: number, 134 | options: MarketOptions = {}, 135 | programId: PublicKey, 136 | ) { 137 | const { skipPreflight = false, commitment = 'recent' } = options; 138 | if (!decoded.accountFlags.initialized || !decoded.accountFlags.market) { 139 | throw new Error('Invalid market state'); 140 | } 141 | this._decoded = decoded; 142 | this._baseSplTokenDecimals = baseMintDecimals; 143 | this._quoteSplTokenDecimals = quoteMintDecimals; 144 | this._skipPreflight = skipPreflight; 145 | this._commitment = commitment; 146 | this._programId = programId; 147 | this._openOrdersAccountsCache = {}; 148 | this._feeDiscountKeysCache = {}; 149 | } 150 | 151 | static getLayout(programId: PublicKey) { 152 | if (getLayoutVersion(programId) === 1) { 153 | return _MARKET_STAT_LAYOUT_V1; 154 | } 155 | return _MARKET_STATE_LAYOUT_V2; 156 | } 157 | 158 | static async load( 159 | connection: Connection, 160 | address: PublicKey, 161 | options: MarketOptions = {}, 162 | programId: PublicKey, 163 | ) { 164 | const { owner, data } = throwIfNull( 165 | await connection.getAccountInfo(address), 166 | 'Market not found', 167 | ); 168 | if (!owner.equals(programId)) { 169 | throw new Error('Address not owned by program: ' + owner.toBase58()); 170 | } 171 | const decoded = this.getLayout(programId).decode(data); 172 | if ( 173 | !decoded.accountFlags.initialized || 174 | !decoded.accountFlags.market || 175 | !decoded.ownAddress.equals(address) 176 | ) { 177 | throw new Error('Invalid market'); 178 | } 179 | const [baseMintDecimals, quoteMintDecimals] = await Promise.all([ 180 | getMintDecimals(connection, decoded.baseMint), 181 | getMintDecimals(connection, decoded.quoteMint), 182 | ]); 183 | return new Market( 184 | decoded, 185 | baseMintDecimals, 186 | quoteMintDecimals, 187 | options, 188 | programId, 189 | ); 190 | } 191 | 192 | get programId(): PublicKey { 193 | return this._programId; 194 | } 195 | 196 | get address(): PublicKey { 197 | return this._decoded.ownAddress; 198 | } 199 | 200 | get publicKey(): PublicKey { 201 | return this.address; 202 | } 203 | 204 | get baseMintAddress(): PublicKey { 205 | return this._decoded.baseMint; 206 | } 207 | 208 | get quoteMintAddress(): PublicKey { 209 | return this._decoded.quoteMint; 210 | } 211 | 212 | get bidsAddress(): PublicKey { 213 | return this._decoded.bids; 214 | } 215 | 216 | get asksAddress(): PublicKey { 217 | return this._decoded.asks; 218 | } 219 | 220 | async loadBids(connection: Connection): Promise { 221 | const { data } = throwIfNull( 222 | await connection.getAccountInfo(this._decoded.bids), 223 | ); 224 | return Orderbook.decode(this, data); 225 | } 226 | 227 | async loadAsks(connection: Connection): Promise { 228 | const { data } = throwIfNull( 229 | await connection.getAccountInfo(this._decoded.asks), 230 | ); 231 | return Orderbook.decode(this, data); 232 | } 233 | 234 | async loadOrdersForOwner( 235 | connection: Connection, 236 | ownerAddress: PublicKey, 237 | cacheDurationMs = 0, 238 | ): Promise { 239 | const [bids, asks, openOrdersAccounts] = await Promise.all([ 240 | this.loadBids(connection), 241 | this.loadAsks(connection), 242 | this.findOpenOrdersAccountsForOwner( 243 | connection, 244 | ownerAddress, 245 | cacheDurationMs, 246 | ), 247 | ]); 248 | return this.filterForOpenOrders(bids, asks, openOrdersAccounts); 249 | } 250 | 251 | filterForOpenOrders( 252 | bids: Orderbook, 253 | asks: Orderbook, 254 | openOrdersAccounts: OpenOrders[], 255 | ): Order[] { 256 | return [...bids, ...asks].filter((order) => 257 | openOrdersAccounts.some((openOrders) => 258 | order.openOrdersAddress.equals(openOrders.address), 259 | ), 260 | ); 261 | } 262 | 263 | async findBaseTokenAccountsForOwner( 264 | connection: Connection, 265 | ownerAddress: PublicKey, 266 | includeUnwrappedSol = false, 267 | ): Promise }>> { 268 | if (this.baseMintAddress.equals(WRAPPED_SOL_MINT) && includeUnwrappedSol) { 269 | const [wrapped, unwrapped] = await Promise.all([ 270 | this.findBaseTokenAccountsForOwner(connection, ownerAddress, false), 271 | connection.getAccountInfo(ownerAddress), 272 | ]); 273 | if (unwrapped !== null) { 274 | return [{ pubkey: ownerAddress, account: unwrapped }, ...wrapped]; 275 | } 276 | return wrapped; 277 | } 278 | return await this.getTokenAccountsByOwnerForMint( 279 | connection, 280 | ownerAddress, 281 | this.baseMintAddress, 282 | ); 283 | } 284 | 285 | async getTokenAccountsByOwnerForMint( 286 | connection: Connection, 287 | ownerAddress: PublicKey, 288 | mintAddress: PublicKey, 289 | ): Promise }>> { 290 | return ( 291 | await connection.getTokenAccountsByOwner(ownerAddress, { 292 | mint: mintAddress, 293 | }) 294 | ).value; 295 | } 296 | 297 | async findQuoteTokenAccountsForOwner( 298 | connection: Connection, 299 | ownerAddress: PublicKey, 300 | includeUnwrappedSol = false, 301 | ): Promise<{ pubkey: PublicKey; account: AccountInfo }[]> { 302 | if (this.quoteMintAddress.equals(WRAPPED_SOL_MINT) && includeUnwrappedSol) { 303 | const [wrapped, unwrapped] = await Promise.all([ 304 | this.findQuoteTokenAccountsForOwner(connection, ownerAddress, false), 305 | connection.getAccountInfo(ownerAddress), 306 | ]); 307 | if (unwrapped !== null) { 308 | return [{ pubkey: ownerAddress, account: unwrapped }, ...wrapped]; 309 | } 310 | return wrapped; 311 | } 312 | return await this.getTokenAccountsByOwnerForMint( 313 | connection, 314 | ownerAddress, 315 | this.quoteMintAddress, 316 | ); 317 | } 318 | 319 | async findOpenOrdersAccountsForOwner( 320 | connection: Connection, 321 | ownerAddress: PublicKey, 322 | cacheDurationMs = 0, 323 | ): Promise { 324 | const strOwner = ownerAddress.toBase58(); 325 | const now = new Date().getTime(); 326 | if ( 327 | strOwner in this._openOrdersAccountsCache && 328 | now - this._openOrdersAccountsCache[strOwner].ts < cacheDurationMs 329 | ) { 330 | return this._openOrdersAccountsCache[strOwner].accounts; 331 | } 332 | const openOrdersAccountsForOwner = await OpenOrders.findForMarketAndOwner( 333 | connection, 334 | this.address, 335 | ownerAddress, 336 | this._programId, 337 | ); 338 | this._openOrdersAccountsCache[strOwner] = { 339 | accounts: openOrdersAccountsForOwner, 340 | ts: now, 341 | }; 342 | return openOrdersAccountsForOwner; 343 | } 344 | 345 | async placeOrder( 346 | connection: Connection, 347 | { 348 | owner, 349 | payer, 350 | side, 351 | price, 352 | size, 353 | orderType = 'limit', 354 | clientId, 355 | openOrdersAddressKey, 356 | feeDiscountPubkey, 357 | }: OrderParams, 358 | ) { 359 | const { transaction, signers } = await this.makePlaceOrderTransaction< 360 | Account 361 | >(connection, { 362 | owner, 363 | payer, 364 | side, 365 | price, 366 | size, 367 | orderType, 368 | clientId, 369 | openOrdersAddressKey, 370 | feeDiscountPubkey, 371 | }); 372 | return await this._sendTransaction(connection, transaction, [ 373 | owner, 374 | ...signers, 375 | ]); 376 | } 377 | 378 | getSplTokenBalanceFromAccountInfo( 379 | accountInfo: AccountInfo, 380 | decimals: number, 381 | ): number { 382 | return divideBnToNumber( 383 | new BN(accountInfo.data.slice(64, 72), 10, 'le'), 384 | new BN(10).pow(new BN(decimals)), 385 | ); 386 | } 387 | 388 | get supportsSrmFeeDiscounts() { 389 | return supportsSrmFeeDiscounts(this._programId); 390 | } 391 | 392 | get supportsReferralFees() { 393 | return getLayoutVersion(this._programId) > 1; 394 | } 395 | 396 | async findFeeDiscountKeys( 397 | connection: Connection, 398 | ownerAddress: PublicKey, 399 | cacheDurationMs = 0, 400 | ): Promise< 401 | Array<{ 402 | pubkey: PublicKey; 403 | feeTier: number; 404 | balance: number; 405 | mint: PublicKey; 406 | }> 407 | > { 408 | let sortedAccounts: Array<{ 409 | balance: number; 410 | mint: PublicKey; 411 | pubkey: PublicKey; 412 | feeTier: number; 413 | }> = []; 414 | const now = new Date().getTime(); 415 | const strOwner = ownerAddress.toBase58(); 416 | if ( 417 | strOwner in this._feeDiscountKeysCache && 418 | now - this._feeDiscountKeysCache[strOwner].ts < cacheDurationMs 419 | ) { 420 | return this._feeDiscountKeysCache[strOwner].accounts; 421 | } 422 | 423 | if (this.supportsSrmFeeDiscounts) { 424 | // Fee discounts based on (M)SRM holdings supported in newer versions 425 | const msrmAccounts = ( 426 | await this.getTokenAccountsByOwnerForMint( 427 | connection, 428 | ownerAddress, 429 | MSRM_MINT, 430 | ) 431 | ).map(({ pubkey, account }) => { 432 | const balance = this.getSplTokenBalanceFromAccountInfo( 433 | account, 434 | MSRM_DECIMALS, 435 | ); 436 | return { 437 | pubkey, 438 | mint: MSRM_MINT, 439 | balance, 440 | feeTier: getFeeTier(balance, 0), 441 | }; 442 | }); 443 | const srmAccounts = ( 444 | await this.getTokenAccountsByOwnerForMint( 445 | connection, 446 | ownerAddress, 447 | SRM_MINT, 448 | ) 449 | ).map(({ pubkey, account }) => { 450 | const balance = this.getSplTokenBalanceFromAccountInfo( 451 | account, 452 | SRM_DECIMALS, 453 | ); 454 | return { 455 | pubkey, 456 | mint: SRM_MINT, 457 | balance, 458 | feeTier: getFeeTier(0, balance), 459 | }; 460 | }); 461 | sortedAccounts = msrmAccounts.concat(srmAccounts).sort((a, b) => { 462 | if (a.feeTier > b.feeTier) { 463 | return -1; 464 | } else if (a.feeTier < b.feeTier) { 465 | return 1; 466 | } else { 467 | if (a.balance > b.balance) { 468 | return -1; 469 | } else if (a.balance < b.balance) { 470 | return 1; 471 | } else { 472 | return 0; 473 | } 474 | } 475 | }); 476 | } 477 | this._feeDiscountKeysCache[strOwner] = { 478 | accounts: sortedAccounts, 479 | ts: now, 480 | }; 481 | return sortedAccounts; 482 | } 483 | 484 | async findBestFeeDiscountKey( 485 | connection: Connection, 486 | ownerAddress: PublicKey, 487 | cacheDurationMs = 0, 488 | ): Promise<{ pubkey: PublicKey | null; feeTier: number }> { 489 | const accounts = await this.findFeeDiscountKeys( 490 | connection, 491 | ownerAddress, 492 | cacheDurationMs, 493 | ); 494 | if (accounts.length > 0) { 495 | return { 496 | pubkey: accounts[0].pubkey, 497 | feeTier: accounts[0].feeTier, 498 | }; 499 | } 500 | return { 501 | pubkey: null, 502 | feeTier: 0, 503 | }; 504 | } 505 | 506 | async makePlaceOrderTransaction( 507 | connection: Connection, 508 | { 509 | owner, 510 | payer, 511 | side, 512 | price, 513 | size, 514 | orderType = 'limit', 515 | clientId, 516 | openOrdersAddressKey, 517 | feeDiscountPubkey = null, 518 | }: OrderParams, 519 | cacheDurationMs = 0, 520 | feeDiscountPubkeyCacheDurationMs = 0, 521 | ) { 522 | // @ts-ignore 523 | const ownerAddress: PublicKey = owner.publicKey ?? owner; 524 | const openOrdersAccounts = await this.findOpenOrdersAccountsForOwner( 525 | connection, 526 | ownerAddress, 527 | cacheDurationMs, 528 | ); 529 | const transaction = new Transaction(); 530 | const signers: Account[] = []; 531 | 532 | // Fetch an SRM fee discount key if the market supports discounts and it is not supplied 533 | feeDiscountPubkey = 534 | feeDiscountPubkey || 535 | (this.supportsSrmFeeDiscounts 536 | ? ( 537 | await this.findBestFeeDiscountKey( 538 | connection, 539 | ownerAddress, 540 | feeDiscountPubkeyCacheDurationMs, 541 | ) 542 | ).pubkey 543 | : null); 544 | 545 | let openOrdersAddress; 546 | if (openOrdersAccounts.length === 0) { 547 | const newOpenOrdersAccount = new Account(); 548 | transaction.add( 549 | await OpenOrders.makeCreateAccountTransaction( 550 | connection, 551 | this.address, 552 | ownerAddress, 553 | newOpenOrdersAccount.publicKey, 554 | this._programId, 555 | ), 556 | ); 557 | openOrdersAddress = newOpenOrdersAccount.publicKey; 558 | signers.push(newOpenOrdersAccount); 559 | // refresh the cache of open order accounts on next fetch 560 | this._openOrdersAccountsCache[ownerAddress.toBase58()].ts = 0; 561 | } else if (openOrdersAddressKey) { 562 | openOrdersAddress = openOrdersAddressKey; 563 | } else { 564 | openOrdersAddress = openOrdersAccounts[0].address; 565 | } 566 | 567 | let wrappedSolAccount: Account | null = null; 568 | if (payer.equals(ownerAddress)) { 569 | if ( 570 | (side === 'buy' && this.quoteMintAddress.equals(WRAPPED_SOL_MINT)) || 571 | (side === 'sell' && this.baseMintAddress.equals(WRAPPED_SOL_MINT)) 572 | ) { 573 | wrappedSolAccount = new Account(); 574 | let lamports; 575 | if (side === 'buy') { 576 | lamports = Math.round(price * size * 1.01 * LAMPORTS_PER_SOL); 577 | if (openOrdersAccounts.length > 0) { 578 | lamports -= openOrdersAccounts[0].quoteTokenFree.toNumber(); 579 | } 580 | } else { 581 | lamports = Math.round(size * LAMPORTS_PER_SOL); 582 | if (openOrdersAccounts.length > 0) { 583 | lamports -= openOrdersAccounts[0].baseTokenFree.toNumber(); 584 | } 585 | } 586 | lamports = Math.max(lamports, 0) + 1e7; 587 | transaction.add( 588 | SystemProgram.createAccount({ 589 | fromPubkey: ownerAddress, 590 | newAccountPubkey: wrappedSolAccount.publicKey, 591 | lamports, 592 | space: 165, 593 | programId: TOKEN_PROGRAM_ID, 594 | }), 595 | ); 596 | transaction.add( 597 | initializeAccount({ 598 | account: wrappedSolAccount.publicKey, 599 | mint: WRAPPED_SOL_MINT, 600 | owner: ownerAddress, 601 | }), 602 | ); 603 | signers.push(wrappedSolAccount); 604 | } else { 605 | throw new Error('Invalid payer account'); 606 | } 607 | } 608 | 609 | const placeOrderInstruction = this.makePlaceOrderInstruction(connection, { 610 | owner, 611 | payer: wrappedSolAccount?.publicKey ?? payer, 612 | side, 613 | price, 614 | size, 615 | orderType, 616 | clientId, 617 | openOrdersAddressKey: openOrdersAddress, 618 | feeDiscountPubkey, 619 | }); 620 | transaction.add(placeOrderInstruction); 621 | 622 | if (wrappedSolAccount) { 623 | transaction.add( 624 | closeAccount({ 625 | source: wrappedSolAccount.publicKey, 626 | destination: ownerAddress, 627 | owner: ownerAddress, 628 | }), 629 | ); 630 | } 631 | 632 | return { transaction, signers, payer: owner }; 633 | } 634 | 635 | makePlaceOrderInstruction( 636 | connection: Connection, 637 | { 638 | owner, 639 | payer, 640 | side, 641 | price, 642 | size, 643 | orderType = 'limit', 644 | clientId, 645 | openOrdersAddressKey, 646 | feeDiscountPubkey = null, 647 | }: OrderParams, 648 | ): TransactionInstruction { 649 | // @ts-ignore 650 | const ownerAddress: PublicKey = owner.publicKey ?? owner; 651 | if (this.baseSizeNumberToLots(size).lte(new BN(0))) { 652 | throw new Error('size too small'); 653 | } 654 | if (this.priceNumberToLots(price).lte(new BN(0))) { 655 | throw new Error('invalid price'); 656 | } 657 | if (!this.supportsSrmFeeDiscounts) { 658 | feeDiscountPubkey = null; 659 | } 660 | return DexInstructions.newOrder({ 661 | market: this.address, 662 | requestQueue: this._decoded.requestQueue, 663 | baseVault: this._decoded.baseVault, 664 | quoteVault: this._decoded.quoteVault, 665 | openOrders: openOrdersAddressKey, 666 | owner: ownerAddress, 667 | payer, 668 | side, 669 | limitPrice: this.priceNumberToLots(price), 670 | maxQuantity: this.baseSizeNumberToLots(size), 671 | orderType, 672 | clientId, 673 | programId: this._programId, 674 | feeDiscountPubkey, 675 | }); 676 | } 677 | 678 | private async _sendTransaction( 679 | connection: Connection, 680 | transaction: Transaction, 681 | signers: Array, 682 | ): Promise { 683 | const signature = await connection.sendTransaction(transaction, signers, { 684 | skipPreflight: this._skipPreflight, 685 | }); 686 | const { value } = await connection.confirmTransaction( 687 | signature, 688 | this._commitment, 689 | ); 690 | if (value?.err) { 691 | throw new Error(JSON.stringify(value.err)); 692 | } 693 | return signature; 694 | } 695 | 696 | async cancelOrderByClientId( 697 | connection: Connection, 698 | owner: Account, 699 | openOrders: PublicKey, 700 | clientId: BN, 701 | ) { 702 | const transaction = await this.makeCancelOrderByClientIdTransaction( 703 | connection, 704 | owner.publicKey, 705 | openOrders, 706 | clientId, 707 | ); 708 | return await this._sendTransaction(connection, transaction, [owner]); 709 | } 710 | 711 | async makeCancelOrderByClientIdTransaction( 712 | connection: Connection, 713 | owner: PublicKey, 714 | openOrders: PublicKey, 715 | clientId: BN, 716 | ) { 717 | const transaction = new Transaction(); 718 | transaction.add( 719 | DexInstructions.cancelOrderByClientId({ 720 | market: this.address, 721 | owner, 722 | openOrders, 723 | requestQueue: this._decoded.requestQueue, 724 | clientId, 725 | programId: this._programId, 726 | }), 727 | ); 728 | return transaction; 729 | } 730 | 731 | async cancelOrder(connection: Connection, owner: Account, order: Order) { 732 | const transaction = await this.makeCancelOrderTransaction( 733 | connection, 734 | owner.publicKey, 735 | order, 736 | ); 737 | return await this._sendTransaction(connection, transaction, [owner]); 738 | } 739 | 740 | async makeCancelOrderTransaction( 741 | connection: Connection, 742 | owner: PublicKey, 743 | order: Order, 744 | ) { 745 | const transaction = new Transaction(); 746 | transaction.add(this.makeCancelOrderInstruction(connection, owner, order)); 747 | return transaction; 748 | } 749 | 750 | makeCancelOrderInstruction( 751 | connection: Connection, 752 | owner: PublicKey, 753 | order: Order, 754 | ) { 755 | return DexInstructions.cancelOrder({ 756 | market: this.address, 757 | owner, 758 | openOrders: order.openOrdersAddress, 759 | requestQueue: this._decoded.requestQueue, 760 | side: order.side, 761 | orderId: order.orderId, 762 | openOrdersSlot: order.openOrdersSlot, 763 | programId: this._programId, 764 | }); 765 | } 766 | 767 | async settleFunds( 768 | connection: Connection, 769 | owner: Account, 770 | openOrders: OpenOrders, 771 | baseWallet: PublicKey, 772 | quoteWallet: PublicKey, 773 | referrerQuoteWallet: PublicKey | null = null, 774 | ) { 775 | if (!openOrders.owner.equals(owner.publicKey)) { 776 | throw new Error('Invalid open orders account'); 777 | } 778 | if (referrerQuoteWallet && !this.supportsReferralFees) { 779 | throw new Error('This program ID does not support referrerQuoteWallet'); 780 | } 781 | const { transaction, signers } = await this.makeSettleFundsTransaction( 782 | connection, 783 | openOrders, 784 | baseWallet, 785 | quoteWallet, 786 | referrerQuoteWallet, 787 | ); 788 | return await this._sendTransaction(connection, transaction, [ 789 | owner, 790 | ...signers, 791 | ]); 792 | } 793 | 794 | async makeSettleFundsTransaction( 795 | connection: Connection, 796 | openOrders: OpenOrders, 797 | baseWallet: PublicKey, 798 | quoteWallet: PublicKey, 799 | referrerQuoteWallet: PublicKey | null = null, 800 | ) { 801 | // @ts-ignore 802 | const vaultSigner = await PublicKey.createProgramAddress( 803 | [ 804 | this.address.toBuffer(), 805 | this._decoded.vaultSignerNonce.toArrayLike(Buffer, 'le', 8), 806 | ], 807 | this._programId, 808 | ); 809 | 810 | const transaction = new Transaction(); 811 | const signers: Account[] = []; 812 | 813 | let wrappedSolAccount: Account | null = null; 814 | if ( 815 | (this.baseMintAddress.equals(WRAPPED_SOL_MINT) && 816 | baseWallet.equals(openOrders.owner)) || 817 | (this.quoteMintAddress.equals(WRAPPED_SOL_MINT) && 818 | quoteWallet.equals(openOrders.owner)) 819 | ) { 820 | wrappedSolAccount = new Account(); 821 | transaction.add( 822 | SystemProgram.createAccount({ 823 | fromPubkey: openOrders.owner, 824 | newAccountPubkey: wrappedSolAccount.publicKey, 825 | lamports: await connection.getMinimumBalanceForRentExemption(165), 826 | space: 165, 827 | programId: TOKEN_PROGRAM_ID, 828 | }), 829 | ); 830 | transaction.add( 831 | initializeAccount({ 832 | account: wrappedSolAccount.publicKey, 833 | mint: WRAPPED_SOL_MINT, 834 | owner: openOrders.owner, 835 | }), 836 | ); 837 | signers.push(wrappedSolAccount); 838 | } 839 | 840 | transaction.add( 841 | DexInstructions.settleFunds({ 842 | market: this.address, 843 | openOrders: openOrders.address, 844 | owner: openOrders.owner, 845 | baseVault: this._decoded.baseVault, 846 | quoteVault: this._decoded.quoteVault, 847 | baseWallet: 848 | baseWallet.equals(openOrders.owner) && wrappedSolAccount 849 | ? wrappedSolAccount.publicKey 850 | : baseWallet, 851 | quoteWallet: 852 | quoteWallet.equals(openOrders.owner) && wrappedSolAccount 853 | ? wrappedSolAccount.publicKey 854 | : quoteWallet, 855 | vaultSigner, 856 | programId: this._programId, 857 | referrerQuoteWallet, 858 | }), 859 | ); 860 | 861 | if (wrappedSolAccount) { 862 | transaction.add( 863 | closeAccount({ 864 | source: wrappedSolAccount.publicKey, 865 | destination: openOrders.owner, 866 | owner: openOrders.owner, 867 | }), 868 | ); 869 | } 870 | 871 | return { transaction, signers, payer: openOrders.owner }; 872 | } 873 | 874 | async matchOrders(connection: Connection, feePayer: Account, limit: number) { 875 | const tx = this.makeMatchOrdersTransaction(limit); 876 | return await this._sendTransaction(connection, tx, [feePayer]); 877 | } 878 | 879 | makeMatchOrdersTransaction(limit: number): Transaction { 880 | const tx = new Transaction(); 881 | tx.add( 882 | DexInstructions.matchOrders({ 883 | market: this.address, 884 | requestQueue: this._decoded.requestQueue, 885 | eventQueue: this._decoded.eventQueue, 886 | bids: this._decoded.bids, 887 | asks: this._decoded.asks, 888 | baseVault: this._decoded.baseVault, 889 | quoteVault: this._decoded.quoteVault, 890 | limit, 891 | programId: this._programId, 892 | }), 893 | ); 894 | return tx; 895 | } 896 | 897 | async loadRequestQueue(connection: Connection) { 898 | const { data } = throwIfNull( 899 | await connection.getAccountInfo(this._decoded.requestQueue), 900 | ); 901 | return decodeRequestQueue(data); 902 | } 903 | 904 | async loadEventQueue(connection: Connection) { 905 | const { data } = throwIfNull( 906 | await connection.getAccountInfo(this._decoded.eventQueue), 907 | ); 908 | return decodeEventQueue(data); 909 | } 910 | 911 | async loadFills(connection: Connection, limit = 100) { 912 | // TODO: once there's a separate source of fills use that instead 913 | const { data } = throwIfNull( 914 | await connection.getAccountInfo(this._decoded.eventQueue), 915 | ); 916 | const events = decodeEventQueue(data, limit); 917 | return events 918 | .filter( 919 | (event) => event.eventFlags.fill && event.nativeQuantityPaid.gtn(0), 920 | ) 921 | .map(this.parseFillEvent.bind(this)); 922 | } 923 | 924 | parseFillEvent(event) { 925 | let size, price, side, priceBeforeFees; 926 | if (event.eventFlags.bid) { 927 | side = 'buy'; 928 | priceBeforeFees = event.eventFlags.maker 929 | ? event.nativeQuantityPaid.add(event.nativeFeeOrRebate) 930 | : event.nativeQuantityPaid.sub(event.nativeFeeOrRebate); 931 | price = divideBnToNumber( 932 | priceBeforeFees.mul(this._baseSplTokenMultiplier), 933 | this._quoteSplTokenMultiplier.mul(event.nativeQuantityReleased), 934 | ); 935 | size = divideBnToNumber( 936 | event.nativeQuantityReleased, 937 | this._baseSplTokenMultiplier, 938 | ); 939 | } else { 940 | side = 'sell'; 941 | priceBeforeFees = event.eventFlags.maker 942 | ? event.nativeQuantityReleased.sub(event.nativeFeeOrRebate) 943 | : event.nativeQuantityReleased.add(event.nativeFeeOrRebate); 944 | price = divideBnToNumber( 945 | priceBeforeFees.mul(this._baseSplTokenMultiplier), 946 | this._quoteSplTokenMultiplier.mul(event.nativeQuantityPaid), 947 | ); 948 | size = divideBnToNumber( 949 | event.nativeQuantityPaid, 950 | this._baseSplTokenMultiplier, 951 | ); 952 | } 953 | return { 954 | ...event, 955 | side, 956 | price, 957 | feeCost: 958 | this.quoteSplSizeToNumber(event.nativeFeeOrRebate) * 959 | (event.eventFlags.maker ? -1 : 1), 960 | size, 961 | }; 962 | } 963 | 964 | private get _baseSplTokenMultiplier() { 965 | return new BN(10).pow(new BN(this._baseSplTokenDecimals)); 966 | } 967 | 968 | private get _quoteSplTokenMultiplier() { 969 | return new BN(10).pow(new BN(this._quoteSplTokenDecimals)); 970 | } 971 | 972 | priceLotsToNumber(price: BN) { 973 | return divideBnToNumber( 974 | price.mul(this._decoded.quoteLotSize).mul(this._baseSplTokenMultiplier), 975 | this._decoded.baseLotSize.mul(this._quoteSplTokenMultiplier), 976 | ); 977 | } 978 | 979 | priceNumberToLots(price: number): BN { 980 | return new BN( 981 | Math.round( 982 | (price * 983 | Math.pow(10, this._quoteSplTokenDecimals) * 984 | this._decoded.baseLotSize.toNumber()) / 985 | (Math.pow(10, this._baseSplTokenDecimals) * 986 | this._decoded.quoteLotSize.toNumber()), 987 | ), 988 | ); 989 | } 990 | 991 | baseSplSizeToNumber(size: BN) { 992 | return divideBnToNumber(size, this._baseSplTokenMultiplier); 993 | } 994 | 995 | quoteSplSizeToNumber(size: BN) { 996 | return divideBnToNumber(size, this._quoteSplTokenMultiplier); 997 | } 998 | 999 | baseSizeLotsToNumber(size: BN) { 1000 | return divideBnToNumber( 1001 | size.mul(this._decoded.baseLotSize), 1002 | this._baseSplTokenMultiplier, 1003 | ); 1004 | } 1005 | 1006 | baseSizeNumberToLots(size: number): BN { 1007 | const native = new BN( 1008 | Math.round(size * Math.pow(10, this._baseSplTokenDecimals)), 1009 | ); 1010 | // rounds down to the nearest lot size 1011 | return native.div(this._decoded.baseLotSize); 1012 | } 1013 | 1014 | quoteSizeLotsToNumber(size: BN) { 1015 | return divideBnToNumber( 1016 | size.mul(this._decoded.quoteLotSize), 1017 | this._quoteSplTokenMultiplier, 1018 | ); 1019 | } 1020 | 1021 | quoteSizeNumberToLots(size: number): BN { 1022 | const native = new BN( 1023 | Math.round(size * Math.pow(10, this._quoteSplTokenDecimals)), 1024 | ); 1025 | // rounds down to the nearest lot size 1026 | return native.div(this._decoded.quoteLotSize); 1027 | } 1028 | 1029 | get minOrderSize() { 1030 | return this.baseSizeLotsToNumber(new BN(1)); 1031 | } 1032 | 1033 | get tickSize() { 1034 | return this.priceLotsToNumber(new BN(1)); 1035 | } 1036 | } 1037 | 1038 | export interface MarketOptions { 1039 | skipPreflight?: boolean; 1040 | commitment?: Commitment; 1041 | } 1042 | 1043 | export interface OrderParams { 1044 | owner: T; 1045 | payer: PublicKey; 1046 | side: 'buy' | 'sell'; 1047 | price: number; 1048 | size: number; 1049 | orderType?: 'limit' | 'ioc' | 'postOnly'; 1050 | clientId?: BN; 1051 | openOrdersAddressKey?: PublicKey; 1052 | feeDiscountPubkey?: PublicKey | null; 1053 | } 1054 | 1055 | export const _OPEN_ORDERS_LAYOUT_V1 = struct([ 1056 | blob(5), 1057 | 1058 | accountFlagsLayout('accountFlags'), 1059 | 1060 | publicKeyLayout('market'), 1061 | publicKeyLayout('owner'), 1062 | 1063 | // These are in spl-token (i.e. not lot) units 1064 | u64('baseTokenFree'), 1065 | u64('baseTokenTotal'), 1066 | u64('quoteTokenFree'), 1067 | u64('quoteTokenTotal'), 1068 | 1069 | u128('freeSlotBits'), 1070 | u128('isBidBits'), 1071 | 1072 | seq(u128(), 128, 'orders'), 1073 | seq(u64(), 128, 'clientIds'), 1074 | 1075 | blob(7), 1076 | ]); 1077 | 1078 | export const _OPEN_ORDERS_LAYOUT_V2 = struct([ 1079 | blob(5), 1080 | 1081 | accountFlagsLayout('accountFlags'), 1082 | 1083 | publicKeyLayout('market'), 1084 | publicKeyLayout('owner'), 1085 | 1086 | // These are in spl-token (i.e. not lot) units 1087 | u64('baseTokenFree'), 1088 | u64('baseTokenTotal'), 1089 | u64('quoteTokenFree'), 1090 | u64('quoteTokenTotal'), 1091 | 1092 | u128('freeSlotBits'), 1093 | u128('isBidBits'), 1094 | 1095 | seq(u128(), 128, 'orders'), 1096 | seq(u64(), 128, 'clientIds'), 1097 | 1098 | u64('referrerRebatesAccrued'), 1099 | 1100 | blob(7), 1101 | ]); 1102 | 1103 | export class OpenOrders { 1104 | private _programId: PublicKey; 1105 | 1106 | address: PublicKey; 1107 | market!: PublicKey; 1108 | owner!: PublicKey; 1109 | 1110 | baseTokenFree!: BN; 1111 | baseTokenTotal!: BN; 1112 | quoteTokenFree!: BN; 1113 | quoteTokenTotal!: BN; 1114 | 1115 | orders!: BN[]; 1116 | clientIds!: BN[]; 1117 | 1118 | constructor(address: PublicKey, decoded, programId: PublicKey) { 1119 | this.address = address; 1120 | this._programId = programId; 1121 | Object.assign(this, decoded); 1122 | } 1123 | 1124 | static getLayout(programId: PublicKey) { 1125 | if (getLayoutVersion(programId) === 1) { 1126 | return _OPEN_ORDERS_LAYOUT_V1; 1127 | } 1128 | return _OPEN_ORDERS_LAYOUT_V2; 1129 | } 1130 | 1131 | static async findForOwner( 1132 | connection: Connection, 1133 | ownerAddress: PublicKey, 1134 | programId: PublicKey, 1135 | ) { 1136 | const filters = [ 1137 | { 1138 | memcmp: { 1139 | offset: this.getLayout(programId).offsetOf('owner'), 1140 | bytes: ownerAddress.toBase58(), 1141 | }, 1142 | }, 1143 | { 1144 | dataSize: this.getLayout(programId).span, 1145 | }, 1146 | ]; 1147 | const accounts = await getFilteredProgramAccounts( 1148 | connection, 1149 | programId, 1150 | filters, 1151 | ); 1152 | return accounts.map(({ publicKey, accountInfo }) => 1153 | OpenOrders.fromAccountInfo(publicKey, accountInfo, programId), 1154 | ); 1155 | } 1156 | 1157 | static async findForMarketAndOwner( 1158 | connection: Connection, 1159 | marketAddress: PublicKey, 1160 | ownerAddress: PublicKey, 1161 | programId: PublicKey, 1162 | ) { 1163 | const filters = [ 1164 | { 1165 | memcmp: { 1166 | offset: this.getLayout(programId).offsetOf('market'), 1167 | bytes: marketAddress.toBase58(), 1168 | }, 1169 | }, 1170 | { 1171 | memcmp: { 1172 | offset: this.getLayout(programId).offsetOf('owner'), 1173 | bytes: ownerAddress.toBase58(), 1174 | }, 1175 | }, 1176 | { 1177 | dataSize: this.getLayout(programId).span, 1178 | }, 1179 | ]; 1180 | const accounts = await getFilteredProgramAccounts( 1181 | connection, 1182 | programId, 1183 | filters, 1184 | ); 1185 | return accounts.map(({ publicKey, accountInfo }) => 1186 | OpenOrders.fromAccountInfo(publicKey, accountInfo, programId), 1187 | ); 1188 | } 1189 | 1190 | static async load( 1191 | connection: Connection, 1192 | address: PublicKey, 1193 | programId: PublicKey, 1194 | ) { 1195 | const accountInfo = await connection.getAccountInfo(address); 1196 | if (accountInfo === null) { 1197 | throw new Error('Open orders account not found'); 1198 | } 1199 | return OpenOrders.fromAccountInfo(address, accountInfo, programId); 1200 | } 1201 | 1202 | static fromAccountInfo( 1203 | address: PublicKey, 1204 | accountInfo: AccountInfo, 1205 | programId: PublicKey, 1206 | ) { 1207 | const { owner, data } = accountInfo; 1208 | if (!owner.equals(programId)) { 1209 | throw new Error('Address not owned by program'); 1210 | } 1211 | const decoded = this.getLayout(programId).decode(data); 1212 | if (!decoded.accountFlags.initialized || !decoded.accountFlags.openOrders) { 1213 | throw new Error('Invalid open orders account'); 1214 | } 1215 | return new OpenOrders(address, decoded, programId); 1216 | } 1217 | 1218 | static async makeCreateAccountTransaction( 1219 | connection: Connection, 1220 | marketAddress: PublicKey, 1221 | ownerAddress: PublicKey, 1222 | newAccountAddress: PublicKey, 1223 | programId: PublicKey, 1224 | ) { 1225 | return SystemProgram.createAccount({ 1226 | fromPubkey: ownerAddress, 1227 | newAccountPubkey: newAccountAddress, 1228 | lamports: await connection.getMinimumBalanceForRentExemption( 1229 | this.getLayout(programId).span, 1230 | ), 1231 | space: this.getLayout(programId).span, 1232 | programId, 1233 | }); 1234 | } 1235 | 1236 | get publicKey() { 1237 | return this.address; 1238 | } 1239 | } 1240 | 1241 | export const ORDERBOOK_LAYOUT = struct([ 1242 | blob(5), 1243 | accountFlagsLayout('accountFlags'), 1244 | SLAB_LAYOUT.replicate('slab'), 1245 | blob(7), 1246 | ]); 1247 | 1248 | export class Orderbook { 1249 | market: Market; 1250 | isBids: boolean; 1251 | slab: Slab; 1252 | 1253 | constructor(market: Market, accountFlags, slab: Slab) { 1254 | if (!accountFlags.initialized || !(accountFlags.bids ^ accountFlags.asks)) { 1255 | throw new Error('Invalid orderbook'); 1256 | } 1257 | this.market = market; 1258 | this.isBids = accountFlags.bids; 1259 | this.slab = slab; 1260 | } 1261 | 1262 | static get LAYOUT() { 1263 | return ORDERBOOK_LAYOUT; 1264 | } 1265 | 1266 | static decode(market: Market, buffer: Buffer) { 1267 | const { accountFlags, slab } = ORDERBOOK_LAYOUT.decode(buffer); 1268 | return new Orderbook(market, accountFlags, slab); 1269 | } 1270 | 1271 | getL2(depth: number): [number, number, BN, BN][] { 1272 | const descending = this.isBids; 1273 | const levels: [BN, BN][] = []; // (price, size) 1274 | for (const { key, quantity } of this.slab.items(descending)) { 1275 | const price = getPriceFromKey(key); 1276 | if (levels.length > 0 && levels[levels.length - 1][0].eq(price)) { 1277 | levels[levels.length - 1][1].iadd(quantity); 1278 | } else if (levels.length === depth) { 1279 | break; 1280 | } else { 1281 | levels.push([price, quantity]); 1282 | } 1283 | } 1284 | return levels.map(([priceLots, sizeLots]) => [ 1285 | this.market.priceLotsToNumber(priceLots), 1286 | this.market.baseSizeLotsToNumber(sizeLots), 1287 | priceLots, 1288 | sizeLots, 1289 | ]); 1290 | } 1291 | 1292 | *[Symbol.iterator](): Generator { 1293 | for (const { 1294 | key, 1295 | ownerSlot, 1296 | owner, 1297 | quantity, 1298 | feeTier, 1299 | clientOrderId, 1300 | } of this.slab) { 1301 | const price = getPriceFromKey(key); 1302 | yield { 1303 | orderId: key, 1304 | clientId: clientOrderId, 1305 | openOrdersAddress: owner, 1306 | openOrdersSlot: ownerSlot, 1307 | feeTier, 1308 | price: this.market.priceLotsToNumber(price), 1309 | priceLots: price, 1310 | size: this.market.baseSizeLotsToNumber(quantity), 1311 | sizeLots: quantity, 1312 | side: (this.isBids ? 'buy' : 'sell') as 'buy' | 'sell', 1313 | }; 1314 | } 1315 | } 1316 | } 1317 | 1318 | export interface Order { 1319 | orderId: BN; 1320 | openOrdersAddress: PublicKey; 1321 | openOrdersSlot: number; 1322 | price: number; 1323 | priceLots: BN; 1324 | size: number; 1325 | feeTier: number; 1326 | sizeLots: BN; 1327 | side: 'buy' | 'sell'; 1328 | clientId?: BN; 1329 | } 1330 | 1331 | function getPriceFromKey(key) { 1332 | return key.ushrn(64); 1333 | } 1334 | 1335 | function divideBnToNumber(numerator: BN, denominator: BN): number { 1336 | const quotient = numerator.div(denominator).toNumber(); 1337 | const rem = numerator.umod(denominator); 1338 | const gcd = rem.gcd(denominator); 1339 | return quotient + rem.div(gcd).toNumber() / denominator.div(gcd).toNumber(); 1340 | } 1341 | 1342 | const MINT_LAYOUT = struct([blob(44), u8('decimals'), blob(37)]); 1343 | 1344 | export async function getMintDecimals( 1345 | connection: Connection, 1346 | mint: PublicKey, 1347 | ): Promise { 1348 | if (mint.equals(WRAPPED_SOL_MINT)) { 1349 | return 9; 1350 | } 1351 | const { data } = throwIfNull( 1352 | await connection.getAccountInfo(mint), 1353 | 'mint not found', 1354 | ); 1355 | const { decimals } = MINT_LAYOUT.decode(data); 1356 | return decimals; 1357 | } 1358 | 1359 | async function getFilteredProgramAccounts( 1360 | connection: Connection, 1361 | programId: PublicKey, 1362 | filters, 1363 | ): Promise<{ publicKey: PublicKey; accountInfo: AccountInfo }[]> { 1364 | // @ts-ignore 1365 | const resp = await connection._rpcRequest('getProgramAccounts', [ 1366 | programId.toBase58(), 1367 | { 1368 | commitment: connection.commitment, 1369 | filters, 1370 | encoding: 'base64', 1371 | }, 1372 | ]); 1373 | if (resp.error) { 1374 | throw new Error(resp.error.message); 1375 | } 1376 | return resp.result.map( 1377 | ({ pubkey, account: { data, executable, owner, lamports } }) => ({ 1378 | publicKey: new PublicKey(pubkey), 1379 | accountInfo: { 1380 | data: Buffer.from(data[0], 'base64'), 1381 | executable, 1382 | owner: new PublicKey(owner), 1383 | lamports, 1384 | }, 1385 | }), 1386 | ); 1387 | } 1388 | 1389 | function throwIfNull(value: T | null, message = 'account not found'): T { 1390 | if (value === null) { 1391 | throw new Error(message); 1392 | } 1393 | return value; 1394 | } 1395 | -------------------------------------------------------------------------------- /src/markets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "address": "EmCzMQfXMgNHcnRoFwAdPe1i2SuiSzMj1mx6wu3KN2uA", 4 | "name": "ALEPH/USDT", 5 | "deprecated": true, 6 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 7 | }, 8 | { 9 | "address": "B37pZmwrwXHjpgvd9hHDAx1yeDsNevTnbbrN9W12BoGK", 10 | "name": "ALEPH/WUSDC", 11 | "deprecated": true, 12 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 13 | }, 14 | { 15 | "address": "8AcVjMG2LTbpkjNoyq8RwysokqZunkjy3d5JDzxC6BJa", 16 | "name": "BTC/USDT", 17 | "deprecated": true, 18 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 19 | }, 20 | { 21 | "address": "CAgAeMD7quTdnr6RPa7JySQpjf3irAmefYNdTb6anemq", 22 | "name": "BTC/WUSDC", 23 | "deprecated": true, 24 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 25 | }, 26 | { 27 | "address": "HfCZdJ1wfsWKfYP2qyWdXTT5PWAGWFctzFjLH48U1Hsd", 28 | "name": "ETH/USDT", 29 | "deprecated": true, 30 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 31 | }, 32 | { 33 | "address": "ASKiV944nKg1W9vsf7hf3fTsjawK6DwLwrnB2LH9n61c", 34 | "name": "ETH/WUSDC", 35 | "deprecated": true, 36 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 37 | }, 38 | { 39 | "address": "8mDuvJJSgoodovMRYArtVVYBbixWYdGzR47GPrRT65YJ", 40 | "name": "SOL/USDT", 41 | "deprecated": true, 42 | "programId": "BJ3jrUzddfuSrZHXSCxMUUQsjKEyLmuuyZebkcaFp2fg" 43 | }, 44 | { 45 | "address": "Cdp72gDcYMCLLk3aDkPxjeiirKoFqK38ECm8Ywvk94Wi", 46 | "name": "SOL/WUSDC", 47 | "deprecated": true, 48 | "programId": "BJ3jrUzddfuSrZHXSCxMUUQsjKEyLmuuyZebkcaFp2fg" 49 | }, 50 | { 51 | "address": "HARFLhSq8nECZk4DVFKvzqXMNMA9a3hjvridGMFizeLa", 52 | "name": "SRM/USDT", 53 | "deprecated": true, 54 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 55 | }, 56 | { 57 | "address": "68J6nkWToik6oM9rTatKSR5ibVSykAtzftBUEAvpRsys", 58 | "name": "SRM/WUSDC", 59 | "deprecated": true, 60 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 61 | }, 62 | { 63 | "address": "DzFjazak6EKHnaB2w6qSsArnj28CV1TKd2Smcj9fqtHW", 64 | "name": "SUSHI/USDT", 65 | "deprecated": true, 66 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 67 | }, 68 | { 69 | "address": "9wDmxsfwaDb2ysmZpBLzxKzoWrF1zHzBN7PV5EmJe19R", 70 | "name": "SUSHI/WUSDC", 71 | "deprecated": true, 72 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 73 | }, 74 | { 75 | "address": "GuvWMATdEV6DExWnXncPYEzn4ePWYkvGdC8pu8gsn7m7", 76 | "name": "SXP/USDT", 77 | "deprecated": true, 78 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 79 | }, 80 | { 81 | "address": "GbQSffne1NcJbS4jsewZEpRGYVR4RNnuVUN8Ht6vAGb6", 82 | "name": "SXP/WUSDC", 83 | "deprecated": true, 84 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 85 | }, 86 | { 87 | "address": "H4snTKK9adiU15gP22ErfZYtro3aqR9BTMXiH3AwiUTQ", 88 | "name": "MSRM/USDT", 89 | "deprecated": true, 90 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 91 | }, 92 | { 93 | "address": "7kgkDyW7dmyMeP8KFXzbcUZz1R2WHsovDZ7n3ihZuNDS", 94 | "name": "MSRM/WUSDC", 95 | "deprecated": true, 96 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 97 | }, 98 | { 99 | "address": "DHDdghmkBhEpReno3tbzBPtsxCt6P3KrMzZvxavTktJt", 100 | "name": "FTT/USDT", 101 | "deprecated": true, 102 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 103 | }, 104 | { 105 | "address": "FZqrBXz7ADGsmDf1TM9YgysPUfvtG8rJiNUrqDpHc9Au", 106 | "name": "FTT/WUSDC", 107 | "deprecated": true, 108 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 109 | }, 110 | { 111 | "address": "5zu5bTZZvqESAAgFsr12CUMxdQvMrvU9CgvC1GW8vJdf", 112 | "name": "YFI/USDT", 113 | "deprecated": true, 114 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 115 | }, 116 | { 117 | "address": "FJg9FUtbN3fg3YFbMCFiZKjGh5Bn4gtzxZmtxFzmz9kT", 118 | "name": "YFI/WUSDC", 119 | "deprecated": true, 120 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 121 | }, 122 | { 123 | "address": "F5xschQBMpu1gD2q1babYEAVJHR1buj1YazLiXyQNqSW", 124 | "name": "LINK/USDT", 125 | "deprecated": true, 126 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 127 | }, 128 | { 129 | "address": "7GZ59DMgJ7D6dfoJTpszPayTRyua9jwcaGJXaRMMF1my", 130 | "name": "LINK/WUSDC", 131 | "deprecated": true, 132 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 133 | }, 134 | { 135 | "address": "BAbc9baz4hV1hnYjWSJ6cZDRjfvziWbYGQu9UFkcdUmx", 136 | "name": "HGET/USDT", 137 | "deprecated": true, 138 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 139 | }, 140 | { 141 | "address": "uPNcBgFhrLW3FtvyYYbBUi53BBEQf9e4NPgwxaLu5Hn", 142 | "name": "HGET/WUSDC", 143 | "deprecated": true, 144 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 145 | }, 146 | { 147 | "address": "3puWJFZyCso14EdxhywjD7xqyTarpsULx483mzvqxQRW", 148 | "name": "CREAM/WUSDC", 149 | "deprecated": true, 150 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 151 | }, 152 | { 153 | "address": "EBxJWA2nLV57ZntbjizxH527ZjPNLT5cpUHMnY5k3oq", 154 | "name": "CREAM/USDT", 155 | "deprecated": true, 156 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 157 | }, 158 | { 159 | "address": "8Ae7Uhigx8k4fKdJG7irdPCVDZLvWsJfeTH2t5fr3TVD", 160 | "name": "UBXT/WUSDC", 161 | "deprecated": true, 162 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 163 | }, 164 | { 165 | "address": "46VdEkj4MJwZinwVb3Y7DUDpVXLNb9YW7P2waKU3vCqr", 166 | "name": "UBXT/USDT", 167 | "deprecated": true, 168 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 169 | }, 170 | { 171 | "address": "Hze5AUX4Qp1cTujiJ4CsAMRGn4g6ZpgXsmptFn3xxhWg", 172 | "name": "HNT/WUSDC", 173 | "deprecated": true, 174 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 175 | }, 176 | { 177 | "address": "Hc22rHKrhbrZBaQMmhJvPTkp1yDr31PDusU8wKoqFSZV", 178 | "name": "HNT/USDT", 179 | "deprecated": true, 180 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 181 | }, 182 | { 183 | "address": "FJq4HX3bUSgF3yQZ8ADALtJYfAyr9fz36SNG18hc3dgF", 184 | "name": "FRONT/WUSDC", 185 | "deprecated": true, 186 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 187 | }, 188 | { 189 | "address": "HFoca5HKwiTPpw9iUY5iXWqzkXdu88dS7YrpSvt2uhyF", 190 | "name": "FRONT/USDT", 191 | "deprecated": true, 192 | "programId": "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn" 193 | }, 194 | { 195 | "address": "5xnYnWca2bFwC6cPufpdsCbDJhMjYCC59YgwoZHEfiee", 196 | "name": "ALEPH/USDT", 197 | "deprecated": false, 198 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 199 | }, 200 | { 201 | "address": "BZMuoQ2i2noNUXMdrRDivc7MwjGspNJTCfZkdHMwK18T", 202 | "name": "ALEPH/WUSDC", 203 | "deprecated": true, 204 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 205 | }, 206 | { 207 | "address": "EXnGBBSamqzd3uxEdRLUiYzjJkTwQyorAaFXdfteuGXe", 208 | "name": "BTC/USDT", 209 | "deprecated": false, 210 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 211 | }, 212 | { 213 | "address": "5LgJphS6D5zXwUVPU7eCryDBkyta3AidrJ5vjNU6BcGW", 214 | "name": "BTC/WUSDC", 215 | "deprecated": true, 216 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 217 | }, 218 | { 219 | "address": "5abZGhrELnUnfM9ZUnvK6XJPoBU5eShZwfFPkdhAC7o", 220 | "name": "ETH/USDT", 221 | "deprecated": false, 222 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 223 | }, 224 | { 225 | "address": "DmEDKZPXXkWgaYiKgWws2ZXWWKCh41eryDPRVD4zKnD9", 226 | "name": "ETH/WUSDC", 227 | "deprecated": true, 228 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 229 | }, 230 | { 231 | "address": "7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932", 232 | "name": "SOL/USDT", 233 | "deprecated": false, 234 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 235 | }, 236 | { 237 | "address": "EBFTQNg2QjyxV7WDDenoLbfLLXLcbSz6w1YrdTCGPWT5", 238 | "name": "SOL/WUSDC", 239 | "deprecated": true, 240 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 241 | }, 242 | { 243 | "address": "H3APNWA8bZW2gLMSq5sRL41JSMmEJ648AqoEdDgLcdvB", 244 | "name": "SRM/USDT", 245 | "deprecated": false, 246 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 247 | }, 248 | { 249 | "address": "8YmQZRXGizZXYPCDmxgjwB8X8XN4PZG7MMwNg76iAmPZ", 250 | "name": "SRM/WUSDC", 251 | "deprecated": true, 252 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 253 | }, 254 | { 255 | "address": "4uZTPc72sCDcVRfKKii67dTPm2Xe4ri3TYnGcUQrtnU9", 256 | "name": "SUSHI/USDT", 257 | "deprecated": false, 258 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 259 | }, 260 | { 261 | "address": "9vFuX2BizwinWjkZLQTmThDcNMFEcY3wVXYuqnRQtcD", 262 | "name": "SUSHI/WUSDC", 263 | "deprecated": true, 264 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 265 | }, 266 | { 267 | "address": "33GHmwG9woY95JuWNi74Aa8uKvysSXxif9P1EwwkrCRz", 268 | "name": "SXP/USDT", 269 | "deprecated": false, 270 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 271 | }, 272 | { 273 | "address": "C5NReXAeQhfjiDCGPFj1UUmDxDqF8v2CUVKoYuQqb4eW", 274 | "name": "SXP/WUSDC", 275 | "deprecated": true, 276 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 277 | }, 278 | { 279 | "address": "FUaF58sDrgbqakHTR8RUwRLauSofRTjqyCsqThFPh6YM", 280 | "name": "MSRM/USDT", 281 | "deprecated": false, 282 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 283 | }, 284 | { 285 | "address": "58H7ZRmiyWtsrz2sQGz1qQCMW6n7447xhNNehUSQGPj5", 286 | "name": "MSRM/WUSDC", 287 | "deprecated": true, 288 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 289 | }, 290 | { 291 | "address": "5NqjQVXLuLSDnsnQMfWp3rF9gbWDusWG4B1Xwtk3rZ5S", 292 | "name": "FTT/USDT", 293 | "deprecated": false, 294 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 295 | }, 296 | { 297 | "address": "ES8skmkEeyH1BYFThd2FtyaFKhkqtwH7XWp8mXptv3vg", 298 | "name": "FTT/WUSDC", 299 | "deprecated": true, 300 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 301 | }, 302 | { 303 | "address": "97NiXHUNkpYd1eb2HthSDGhaPfepuqMAV3QsZhAgb1wm", 304 | "name": "YFI/USDT", 305 | "deprecated": false, 306 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 307 | }, 308 | { 309 | "address": "Gw78CYLLFbgmmn4rps9KoPAnNtBQ2S1foL2Mn6Z5ZHYB", 310 | "name": "YFI/WUSDC", 311 | "deprecated": true, 312 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 313 | }, 314 | { 315 | "address": "hBswhpNyz4m5nt4KwtCA7jYXvh7VmyZ4TuuPmpaKQb1", 316 | "name": "LINK/USDT", 317 | "deprecated": false, 318 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 319 | }, 320 | { 321 | "address": "WjfsTPyrvUUrhGJ9hVQFubMnKDcnQS8VxSXU7L2gLcA", 322 | "name": "LINK/WUSDC", 323 | "deprecated": true, 324 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 325 | }, 326 | { 327 | "address": "GaeUpY7CT8rjoeVGjY1t3mJJDd1bdXxYWtrGSpsVFors", 328 | "name": "HGET/USDT", 329 | "deprecated": false, 330 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 331 | }, 332 | { 333 | "address": "2ZmB255T4FVUugpeXTFxD6Yz5GE47yTByYvqSTDUbk3G", 334 | "name": "HGET/WUSDC", 335 | "deprecated": true, 336 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 337 | }, 338 | { 339 | "address": "FGJtCDXoHLHjagP5Ht6xcUFt2rW3z8MJPe87rFKP2ZW6", 340 | "name": "CREAM/WUSDC", 341 | "deprecated": true, 342 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 343 | }, 344 | { 345 | "address": "7qq9BABQvTWKZuJ5fX2PeTKX6XVtduEs9zW9WS21fSzN", 346 | "name": "CREAM/USDT", 347 | "deprecated": false, 348 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 349 | }, 350 | { 351 | "address": "7K6MPog6LskZmyaYwqtLvRUuedoiE68nirbQ9tK3LasE", 352 | "name": "UBXT/WUSDC", 353 | "deprecated": true, 354 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 355 | }, 356 | { 357 | "address": "DCHvVahuLTNWBGUtEzF5GrTdx5FRpxqEJiS6Ru1hrDfD", 358 | "name": "UBXT/USDT", 359 | "deprecated": false, 360 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 361 | }, 362 | { 363 | "address": "9RyozJe3bkAFfH3jmoiKHjkWCoLTxn7aBQSi6YfaV6ab", 364 | "name": "HNT/WUSDC", 365 | "deprecated": true, 366 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 367 | }, 368 | { 369 | "address": "DWjJ8VHdGYBxDQYdrRBVDWkHswrgjuBFEv5pBhiRoPBz", 370 | "name": "HNT/USDT", 371 | "deprecated": false, 372 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 373 | }, 374 | { 375 | "address": "AGtBbGuJZiv3Ko3dfT4v6g4kCqnNc9DXfoGLe5HpjmWx", 376 | "name": "FRONT/WUSDC", 377 | "deprecated": true, 378 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 379 | }, 380 | { 381 | "address": "56eqxJYzPigm4FkigiBdsfebjMgAbKNh24E7oiKLBtye", 382 | "name": "FRONT/USDT", 383 | "deprecated": false, 384 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 385 | }, 386 | { 387 | "address": "AA1HSrsMcRNzjaQfRMTNarHR9B7e4U79LJ2319UtiqPF", 388 | "name": "AKRO/WUSDC", 389 | "deprecated": true, 390 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 391 | }, 392 | { 393 | "address": "FQbCNSVH3RgosCPB4CJRstkLh5hXkvuXzAjQzT11oMYo", 394 | "name": "AKRO/USDT", 395 | "deprecated": false, 396 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 397 | }, 398 | { 399 | "address": "Fs5xtGUmJTYo8Ao75M3R3m3mVX53KMUhzfXCmyRLnp2P", 400 | "name": "HXRO/USDT", 401 | "deprecated": false, 402 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 403 | }, 404 | { 405 | "address": "AUAobJdffexcoJBMeyLorpShu3ZtG9VvPEPjoeTN4u5Z", 406 | "name": "HXRO/WUSDC", 407 | "deprecated": true, 408 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 409 | }, 410 | { 411 | "address": "ChKV7mxecPqFPGYJjhzowPHDiLKFWXXVujUiE3EWxFcg", 412 | "name": "UNI/USDT", 413 | "deprecated": false, 414 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 415 | }, 416 | { 417 | "address": "GpdYLFbKHeSeDGqsnQ4jnP7D1294iBpQcsN1VPwhoaFS", 418 | "name": "UNI/WUSDC", 419 | "deprecated": true, 420 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 421 | }, 422 | { 423 | "address": "6N3oU7ALvn2RPwdpYVzPBgQJ8njT29inBbS2tSrwx8fh", 424 | "name": "KEEP/USDT", 425 | "deprecated": false, 426 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 427 | }, 428 | { 429 | "address": "sxS9EdTx1UPe4j2c6Au9f1GKZXrFj5pTgNKgjGGtGdY", 430 | "name": "KEEP/WUSDC", 431 | "deprecated": true, 432 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 433 | }, 434 | { 435 | "address": "5P6dJbyKySFXMYNWiEcNQu8xPRYsehYzCeVpae9Ueqrg", 436 | "name": "MATH/USDT", 437 | "deprecated": false, 438 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 439 | }, 440 | { 441 | "address": "CfnnU38ACScF6pcurxSB3FLXeZmfFYunVKExeUyosu5P", 442 | "name": "MATH/WUSDC", 443 | "deprecated": true, 444 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 445 | }, 446 | { 447 | "address": "7NR5GDouQYkkfppVkNhpa4HfJ2LwqUQymE3b4CYQiYHa", 448 | "name": "ALEPH/USDC", 449 | "deprecated": false, 450 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 451 | }, 452 | { 453 | "address": "CVfYa8RGXnuDBeGmniCcdkBwoLqVxh92xB1JqgRQx3F", 454 | "name": "BTC/USDC", 455 | "deprecated": false, 456 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 457 | }, 458 | { 459 | "address": "H5uzEytiByuXt964KampmuNCurNDwkVVypkym75J2DQW", 460 | "name": "ETH/USDC", 461 | "deprecated": false, 462 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 463 | }, 464 | { 465 | "address": "7xMDbYTCqQEcK2aM9LbetGtNFJpzKdfXzLL5juaLh4GJ", 466 | "name": "SOL/USDC", 467 | "deprecated": false, 468 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 469 | }, 470 | { 471 | "address": "CDdR97S8y96v3To93aKvi3nCnjUrbuVSuumw8FLvbVeg", 472 | "name": "SRM/USDC", 473 | "deprecated": false, 474 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 475 | }, 476 | { 477 | "address": "7LVJtqSrF6RudMaz5rKGTmR3F3V5TKoDcN6bnk68biYZ", 478 | "name": "SUSHI/USDC", 479 | "deprecated": false, 480 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 481 | }, 482 | { 483 | "address": "13vjJ8pxDMmzen26bQ5UrouX8dkXYPW1p3VLVDjxXrKR", 484 | "name": "SXP/USDC", 485 | "deprecated": false, 486 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 487 | }, 488 | { 489 | "address": "AwvPwwSprfDZ86beBJDNH5vocFvuw4ZbVQ6upJDbSCXZ", 490 | "name": "MSRM/USDC", 491 | "deprecated": false, 492 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 493 | }, 494 | { 495 | "address": "FfDb3QZUdMW2R2aqJQgzeieys4ETb3rPrFFfPSemzq7R", 496 | "name": "FTT/USDC", 497 | "deprecated": false, 498 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 499 | }, 500 | { 501 | "address": "4QL5AQvXdMSCVZmnKXiuMMU83Kq3LCwVfU8CyznqZELG", 502 | "name": "YFI/USDC", 503 | "deprecated": false, 504 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 505 | }, 506 | { 507 | "address": "7JCG9TsCx3AErSV3pvhxiW4AbkKRcJ6ZAveRmJwrgQ16", 508 | "name": "LINK/USDC", 509 | "deprecated": false, 510 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 511 | }, 512 | { 513 | "address": "3otQFkeQ7GNUKT3i2p3aGTQKS2SAw6NLYPE5qxh3PoqZ", 514 | "name": "HGET/USDC", 515 | "deprecated": false, 516 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 517 | }, 518 | { 519 | "address": "2M8EBxFbLANnCoHydypL1jupnRHG782RofnvkatuKyLL", 520 | "name": "CREAM/USDC", 521 | "deprecated": false, 522 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 523 | }, 524 | { 525 | "address": "3UqXdFtNBZsFrFtRGAWGvy9R8H6GJR2hAyGRdYT9BgG3", 526 | "name": "UBXT/USDC", 527 | "deprecated": false, 528 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 529 | }, 530 | { 531 | "address": "9jiasgdYGGh34fAbBQSwkKe1dYSapXbjy2sLsYpetqFp", 532 | "name": "HNT/USDC", 533 | "deprecated": false, 534 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 535 | }, 536 | { 537 | "address": "7oKqJhnz9b8af8Mw47dieTiuxeaHnRYYGBiqCrRpzTRD", 538 | "name": "FRONT/USDC", 539 | "deprecated": false, 540 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 541 | }, 542 | { 543 | "address": "F1rxD8Ns5w4WzVcTRdaJ96LG7YKaA5a25BBmM32yFP4b", 544 | "name": "AKRO/USDC", 545 | "deprecated": false, 546 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 547 | }, 548 | { 549 | "address": "6ToedDwjRCvrcKX7fnHSTA9uABQe1dcLK6YgS5B9M3wo", 550 | "name": "HXRO/USDC", 551 | "deprecated": false, 552 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 553 | }, 554 | { 555 | "address": "FURvCsDUiuUaxZ13pZqQbbfktFGWmQVTHz7tL992LQVZ", 556 | "name": "UNI/USDC", 557 | "deprecated": false, 558 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 559 | }, 560 | { 561 | "address": "EcfDRMrEJ3yW4SgrRyyxTPoKqAZDNSBV8EerigT7BNSS", 562 | "name": "KEEP/USDC", 563 | "deprecated": false, 564 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 565 | }, 566 | { 567 | "address": "2bPsJ6bZ9KDLfJ8QgSN1Eb4mRsbAiaGyHN6cJkoVLpwd", 568 | "name": "MATH/USDC", 569 | "deprecated": false, 570 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 571 | }, 572 | { 573 | "address": "B1GypajMh7S8zJVp6M1xMfu6zGsMgvYrt3cSn9wG7Dd6", 574 | "name": "TOMO/USDC", 575 | "deprecated": false, 576 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 577 | }, 578 | { 579 | "address": "H7c8FcQPJ2E5tJmpWBPSi7xCAbk8immdtUxKFRUyE4Ro", 580 | "name": "TOMO/USDT", 581 | "deprecated": false, 582 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 583 | }, 584 | { 585 | "address": "rPTGvVrNFYzBeTEcYnHiaWGNnkSXsWNNjUgk771LkwJ", 586 | "name": "LUA/USDC", 587 | "deprecated": false, 588 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 589 | }, 590 | { 591 | "address": "7PSeX1AEtBY9KvgegF5rUh452VemMh7oDzFtJgH7sxMG", 592 | "name": "LUA/USDT", 593 | "deprecated": false, 594 | "programId": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o" 595 | } 596 | ] 597 | -------------------------------------------------------------------------------- /src/queue.ts: -------------------------------------------------------------------------------- 1 | import { bits, blob, struct, u32, u8 } from 'buffer-layout'; 2 | import { 3 | accountFlagsLayout, 4 | publicKeyLayout, 5 | u128, 6 | u64, 7 | zeros, 8 | } from './layout'; 9 | import BN from 'bn.js'; 10 | import { PublicKey } from '@solana/web3.js'; 11 | 12 | const REQUEST_QUEUE_HEADER = struct([ 13 | blob(5), 14 | 15 | accountFlagsLayout('accountFlags'), 16 | u32('head'), 17 | zeros(4), 18 | u32('count'), 19 | zeros(4), 20 | u32('nextSeqNum'), 21 | zeros(4), 22 | ]); 23 | 24 | const REQUEST_FLAGS = bits(u8(), false, 'requestFlags'); 25 | REQUEST_FLAGS.addBoolean('newOrder'); 26 | REQUEST_FLAGS.addBoolean('cancelOrder'); 27 | REQUEST_FLAGS.addBoolean('bid'); 28 | REQUEST_FLAGS.addBoolean('postOnly'); 29 | REQUEST_FLAGS.addBoolean('ioc'); 30 | 31 | const REQUEST = struct([ 32 | REQUEST_FLAGS, 33 | u8('openOrdersSlot'), 34 | u8('feeTier'), 35 | blob(5), 36 | u64('maxBaseSizeOrCancelId'), 37 | u64('nativeQuoteQuantityLocked'), 38 | u128('orderId'), 39 | publicKeyLayout('openOrders'), 40 | u64('clientOrderId'), 41 | ]); 42 | 43 | const EVENT_QUEUE_HEADER = struct([ 44 | blob(5), 45 | 46 | accountFlagsLayout('accountFlags'), 47 | u32('head'), 48 | zeros(4), 49 | u32('count'), 50 | zeros(4), 51 | u32('seqNum'), 52 | zeros(4), 53 | ]); 54 | 55 | const EVENT_FLAGS = bits(u8(), false, 'eventFlags'); 56 | EVENT_FLAGS.addBoolean('fill'); 57 | EVENT_FLAGS.addBoolean('out'); 58 | EVENT_FLAGS.addBoolean('bid'); 59 | EVENT_FLAGS.addBoolean('maker'); 60 | 61 | const EVENT = struct([ 62 | EVENT_FLAGS, 63 | u8('openOrdersSlot'), 64 | u8('feeTier'), 65 | blob(5), 66 | u64('nativeQuantityReleased'), // Amount the user received 67 | u64('nativeQuantityPaid'), // Amount the user paid 68 | u64('nativeFeeOrRebate'), 69 | u128('orderId'), 70 | publicKeyLayout('openOrders'), 71 | u64('clientOrderId'), 72 | ]); 73 | 74 | export interface Event { 75 | eventFlags: { fill: boolean; out: boolean; bid: boolean; maker: boolean }; 76 | 77 | orderId: BN; 78 | openOrders: PublicKey; 79 | openOrdersSlot: number; 80 | feeTier: number; 81 | 82 | nativeQuantityReleased: BN; 83 | nativeQuantityPaid: BN; 84 | nativeFeeOrRebate: BN; 85 | } 86 | 87 | function decodeQueue( 88 | headerLayout, 89 | nodeLayout, 90 | buffer: Buffer, 91 | history?: number, 92 | ) { 93 | const header = headerLayout.decode(buffer); 94 | const allocLen = Math.floor( 95 | (buffer.length - headerLayout.span) / nodeLayout.span, 96 | ); 97 | const nodes: any[] = []; 98 | if (history) { 99 | for (let i = 0; i < Math.min(history, allocLen); ++i) { 100 | const nodeIndex = 101 | (header.head + header.count + allocLen - 1 - i) % allocLen; 102 | nodes.push( 103 | nodeLayout.decode( 104 | buffer, 105 | headerLayout.span + nodeIndex * nodeLayout.span, 106 | ), 107 | ); 108 | } 109 | } else { 110 | for (let i = 0; i < header.count; ++i) { 111 | const nodeIndex = (header.head + i) % allocLen; 112 | nodes.push( 113 | nodeLayout.decode( 114 | buffer, 115 | headerLayout.span + nodeIndex * nodeLayout.span, 116 | ), 117 | ); 118 | } 119 | } 120 | return { header, nodes }; 121 | } 122 | 123 | export function decodeRequestQueue(buffer: Buffer, history?: number) { 124 | const { header, nodes } = decodeQueue( 125 | REQUEST_QUEUE_HEADER, 126 | REQUEST, 127 | buffer, 128 | history, 129 | ); 130 | if (!header.accountFlags.initialized || !header.accountFlags.requestQueue) { 131 | throw new Error('Invalid requests queue'); 132 | } 133 | return nodes; 134 | } 135 | 136 | export function decodeEventQueue(buffer: Buffer, history?: number): Event[] { 137 | const { header, nodes } = decodeQueue( 138 | EVENT_QUEUE_HEADER, 139 | EVENT, 140 | buffer, 141 | history, 142 | ); 143 | if (!header.accountFlags.initialized || !header.accountFlags.eventQueue) { 144 | throw new Error('Invalid events queue'); 145 | } 146 | return nodes; 147 | } 148 | 149 | export const REQUEST_QUEUE_LAYOUT = { 150 | HEADER: REQUEST_QUEUE_HEADER, 151 | NODE: REQUEST, 152 | }; 153 | 154 | export const EVENT_QUEUE_LAYOUT = { 155 | HEADER: EVENT_QUEUE_HEADER, 156 | NODE: EVENT, 157 | }; 158 | -------------------------------------------------------------------------------- /src/slab.test.js: -------------------------------------------------------------------------------- 1 | import { Slab } from './slab'; 2 | import BN from 'bn.js'; 3 | 4 | const SLAB_BUFFER = Buffer.from( 5 | '0900000000000000020000000000000008000000000000000400000000000000010000001e00000000000040952fe4da5c1f3c860200000004000000030000000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d7b0000000000000000000000000000000200000002000000000000a0ca17726dae0f1e43010000001111111111111111111111111111111111111111111111111111111111111111410100000000000000000000000000000200000001000000d20a3f4eeee073c3f60fe98e010000000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d7b000000000000000000000000000000020000000300000000000040952fe4da5c1f3c8602000000131313131313131313131313131313131313131313131313131313131313131340e20100000000000000000000000000010000001f0000000500000000000000000000000000000005000000060000000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d7b0000000000000000000000000000000200000004000000040000000000000000000000000000001717171717171717171717171717171717171717171717171717171717171717020000000000000000000000000000000100000020000000000000a0ca17726dae0f1e430100000001000000020000000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d7b000000000000000000000000000000040000000000000004000000000000000000000000000000171717171717171717171717171717171717171717171717171717171717171702000000000000000000000000000000030000000700000005000000000000000000000000000000171717171717171717171717171717171717171717171717171717171717171702000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 6 | 'hex', 7 | ); 8 | 9 | describe('slab', () => { 10 | let slab; 11 | 12 | it('parses', () => { 13 | slab = Slab.decode(SLAB_BUFFER); 14 | expect(slab).toBeTruthy(); 15 | expect(slab.header.bumpIndex).toBe(9); 16 | expect(slab.nodes).toHaveLength(9); 17 | }); 18 | 19 | it('finds nodes', () => { 20 | expect(slab.get(new BN('123456789012345678901234567890')).ownerSlot).toBe( 21 | 1, 22 | ); 23 | expect(slab.get(new BN('100000000000000000000000000000')).ownerSlot).toBe( 24 | 2, 25 | ); 26 | expect(slab.get(new BN('200000000000000000000000000000')).ownerSlot).toBe( 27 | 3, 28 | ); 29 | expect(slab.get(4).ownerSlot).toBe(4); 30 | }); 31 | 32 | it('does not find nonexistant nodes', () => { 33 | expect(slab.get(0)).toBeNull(); 34 | expect(slab.get(3)).toBeNull(); 35 | expect(slab.get(5)).toBeNull(); 36 | expect(slab.get(6)).toBeNull(); 37 | expect(slab.get(new BN('200000000000000000000000000001'))).toBeNull(); 38 | expect(slab.get(new BN('100000000000000000000000000001'))).toBeNull(); 39 | expect(slab.get(new BN('123456789012345678901234567889'))).toBeNull(); 40 | expect(slab.get(new BN('123456789012345678901234567891'))).toBeNull(); 41 | expect(slab.get(new BN('99999999999999999999999999999'))).toBeNull(); 42 | }); 43 | 44 | it('iterates', () => { 45 | expect(Array.from(slab)).toHaveLength(4); 46 | }); 47 | 48 | it('iterates in order', () => { 49 | let previous = null; 50 | for (const item of slab) { 51 | if (previous) { 52 | expect(item.key.gt(previous.key)).toBeTruthy(); 53 | } 54 | previous = item; 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/slab.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { blob, offset, seq, struct, u32, u8, union } from 'buffer-layout'; 3 | import { publicKeyLayout, setLayoutDecoder, u128, u64, zeros } from './layout'; 4 | import { PublicKey } from '@solana/web3.js'; 5 | 6 | const SLAB_HEADER_LAYOUT = struct( 7 | [ 8 | // Number of modified slab nodes 9 | u32('bumpIndex'), 10 | zeros(4), // Consider slabs with more than 2^32 nodes to be invalid 11 | 12 | // Linked list of unused nodes 13 | u32('freeListLen'), 14 | zeros(4), 15 | u32('freeListHead'), 16 | 17 | u32('root'), 18 | 19 | u32('leafCount'), 20 | zeros(4), 21 | ], 22 | 'header', 23 | ); 24 | 25 | const SLAB_NODE_LAYOUT = union(u32('tag'), blob(68), 'node'); 26 | SLAB_NODE_LAYOUT.addVariant(0, struct([]), 'uninitialized'); 27 | SLAB_NODE_LAYOUT.addVariant( 28 | 1, 29 | struct([ 30 | // Only the first prefixLen high-order bits of key are meaningful 31 | u32('prefixLen'), 32 | u128('key'), 33 | seq(u32(), 2, 'children'), 34 | ]), 35 | 'innerNode', 36 | ); 37 | SLAB_NODE_LAYOUT.addVariant( 38 | 2, 39 | struct([ 40 | u8('ownerSlot'), // Index into OPEN_ORDERS_LAYOUT.orders 41 | u8('feeTier'), 42 | blob(2), 43 | u128('key'), // (price, seqNum) 44 | publicKeyLayout('owner'), // Open orders account 45 | u64('quantity'), // In units of lot size 46 | u64('clientOrderId'), 47 | ]), 48 | 'leafNode', 49 | ); 50 | SLAB_NODE_LAYOUT.addVariant(3, struct([u32('next')]), 'freeNode'); 51 | SLAB_NODE_LAYOUT.addVariant(4, struct([]), 'lastFreeNode'); 52 | 53 | export const SLAB_LAYOUT = struct([ 54 | SLAB_HEADER_LAYOUT, 55 | seq( 56 | SLAB_NODE_LAYOUT, 57 | offset( 58 | SLAB_HEADER_LAYOUT.layoutFor('bumpIndex'), 59 | SLAB_HEADER_LAYOUT.offsetOf('bumpIndex') - SLAB_HEADER_LAYOUT.span, 60 | ), 61 | 'nodes', 62 | ), 63 | ]); 64 | 65 | export class Slab { 66 | private header: any; 67 | private nodes: any; 68 | 69 | constructor(header, nodes) { 70 | this.header = header; 71 | this.nodes = nodes; 72 | } 73 | 74 | static decode(buffer: Buffer) { 75 | return SLAB_LAYOUT.decode(buffer); 76 | } 77 | 78 | get(searchKey: BN | number) { 79 | if (this.header.leafCount === 0) { 80 | return null; 81 | } 82 | if (!(searchKey instanceof BN)) { 83 | searchKey = new BN(searchKey); 84 | } 85 | let index = this.header.root; 86 | while (true) { 87 | const { leafNode, innerNode } = this.nodes[index]; 88 | if (leafNode) { 89 | if (leafNode.key.eq(searchKey)) { 90 | return leafNode; 91 | } 92 | return null; 93 | } else if (innerNode) { 94 | if ( 95 | !innerNode.key 96 | .xor(searchKey) 97 | .iushrn(128 - innerNode.prefixLen) 98 | .isZero() 99 | ) { 100 | return null; 101 | } 102 | index = 103 | innerNode.children[ 104 | searchKey.testn(128 - innerNode.prefixLen - 1) ? 1 : 0 105 | ]; 106 | } else { 107 | throw new Error('Invalid slab'); 108 | } 109 | } 110 | } 111 | 112 | [Symbol.iterator]() { 113 | return this.items(false); 114 | } 115 | 116 | *items( 117 | descending = false, 118 | ): Generator<{ 119 | ownerSlot: number; 120 | key: BN; 121 | owner: PublicKey; 122 | quantity: BN; 123 | feeTier: number; 124 | clientOrderId: BN; 125 | }> { 126 | if (this.header.leafCount === 0) { 127 | return; 128 | } 129 | const stack = [this.header.root]; 130 | while (stack.length > 0) { 131 | const index = stack.pop(); 132 | const { leafNode, innerNode } = this.nodes[index]; 133 | if (leafNode) { 134 | yield leafNode; 135 | } else if (innerNode) { 136 | if (descending) { 137 | stack.push(innerNode.children[0], innerNode.children[1]); 138 | } else { 139 | stack.push(innerNode.children[1], innerNode.children[0]); 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | setLayoutDecoder(SLAB_LAYOUT, ({ header, nodes }) => new Slab(header, nodes)); 147 | -------------------------------------------------------------------------------- /src/token-instructions.js: -------------------------------------------------------------------------------- 1 | import * as BufferLayout from 'buffer-layout'; 2 | import { 3 | PublicKey, 4 | SYSVAR_RENT_PUBKEY, 5 | TransactionInstruction, 6 | } from '@solana/web3.js'; 7 | import { publicKeyLayout } from './layout'; 8 | 9 | // NOTE: Update these if the position of arguments for the initializeAccount instruction changes 10 | export const INITIALIZE_ACCOUNT_ACCOUNT_INDEX = 0; 11 | export const INITIALIZE_ACCOUNT_MINT_INDEX = 1; 12 | export const INITIALIZE_ACCOUNT_OWNER_INDEX = 2; 13 | 14 | // NOTE: Update these if the position of arguments for the transfer instruction changes 15 | export const TRANSFER_SOURCE_INDEX = 0; 16 | export const TRANSFER_DESTINATION_INDEX = 1; 17 | export const TRANSFER_OWNER_INDEX = 2; 18 | 19 | // NOTE: Update these if the position of arguments for the closeAccount instruction changes 20 | export const CLOSE_ACCOUNT_SOURCE_INDEX = 0; 21 | export const CLOSE_ACCOUNT_DESTINATION_INDEX = 1; 22 | export const CLOSE_ACCOUNT_OWNER_INDEX = 2; 23 | 24 | export const TOKEN_PROGRAM_ID = new PublicKey( 25 | 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 26 | ); 27 | 28 | export const WRAPPED_SOL_MINT = new PublicKey( 29 | 'So11111111111111111111111111111111111111112', 30 | ); 31 | 32 | export const MSRM_MINT = new PublicKey( 33 | 'MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L', 34 | ); 35 | export const MSRM_DECIMALS = 0; 36 | 37 | export const SRM_MINT = new PublicKey( 38 | 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt', 39 | ); 40 | export const SRM_DECIMALS = 6; 41 | 42 | const LAYOUT = BufferLayout.union(BufferLayout.u8('instruction')); 43 | LAYOUT.addVariant( 44 | 0, 45 | BufferLayout.struct([ 46 | BufferLayout.u8('decimals'), 47 | publicKeyLayout('mintAuthority'), 48 | BufferLayout.u8('freezeAuthorityOption'), 49 | publicKeyLayout('freezeAuthority'), 50 | ]), 51 | 'initializeMint', 52 | ); 53 | LAYOUT.addVariant(1, BufferLayout.struct([]), 'initializeAccount'); 54 | LAYOUT.addVariant( 55 | 3, 56 | BufferLayout.struct([BufferLayout.nu64('amount')]), 57 | 'transfer', 58 | ); 59 | LAYOUT.addVariant( 60 | 4, 61 | BufferLayout.struct([BufferLayout.nu64('amount')]), 62 | 'approve', 63 | ); 64 | LAYOUT.addVariant(5, BufferLayout.struct([]), 'revoke'); 65 | LAYOUT.addVariant( 66 | 6, 67 | BufferLayout.struct([ 68 | BufferLayout.u8('authorityType'), 69 | BufferLayout.u8('newAuthorityOption'), 70 | publicKeyLayout('newAuthority'), 71 | ]), 72 | 'setAuthority', 73 | ); 74 | LAYOUT.addVariant( 75 | 7, 76 | BufferLayout.struct([BufferLayout.nu64('amount')]), 77 | 'mintTo', 78 | ); 79 | LAYOUT.addVariant( 80 | 8, 81 | BufferLayout.struct([BufferLayout.nu64('amount')]), 82 | 'burn', 83 | ); 84 | LAYOUT.addVariant(9, BufferLayout.struct([]), 'closeAccount'); 85 | 86 | const instructionMaxSpan = Math.max( 87 | ...Object.values(LAYOUT.registry).map((r) => r.span), 88 | ); 89 | 90 | function encodeTokenInstructionData(instruction) { 91 | const b = Buffer.alloc(instructionMaxSpan); 92 | const span = LAYOUT.encode(instruction, b); 93 | return b.slice(0, span); 94 | } 95 | 96 | export function decodeTokenInstructionData(instruction) { 97 | return LAYOUT.decode(instruction); 98 | } 99 | 100 | export function initializeMint({ 101 | mint, 102 | decimals, 103 | mintAuthority, 104 | freezeAuthority = null, 105 | }) { 106 | const keys = [ 107 | { pubkey: mint, isSigner: false, isWritable: true }, 108 | { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, 109 | ]; 110 | return new TransactionInstruction({ 111 | keys, 112 | data: encodeTokenInstructionData({ 113 | initializeMint: { 114 | decimals, 115 | mintAuthority, 116 | freezeAuthorityOption: !!freezeAuthority, 117 | freezeAuthority: freezeAuthority || new PublicKey(0), 118 | }, 119 | }), 120 | programId: TOKEN_PROGRAM_ID, 121 | }); 122 | } 123 | 124 | export function initializeAccount({ account, mint, owner }) { 125 | const keys = [ 126 | { pubkey: account, isSigner: false, isWritable: true }, 127 | { pubkey: mint, isSigner: false, isWritable: false }, 128 | { pubkey: owner, isSigner: false, isWritable: false }, 129 | { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, 130 | ]; 131 | return new TransactionInstruction({ 132 | keys, 133 | data: encodeTokenInstructionData({ 134 | initializeAccount: {}, 135 | }), 136 | programId: TOKEN_PROGRAM_ID, 137 | }); 138 | } 139 | 140 | export function transfer({ source, destination, amount, owner }) { 141 | const keys = [ 142 | { pubkey: source, isSigner: false, isWritable: true }, 143 | { pubkey: destination, isSigner: false, isWritable: true }, 144 | { pubkey: owner, isSigner: true, isWritable: false }, 145 | ]; 146 | return new TransactionInstruction({ 147 | keys, 148 | data: encodeTokenInstructionData({ 149 | transfer: { amount }, 150 | }), 151 | programId: TOKEN_PROGRAM_ID, 152 | }); 153 | } 154 | 155 | export function approve({ source, delegate, amount, owner }) { 156 | const keys = [ 157 | { pubkey: source, isSigner: false, isWritable: true }, 158 | { pubkey: delegate, isSigner: false, isWritable: false }, 159 | { pubkey: owner, isSigner: true, isWritable: false }, 160 | ]; 161 | return new TransactionInstruction({ 162 | keys, 163 | data: encodeTokenInstructionData({ 164 | approve: { amount }, 165 | }), 166 | programId: TOKEN_PROGRAM_ID, 167 | }); 168 | } 169 | 170 | export function revoke({ source, owner }) { 171 | const keys = [ 172 | { pubkey: source, isSigner: false, isWritable: true }, 173 | { pubkey: owner, isSigner: true, isWritable: false }, 174 | ]; 175 | return new TransactionInstruction({ 176 | keys, 177 | data: encodeTokenInstructionData({ 178 | revoke: {}, 179 | }), 180 | programId: TOKEN_PROGRAM_ID, 181 | }); 182 | } 183 | 184 | export function setAuthority({ 185 | target, 186 | currentAuthority, 187 | newAuthority, 188 | authorityType, 189 | }) { 190 | const keys = [ 191 | { pubkey: target, isSigner: false, isWritable: true }, 192 | { pubkey: currentAuthority, isSigner: true, isWritable: false }, 193 | ]; 194 | return new TransactionInstruction({ 195 | keys, 196 | data: encodeTokenInstructionData({ 197 | setAuthority: { 198 | authorityType, 199 | newAuthorityOption: !!newAuthority, 200 | newAuthority, 201 | }, 202 | }), 203 | programId: TOKEN_PROGRAM_ID, 204 | }); 205 | } 206 | 207 | export function mintTo({ mint, destination, amount, mintAuthority }) { 208 | const keys = [ 209 | { pubkey: mint, isSigner: false, isWritable: true }, 210 | { pubkey: destination, isSigner: false, isWritable: true }, 211 | { pubkey: mintAuthority, isSigner: true, isWritable: false }, 212 | ]; 213 | return new TransactionInstruction({ 214 | keys, 215 | data: encodeTokenInstructionData({ 216 | mintTo: { amount }, 217 | }), 218 | programId: TOKEN_PROGRAM_ID, 219 | }); 220 | } 221 | 222 | export function closeAccount({ source, destination, owner }) { 223 | const keys = [ 224 | { pubkey: source, isSigner: false, isWritable: true }, 225 | { pubkey: destination, isSigner: false, isWritable: true }, 226 | { pubkey: owner, isSigner: true, isWritable: false }, 227 | ]; 228 | return new TransactionInstruction({ 229 | keys, 230 | data: encodeTokenInstructionData({ 231 | closeAccount: {}, 232 | }), 233 | programId: TOKEN_PROGRAM_ID, 234 | }); 235 | } 236 | -------------------------------------------------------------------------------- /src/token-mints.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "address": "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E", 4 | "name": "BTC" 5 | }, 6 | { 7 | "address": "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", 8 | "name": "ETH" 9 | }, 10 | { 11 | "address": "AGFEad2et2ZJif9jaGpdMixQqvW5i81aBdvKe7PHNfz3", 12 | "name": "FTT" 13 | }, 14 | { 15 | "address": "3JSf5tPeuscJGtaCp5giEiDhv51gQ4v3zWg8DGgyLfAB", 16 | "name": "YFI" 17 | }, 18 | { 19 | "address": "CWE8jPTUYhdCTZYWPTe1o5DFqfdjzWKc9WKz6rSjQUdG", 20 | "name": "LINK" 21 | }, 22 | { 23 | "address": "Ga2AXHpfAF6mv2ekZwcsJFqu7wB4NV331qNH7fW9Nst8", 24 | "name": "XRP" 25 | }, 26 | { 27 | "address": "BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4", 28 | "name": "USDT" 29 | }, 30 | { 31 | "address": "BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW", 32 | "name": "WUSDC" 33 | }, 34 | { 35 | "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 36 | "name": "USDC" 37 | }, 38 | { 39 | "address": "MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L", 40 | "name": "MSRM" 41 | }, 42 | { 43 | "address": "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt", 44 | "name": "SRM" 45 | }, 46 | { 47 | "address": "AR1Mtgh7zAtxuxGd2XPovXPVjcSdY3i4rQYisNadjfKy", 48 | "name": "SUSHI" 49 | }, 50 | { 51 | "address": "SF3oTvfWzEP3DTwGSvUXRrGTvr75pdZNnBLAH9bzMuX", 52 | "name": "SXP" 53 | }, 54 | { 55 | "address": "CsZ5LZkDS7h9TDKjrbL7VAwQZ9nsRu8vJLhRYfmGaN8K", 56 | "name": "ALEPH" 57 | }, 58 | { 59 | "address": "BtZQfWqDGbk9Wf2rXEiWyQBdBY1etnUUn6zEphvVS7yN", 60 | "name": "HGET" 61 | }, 62 | { 63 | "address": "5Fu5UUgbjpUvdBveb3a1JTNirL8rXtiYeSMWvKjtUNQv", 64 | "name": "CREAM" 65 | }, 66 | { 67 | "address": "873KLxCbz7s9Kc4ZzgYRtNmhfkQrhfyWGZJBmyCbC3ei", 68 | "name": "UBXT" 69 | }, 70 | { 71 | "address": "HqB7uswoVg4suaQiDP3wjxob1G5WdZ144zhdStwMCq7e", 72 | "name": "HNT" 73 | }, 74 | { 75 | "address": "9S4t2NEAiJVMvPdRYKVrfJpBafPBLtvbvyS3DecojQHw", 76 | "name": "FRONT" 77 | }, 78 | { 79 | "address": "6WNVCuxCGJzNjmMZoKyhZJwvJ5tYpsLyAtagzYASqBoF", 80 | "name": "AKRO" 81 | }, 82 | { 83 | "address": "DJafV9qemGp7mLMEn5wrfqaFwxsbLgUsGVS16zKRk9kc", 84 | "name": "HXRO" 85 | }, 86 | { 87 | "address": "DEhAasscXF4kEGxFgJ3bq4PpVGp5wyUxMRvn6TzGVHaw", 88 | "name": "UNI" 89 | }, 90 | { 91 | "address": "GUohe4DJUA5FKPWo3joiPgsB7yzer7LpDmt1Vhzy3Zht", 92 | "name": "KEEP" 93 | }, 94 | { 95 | "address": "GeDS162t9yGJuLEHPWXXGrb1zwkzinCgRwnT8vHYjKza", 96 | "name": "MATH" 97 | }, 98 | { 99 | "address": "So11111111111111111111111111111111111111112", 100 | "name": "SOL" 101 | }, 102 | { 103 | "address": "GXMvfY2jpQctDqZ9RoU3oWPhufKiCcFEfchvYumtX7jd", 104 | "name": "TOMO" 105 | }, 106 | { 107 | "address": "EqWCKXfs3x47uVosDpTRgFniThL9Y8iCztJaapxbEaVX", 108 | "name": "LUA" 109 | } 110 | ] 111 | -------------------------------------------------------------------------------- /src/tokens_and_markets.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import Markets from "./markets.json"; 3 | import TokenMints from "./token-mints.json"; 4 | 5 | export function getLayoutVersion(programId: PublicKey) { 6 | if ( 7 | programId.equals( 8 | new PublicKey('4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn'), 9 | ) || 10 | programId.equals( 11 | new PublicKey('BJ3jrUzddfuSrZHXSCxMUUQsjKEyLmuuyZebkcaFp2fg'), 12 | ) 13 | ) { 14 | return 1; 15 | } 16 | return 2; 17 | } 18 | 19 | export const TOKEN_MINTS: Array<{ address: PublicKey; name: string }> = TokenMints.map(mint => { 20 | return { 21 | address: new PublicKey(mint.address), 22 | name: mint.name 23 | } 24 | }) 25 | 26 | export const MARKETS: Array<{ 27 | address: PublicKey; 28 | name: string; 29 | programId: PublicKey; 30 | deprecated: boolean; 31 | }> = Markets.map(market => { 32 | return { 33 | address: new PublicKey(market.address), 34 | name: market.name, 35 | programId: new PublicKey(market.programId), 36 | deprecated: market.deprecated, 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "noImplicitAny": false, 10 | "resolveJsonModule": true, 11 | "sourceMap": true 12 | }, 13 | "include": ["./src/**/*"], 14 | "exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"] 15 | } 16 | --------------------------------------------------------------------------------