Implement order placement
This commit is contained in:
parent
3753e0ac29
commit
c405061e92
|
@ -24,7 +24,7 @@ for (let order of asks) {
|
|||
order.orderId,
|
||||
order.owner.toBase58(),
|
||||
order.price,
|
||||
order.quantity,
|
||||
order.size,
|
||||
order.side,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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({
|
||||
|
|
270
src/market.js
270
src/market.js
|
@ -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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue