Add more instructions

This commit is contained in:
Gary Wang 2020-08-10 06:06:31 -07:00
parent 0888234da3
commit 02f41fd3b1
9 changed files with 222 additions and 31 deletions

View File

@ -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",

View File

@ -1 +1,2 @@
export { MarketState, Orderbook } from './market-state';
export { Market, Orderbook } from './market';
export { DexInstructions } from './instructions';

View File

@ -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 },
}),
});
}
}

View File

@ -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(

View File

@ -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) {

View File

@ -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)),
]);
}
}

View File

@ -1,4 +1,4 @@
import { accountFlags } from './market-state';
import { accountFlags } from './market';
describe('accountFlags', () => {
const layout = accountFlags();

View File

@ -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);

View File

@ -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();
}