From 02f41fd3b1a60956e5403dedad332bf4cb96f6f3 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Mon, 10 Aug 2020 06:06:31 -0700 Subject: [PATCH] Add more instructions --- package.json | 8 +- src/index.js | 3 +- src/instructions.js | 173 ++++++++++++++++++- src/instructions.test.js | 10 +- src/layout.js | 35 +++- src/{market-state.js => market.js} | 16 +- src/{market-state.test.js => market.test.js} | 2 +- src/slab.js | 4 +- src/slab.test.js | 2 +- 9 files changed, 222 insertions(+), 31 deletions(-) rename src/{market-state.js => market.js} (92%) rename src/{market-state.test.js => market.test.js} (96%) diff --git a/package.json b/package.json index 9fcf18f..cce21a3 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,9 @@ "prepare": "run-s build", "test": "run-s test:unit test:lint test:build", "test:build": "run-s build", - "test:lint": "eslint .", - "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", - "test:watch": "react-scripts test --env=jsdom", - "predeploy": "cd example && yarn install && yarn run build", - "deploy": "gh-pages -d example/build" + "test:lint": "eslint src", + "test:unit": "cross-env CI=1 react-scripts test", + "test:watch": "react-scripts test" }, "devDependencies": { "@babel/cli": "^7.10.5", diff --git a/src/index.js b/src/index.js index 3fb05ae..ec71a9a 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,2 @@ -export { MarketState, Orderbook } from './market-state'; +export { Market, Orderbook } from './market'; +export { DexInstructions } from './instructions'; diff --git a/src/instructions.js b/src/instructions.js index 4611f2f..44aceca 100644 --- a/src/instructions.js +++ b/src/instructions.js @@ -1,9 +1,17 @@ -import { blob, Layout, struct, u16, u32, u8, union } from 'buffer-layout'; -import { u64, VersionedLayout } from './layout'; -import { PublicKey } from '@solana/web3.js'; +import { blob, struct, u16, u32, u8, union } from 'buffer-layout'; +import { + orderTypeLayout, + publicKeyLayout, + sideLayout, + u128, + u64, + VersionedLayout, + zeros, +} from './layout'; +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; export const DEX_PROGRAM_ID = new PublicKey( - '6iM2JjaPVViB2u82aVjf3ZfHDNHQ4G635XgnvgN6CNhY', + '9o1FisE366msTQcEvXapyMorTLmvezrxSD8DnM5e5XKw', ); export const INSTRUCTION_LAYOUT = new VersionedLayout( @@ -24,12 +32,12 @@ INSTRUCTION_LAYOUT.inner.addVariant( INSTRUCTION_LAYOUT.inner.addVariant( 1, struct([ - u8('side'), // buy = 0, sell = 1 - blob(3), + sideLayout('side'), + zeros(3), u64('limitPrice'), u64('maxQuantity'), - u8('orderType'), // limit = 0, ioc = 1, postOnly = 2 - blob(3), + orderTypeLayout('orderType'), + zeros(3), ]), 'newOrder', ); @@ -43,9 +51,156 @@ INSTRUCTION_LAYOUT.inner.addVariant( struct([u16('limit'), blob(2)]), 'consumeEvents', ); -INSTRUCTION_LAYOUT.inner.addVariant(4, struct([]), 'cancelOrder'); +INSTRUCTION_LAYOUT.inner.addVariant( + 4, + struct([ + sideLayout('side'), + zeros(3), + u128('orderId'), + publicKeyLayout('owner'), + u8('ownerSlot'), + ]), + 'cancelOrder', +); export function encodeInstruction(instruction) { const b = Buffer.alloc(100); return b.slice(0, INSTRUCTION_LAYOUT.encode(instruction, b)); } + +export class DexInstructions { + static initializeMarket({ + market, + requestQueue, + eventQueue, + bids, + asks, + baseVault, + quoteVault, + baseMint, + quoteMint, + baseLotSize, + quoteLotSize, + feeRateBps, + vaultSignerNonce, + quoteDustThreshold, + }) { + return new TransactionInstruction({ + keys: [ + { pubkey: market, isSigner: false, isWritable: true }, + { pubkey: requestQueue, isSigner: false, isWritable: true }, + { pubkey: eventQueue, isSigner: false, isWritable: true }, + { pubkey: bids, isSigner: false, isWritable: true }, + { pubkey: asks, isSigner: false, isWritable: true }, + { pubkey: baseVault, isSigner: false, isWritable: true }, + { pubkey: quoteVault, isSigner: false, isWritable: true }, + { pubkey: baseMint, isSigner: false, isWritable: false }, + { pubkey: quoteMint, isSigner: false, isWritable: false }, + ], + programId: DEX_PROGRAM_ID, + data: encodeInstruction({ + initializeMarket: { + baseLotSize, + quoteLotSize, + feeRateBps, + vaultSignerNonce, + quoteDustThreshold, + }, + }), + }); + } + + static newOrder({ + market, + openOrders, + payer, + owner, + requestQueue, + baseVault, + quoteVault, + side, + limitPrice, + maxQuantity, + orderType, + }) { + return new TransactionInstruction({ + keys: [ + { pubkey: market, isSigner: false, isWritable: false }, + { pubkey: openOrders, isSigner: false, isWritable: true }, + { pubkey: payer, isSigner: false, isWritable: true }, + { pubkey: owner, isSigner: true, isWritable: false }, + { pubkey: requestQueue, isSigner: false, isWritable: true }, + { pubkey: baseVault, isSigner: false, isWritable: false }, + { pubkey: quoteVault, isSigner: false, isWritable: false }, + ], + programId: DEX_PROGRAM_ID, + data: encodeInstruction({ + newOrder: { side, limitPrice, maxQuantity, orderType }, + }), + }); + } + + static matchOrders({ + market, + requestQueue, + eventQueue, + bids, + asks, + baseVault, + quoteVault, + limit, + }) { + return new TransactionInstruction({ + keys: [ + { pubkey: market, isSigner: false, isWritable: true }, + { pubkey: requestQueue, isSigner: false, isWritable: true }, + { pubkey: eventQueue, isSigner: false, isWritable: true }, + { pubkey: bids, isSigner: false, isWritable: true }, + { pubkey: asks, isSigner: false, isWritable: true }, + { pubkey: baseVault, isSigner: false, isWritable: true }, + { pubkey: quoteVault, isSigner: false, isWritable: true }, + ], + programId: DEX_PROGRAM_ID, + data: encodeInstruction({ matchOrders: { limit } }), + }); + } + + static consumeEvents({ market, eventQueue, openOrdersAccounts, limit }) { + return new TransactionInstruction({ + keys: [ + { pubkey: market, isSigner: false, isWritable: true }, + { pubkey: eventQueue, isSigner: false, isWritable: true }, + ...openOrdersAccounts.map((account) => ({ + pubkey: account, + isSigner: false, + isWritable: true, + })), + ], + programId: DEX_PROGRAM_ID, + data: encodeInstruction({ consumeEvents: { limit } }), + }); + } + + static cancelOrder({ + market, + openOrders, + owner, + requestQueue, + side, + orderId, + ownerSlot, + }) { + return new TransactionInstruction({ + keys: [ + { pubkey: market, isSigner: false, isWritable: false }, + { pubkey: openOrders, isSigner: false, isWritable: true }, + { pubkey: owner, isSigner: true, isWritable: false }, + { pubkey: requestQueue, isSigner: false, isWritable: true }, + ], + programId: DEX_PROGRAM_ID, + data: encodeInstruction({ + cancelOrder: { side, orderId, owner, ownerSlot }, + }), + }); + } +} diff --git a/src/instructions.test.js b/src/instructions.test.js index 96867b7..c62e2fe 100644 --- a/src/instructions.test.js +++ b/src/instructions.test.js @@ -1,9 +1,9 @@ -import { encodeInstruction, INSTRUCTION_LAYOUT } from './instructions'; +import { encodeInstruction } from './instructions'; import BN from 'bn.js'; describe('instruction', () => { it('encodes initialize market', () => { - let b = encodeInstruction({ + const b = encodeInstruction({ initializeMarket: { baseLotSize: new BN(10), quoteLotSize: new BN(100000), @@ -18,12 +18,12 @@ describe('instruction', () => { }); it('encodes new order', () => { - let b = encodeInstruction({ + const b = encodeInstruction({ newOrder: { - side: 1, // buy + side: 'sell', limitPrice: new BN(10), maxQuantity: new BN(5), - orderType: 2, // postOnly + orderType: 'postOnly', }, }); expect(b.toString('hex')).toEqual( diff --git a/src/layout.js b/src/layout.js index f41dfb7..fa18d59 100644 --- a/src/layout.js +++ b/src/layout.js @@ -1,4 +1,4 @@ -import { bits, Blob, Layout, u32 } from 'buffer-layout'; +import { bits, Blob, Layout, u32, UInt } from 'buffer-layout'; import { PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; @@ -105,6 +105,39 @@ export class VersionedLayout extends Layout { } } +class EnumLayout extends UInt { + constructor(values, span, property) { + super(span, property); + this.values = values; + } + + encode(src, b, offset) { + if (this.values[src] !== undefined) { + return super.encode(this.values[src], b, offset); + } + throw new Error('Invalid ' + this.property); + } + + decode(b, offset) { + const decodedValue = super.decode(b, offset); + const entry = Object.entries(this.values).find( + ([key, value]) => value === decodedValue, + ); + if (entry) { + return entry[0]; + } + throw new Error('Invalid ' + this.property); + } +} + +export function sideLayout(property) { + return new EnumLayout({ buy: 0, sell: 1 }, 1, property); +} + +export function orderTypeLayout(property) { + return new EnumLayout({ limit: 0, ioc: 1, postOnly: 2 }, 1, property); +} + export function setLayoutDecoder(layout, decoder) { const originalDecode = layout.decode; layout.decode = function decode(b, offset = 0) { diff --git a/src/market-state.js b/src/market.js similarity index 92% rename from src/market-state.js rename to src/market.js index f81f27a..3d78d5f 100644 --- a/src/market-state.js +++ b/src/market.js @@ -8,6 +8,7 @@ import { } from './layout'; import { SLAB_LAYOUT } from './slab'; import { DEX_PROGRAM_ID } from './instructions'; +import BN from 'bn.js'; const ACCOUNT_FLAGS_LAYOUT = new WideBits(); ACCOUNT_FLAGS_LAYOUT.addBoolean('initialized'); @@ -52,7 +53,7 @@ export const MARKET_STATE_LAYOUT = struct([ u64('quoteLotSize'), ]); -export class MarketState { +export class Market { constructor(decoded) { if (!decoded.accountFlags.initialized || !decoded.accountFlags.market) { throw new Error('Invalid market state'); @@ -74,6 +75,7 @@ export class MarketState { throw new Error('Address not owned by program'); } return MARKET_STATE_LAYOUT.decode(data); + // TODO: also load mint account info } async loadBids(connection) { @@ -95,14 +97,14 @@ export class MarketState { ); } - baseBnToNumber(size) { + baseSizeBnToNumber(size) { return divideBnToNumber( size.mul(this._decoded.baseLotSize), new BN(10).pow(this._baseMintDecimals), ); } - quoteBnToNumber(size) { + quoteSizeBnToNumber(size) { return divideBnToNumber( size.mul(this._decoded.quoteLotSize), new BN(10).pow(this._quoteMintDecimals), @@ -110,7 +112,7 @@ export class MarketState { } } -setLayoutDecoder(MARKET_STATE_LAYOUT, (decoded) => new MarketState(decoded)); +setLayoutDecoder(MARKET_STATE_LAYOUT, (decoded) => new Market(decoded)); export const OPEN_ORDERS_LAYOUT = struct([ accountFlags('accountFlags'), @@ -130,6 +132,8 @@ export const OPEN_ORDERS_LAYOUT = struct([ seq(u128(), 128, 'orders'), ]); +export class OpenOrders {} + export const ORDERBOOK_LAYOUT = struct([ accountFlags('accountFlags'), SLAB_LAYOUT.replicate('slab'), @@ -153,7 +157,7 @@ export class Orderbook { getLevels(depth) { const descending = this.isBids; const levels = []; // (price, size) - for (let { key, quantity } of this.slab.items(descending)) { + for (const { key, quantity } of this.slab.items(descending)) { const price = getPriceFromKey(key); if (levels.length > 0 && levels[levels.length - 1][0].equals(price)) { levels[levels.length - 1].iadd(quantity); @@ -165,7 +169,7 @@ export class Orderbook { } return levels.map(([price, size]) => [ this.market.priceBnToNumber(price), - this.market.baseBnToNumber(size.mul(this.market.baseLotSize)), + this.market.baseSizeBnToNumber(size.mul(this.market.baseLotSize)), ]); } } diff --git a/src/market-state.test.js b/src/market.test.js similarity index 96% rename from src/market-state.test.js rename to src/market.test.js index d14a97d..ee6cdef 100644 --- a/src/market-state.test.js +++ b/src/market.test.js @@ -1,4 +1,4 @@ -import { accountFlags } from './market-state'; +import { accountFlags } from './market'; describe('accountFlags', () => { const layout = accountFlags(); diff --git a/src/slab.js b/src/slab.js index 84a8275..8a68811 100644 --- a/src/slab.js +++ b/src/slab.js @@ -69,7 +69,7 @@ export class Slab { return SLAB_LAYOUT.decode(buffer); } - get = (searchKey) => { + get(searchKey) { if (this.header.leafCount === 0) { return null; } @@ -101,7 +101,7 @@ export class Slab { throw new Error('Invalid slab'); } } - }; + } [Symbol.iterator]() { return this.items(false); diff --git a/src/slab.test.js b/src/slab.test.js index 01baf04..c12d135 100644 --- a/src/slab.test.js +++ b/src/slab.test.js @@ -47,7 +47,7 @@ describe('slab', () => { it('iterates in order', () => { let previous = null; - for (let item of slab) { + for (const item of slab) { if (previous) { expect(item.key.gt(previous.key)).toBeTruthy(); }