Implement order placement

This commit is contained in:
Gary Wang 2020-08-12 11:45:42 -07:00
parent 3753e0ac29
commit c405061e92
5 changed files with 401 additions and 17 deletions

View File

@ -24,7 +24,7 @@ for (let order of asks) {
order.orderId,
order.owner.toBase58(),
order.price,
order.quantity,
order.size,
order.side,
);
}

View File

@ -1,6 +1,6 @@
{
"name": "@project-serum/serum",
"version": "0.1.1",
"version": "0.2.0",
"description": "Library for interacting with the serum dex",
"license": "MIT",
"repository": "project-serum/serum-js",

View File

@ -9,6 +9,7 @@ import {
zeros,
} from './layout';
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from './token-instructions';
export const DEX_PROGRAM_ID = new PublicKey(
'9o1FisE366msTQcEvXapyMorTLmvezrxSD8DnM5e5XKw',
@ -130,8 +131,9 @@ export class DexInstructions {
{ 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 },
{ pubkey: baseVault, isSigner: false, isWritable: true },
{ pubkey: quoteVault, isSigner: false, isWritable: true },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
],
programId: DEX_PROGRAM_ID,
data: encodeInstruction({

View File

@ -1,8 +1,14 @@
import { blob, seq, struct, u8 } from 'buffer-layout';
import { publicKeyLayout, u128, u64, WideBits } from './layout';
import { SLAB_LAYOUT } from './slab';
import { DEX_PROGRAM_ID } from './instructions';
import { DEX_PROGRAM_ID, DexInstructions } from './instructions';
import BN from 'bn.js';
import {
Account,
PublicKey,
SystemProgram,
Transaction,
} from '@solana/web3.js';
const ACCOUNT_FLAGS_LAYOUT = new WideBits();
ACCOUNT_FLAGS_LAYOUT.addBoolean('initialized');
@ -59,8 +65,8 @@ export class Market {
this._quoteMintDecimals = quoteMintDecimals;
}
static decode(buffer) {
return MARKET_STATE_LAYOUT.decode(buffer);
static get LAYOUT() {
return MARKET_STATE_LAYOUT;
}
static async load(connection, address) {
@ -69,6 +75,13 @@ export class Market {
throw new Error('Address not owned by program');
}
const decoded = MARKET_STATE_LAYOUT.decode(data);
if (
!decoded.accountFlags.initialized ||
!decoded.accountFlags.market ||
!decoded.ownAddress.equals(address)
) {
throw new Error('Invalid market');
}
const [baseMintDecimals, quoteMintDecimals] = await Promise.all([
getMintDecimals(connection, decoded.baseMint),
getMintDecimals(connection, decoded.quoteMint),
@ -76,6 +89,22 @@ export class Market {
return new Market(decoded, baseMintDecimals, quoteMintDecimals);
}
get address() {
return this._decoded.ownAddress;
}
get publicKey() {
return this.address;
}
get baseMintAddress() {
return this._decoded.baseMint;
}
get quoteMintAddress() {
return this._decoded.quoteMint;
}
async loadBids(connection) {
const { data } = await connection.getAccountInfo(this._decoded.bids);
return Orderbook.decode(this, data);
@ -86,18 +115,98 @@ export class Market {
return Orderbook.decode(this, data);
}
async findBaseTokenAccountsForOwner(connection, ownerAddress) {
return (
await connection.getTokenAccountsByOwner(ownerAddress, {
mint: this.baseMintAddress,
})
).value;
}
async findQuoteTokenAccountsForOwner(connection, ownerAddress) {
return (
await connection.getTokenAccountsByOwner(ownerAddress, {
mint: this.quoteMintAddress,
})
).value;
}
async findOpenOrdersAccountsForOwner(connection, ownerAddress) {
return OpenOrders.findForMarketAndOwner(
connection,
this.address,
ownerAddress,
);
}
async placeOrder(
connection,
{ owner, payer, side, price, size, orderType = 'limit' },
) {
throw new Error('not yet implemented');
const { transaction, signers } = await this.makePlaceOrderTransaction(
connection,
{
owner,
payer,
side,
price,
size,
orderType,
},
);
return await connection.sendTransaction(transaction, signers);
}
async makePlaceOrderTransaction(
connection,
{ owner, payer, side, price, size, orderType = 'limit' },
) {
const ownerAddress = owner.publicKey ?? owner;
const openOrdersAccounts = await this.findOpenOrdersAccountsForOwner(
connection,
ownerAddress,
); // TODO: cache this
const transaction = new Transaction();
const signers = [owner];
let openOrdersAddress;
if (openOrdersAccounts.length === 0) {
const newOpenOrdersAccount = new Account();
transaction.add(
await OpenOrders.makeCreateAccountTransaction(
connection,
this.address,
ownerAddress,
newOpenOrdersAccount.publicKey,
),
);
openOrdersAddress = newOpenOrdersAccount.publicKey;
signers.push(newOpenOrdersAccount);
} else {
openOrdersAddress = openOrdersAccounts[0].address;
}
transaction.add(
DexInstructions.newOrder({
market: this.address,
requestQueue: this._decoded.requestQueue,
baseVault: this._decoded.baseVault,
quoteVault: this._decoded.quoteVault,
openOrders: openOrdersAddress,
owner: ownerAddress,
payer,
side,
limitPrice: this.priceNumberToLots(price),
maxQuantity: this.baseSizeNumberToLots(size),
orderType,
}),
);
return { transaction, signers };
}
async cancelOrder(connection, order) {
throw new Error('not yet implemented');
}
priceBnToNumber(price) {
priceLotsToNumber(price) {
return divideBnToNumber(
price
.mul(this._decoded.quoteLotSize)
@ -106,19 +215,47 @@ export class Market {
);
}
baseSizeBnToNumber(size) {
priceNumberToLots(price) {
return new BN(
Math.round(
(price *
Math.pow(10, this._quoteMintDecimals) *
this._decoded.baseLotSize.toNumber()) /
(Math.pow(10, this._baseMintDecimals) *
this._decoded.quoteLotSize.toNumber()),
),
);
}
baseSizeLotsToNumber(size) {
return divideBnToNumber(
size.mul(this._decoded.baseLotSize),
new BN(10).pow(this._baseMintDecimals),
);
}
quoteSizeBnToNumber(size) {
baseSizeNumberToLots(size) {
const native = new BN(
Math.round(size * Math.pow(10, this._baseMintDecimals)),
);
// rounds down to the nearest lot size
return native.div(this._decoded.baseLotSize);
}
quoteSizeLotsToNumber(size) {
return divideBnToNumber(
size.mul(this._decoded.quoteLotSize),
new BN(10).pow(this._quoteMintDecimals),
);
}
quoteSizeNumberToLots(size) {
const native = new BN(
Math.round(size * Math.pow(10, this._quoteMintDecimals)),
);
// rounds down to the nearest lot size
return native.div(this._decoded.quoteLotSize);
}
}
export const OPEN_ORDERS_LAYOUT = struct([
@ -139,7 +276,85 @@ export const OPEN_ORDERS_LAYOUT = struct([
seq(u128(), 128, 'orders'),
]);
export class OpenOrders {}
export class OpenOrders {
constructor(address, decoded) {
this.address = address;
Object.assign(this, decoded);
}
static get LAYOUT() {
return OPEN_ORDERS_LAYOUT;
}
static async findForMarketAndOwner(connection, marketAddress, ownerAddress) {
const filters = [
{
memcmp: {
offset: OPEN_ORDERS_LAYOUT.offsetOf('market'),
bytes: marketAddress.toBase58(),
},
},
{
memcmp: {
offset: OPEN_ORDERS_LAYOUT.offsetOf('owner'),
bytes: ownerAddress.toBase58(),
},
},
{
dataSize: OPEN_ORDERS_LAYOUT.span,
},
];
const accounts = await getFilteredProgramAccounts(
connection,
DEX_PROGRAM_ID,
filters,
);
return accounts.map(({ publicKey, accountInfo }) =>
OpenOrders.fromAccountInfo(publicKey, accountInfo),
);
}
static async load(connection, address) {
const accountInfo = await connection.getAccountInfo(address);
if (accountInfo === null) {
throw new Error('Open orders account not found');
}
return OpenOrders.fromAccountInfo(connection, accountInfo);
}
static fromAccountInfo(address, accountInfo) {
const { owner, data } = accountInfo;
if (!owner.equals(DEX_PROGRAM_ID)) {
throw new Error('Address not owned by program');
}
const decoded = OPEN_ORDERS_LAYOUT.decode(data);
if (!decoded.accountFlags.initialized || !decoded.accountFlags.openOrders) {
throw new Error('Invalid open orders account');
}
return new OpenOrders(address, decoded);
}
static async makeCreateAccountTransaction(
connection,
marketAddress,
ownerAddress,
newAccountAddress,
) {
return SystemProgram.createAccount({
fromPubkey: ownerAddress,
newAccountPubkey: newAccountAddress,
lamports: await connection.getMinimumBalanceForRentExemption(
OPEN_ORDERS_LAYOUT.span,
),
space: OPEN_ORDERS_LAYOUT.span,
programId: DEX_PROGRAM_ID,
});
}
get publicKey() {
return this.address;
}
}
export const ORDERBOOK_LAYOUT = struct([
accountFlags('accountFlags'),
@ -174,9 +389,11 @@ export class Orderbook {
levels.push([price, quantity]);
}
}
return levels.map(([price, size]) => [
this.market.priceBnToNumber(price),
this.market.baseSizeBnToNumber(size.mul(this.market.baseLotSize)),
return levels.map(([priceLots, sizeLots]) => [
this.market.priceLotsToNumber(priceLots),
this.market.baseSizeLotsToNumber(sizeLots.mul(this.market.baseLotSize)),
priceLots,
sizeLots,
]);
}
@ -187,8 +404,10 @@ export class Orderbook {
orderId: key,
ownerSlot,
owner,
price: this.market.priceBnToNumber(price),
quantity: this.market.baseSizeBnToNumber(quantity),
price: this.market.priceLotsToNumber(price),
priceLots: price,
size: this.market.baseSizeLotsToNumber(quantity),
sizeLots: quantity,
side: this.isBids ? 'buy' : 'sell',
};
}
@ -213,3 +432,28 @@ export async function getMintDecimals(connection, mint) {
const { decimals } = MINT_LAYOUT.decode(data);
return decimals;
}
async function getFilteredProgramAccounts(connection, programId, filters) {
const resp = await connection._rpcRequest('getProgramAccounts', [
programId.toBase58(),
{
commitment: connection.commitment,
filters,
encoding: 'binary64',
},
]);
if (resp.error) {
throw new Error(resp.error.message);
}
return resp.result.map(
({ pubkey, account: { data, executable, owner, lamports } }) => ({
publicKey: new PublicKey(pubkey),
accountInfo: {
data: Buffer.from(data, 'base64'),
executable,
owner: new PublicKey(owner),
lamports,
},
}),
);
}

138
src/token-instructions.js Normal file
View File

@ -0,0 +1,138 @@
import * as BufferLayout from 'buffer-layout';
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
export const TOKEN_PROGRAM_ID = new PublicKey(
'TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o',
);
export const WRAPPED_SOL_MINT = new PublicKey(
'So11111111111111111111111111111111111111111',
);
const LAYOUT = BufferLayout.union(BufferLayout.u8('instruction'));
LAYOUT.addVariant(
0,
BufferLayout.struct([
// TODO: does this need to be aligned?
BufferLayout.nu64('amount'),
BufferLayout.u8('decimals'),
]),
'initializeMint',
);
LAYOUT.addVariant(1, BufferLayout.struct([]), 'initializeAccount');
LAYOUT.addVariant(
3,
BufferLayout.struct([BufferLayout.nu64('amount')]),
'transfer',
);
LAYOUT.addVariant(
4,
BufferLayout.struct([BufferLayout.nu64('amount')]),
'approve',
);
LAYOUT.addVariant(
7,
BufferLayout.struct([BufferLayout.nu64('amount')]),
'mintTo',
);
LAYOUT.addVariant(
8,
BufferLayout.struct([BufferLayout.nu64('amount')]),
'burn',
);
const instructionMaxSpan = Math.max(
...Object.values(LAYOUT.registry).map((r) => r.span),
);
function encodeTokenInstructionData(instruction) {
const b = Buffer.alloc(instructionMaxSpan);
const span = LAYOUT.encode(instruction, b);
return b.slice(0, span);
}
export function initializeMint({
mint,
amount,
decimals,
initialAccount,
mintOwner,
}) {
const keys = [{ pubkey: mint, isSigner: false, isWritable: true }];
if (amount) {
keys.push({ pubkey: initialAccount, isSigner: false, isWritable: true });
}
if (mintOwner) {
keys.push({ pubkey: mintOwner, isSigner: false, isWritable: false });
}
return new TransactionInstruction({
keys,
data: encodeTokenInstructionData({
initializeMint: {
amount,
decimals,
},
}),
programId: TOKEN_PROGRAM_ID,
});
}
export function initializeAccount({ account, mint, owner }) {
const keys = [
{ pubkey: account, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: owner, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
data: encodeTokenInstructionData({
initializeAccount: {},
}),
programId: TOKEN_PROGRAM_ID,
});
}
export function transfer({ source, destination, amount, owner }) {
const keys = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false },
];
return new TransactionInstruction({
keys,
data: encodeTokenInstructionData({
transfer: { amount },
}),
programId: TOKEN_PROGRAM_ID,
});
}
export function approve({ source, delegate, amount, owner }) {
const keys = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: delegate, isSigner: false, isWritable: false },
{ pubkey: owner, isSigner: true, isWritable: false },
];
return new TransactionInstruction({
keys,
data: encodeTokenInstructionData({
approve: { amount },
}),
programId: TOKEN_PROGRAM_ID,
});
}
export function mintTo({ mint, account, amount, owner }) {
const keys = [
{ pubkey: mint, isSigner: false, isWritable: true },
{ pubkey: account, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: false },
];
return new TransactionInstruction({
keys,
data: encodeTokenInstructionData({
mintTo: { amount },
}),
programId: TOKEN_PROGRAM_ID,
});
}