This commit is contained in:
Nathaniel Parke 2020-10-22 09:42:43 +08:00
parent 5f0eed3851
commit 787a82a9e9
8 changed files with 519 additions and 1 deletions

View File

@ -1,6 +1,8 @@
import dotenv from "dotenv";
import {PublicKey} from "@solana/web3.js";
import {Market} from "./exchange/types";
// use passed port if sepcified otherwise default to the .env file
// use passed port if specified otherwise default to the .env file
const PASSED_PORT = process.env.PORT;
dotenv.config();
@ -14,3 +16,8 @@ export const LOGGING_DIR = process.env.LOGGING_DIR || "";
export const RESTART_INTERVAL_SEC = parseInt(
process.env.RESTART_INTERVAL_SEC || "0"
);
export const HARD_CODED_MINTS = process.env.HARD_CODED_MINTS || {};
export const DEFAULT_TIMEOUT = 15000;
export const NUM_CONNECTIONS = 1;
export const SOLANA_URL = process.env.SOLANA_URL || "http://validator-lb.wirelesstable.net";

63
src/exchange/api.ts Normal file
View File

@ -0,0 +1,63 @@
import { Connection, PublicKey } from "@solana/web3.js";
import { Exchange, Market, SerumMarketInfo } from "./types";
import * as config from "../config";
export class SerumApi {
static readonly exchange: Exchange = "serum";
private _connections: Connection[];
static url = config.SOLANA_URL;
constructor(
conections: Connection[],
marketInfo: { [market: string]: SerumMarketInfo },
markets: Market[],
marketAddresses: { [market: string]: PublicKey },
addressProgramIds: { [address: string]: PublicKey },
url: string
) {}
static async create(options: { [optionName: string]: unknown } = {}): Promise<SerumApi> {
const connections: Connection[] = [];
for (let i = 0; i < config.NUM_CONNECTIONS; i++) {
const url =
"url" in options && typeof options.url === "string"
? options.url
: this.url;
const connection = new Connection(url, "recent");
connection.onSlotChange((slotInfo) => {});
connections.push(connection);
}
const marketAddresses = Object.fromEntries(
this.constMarketInfo.map((info) => [info.market.key(), info.address])
);
const markets = this.constMarketInfo.map((marketInfo) => marketInfo.market);
const addressProgramIds = Object.fromEntries(
Object.entries(this.constMarketInfo).map(([market, info]) => [
info.address.toBase58(),
info.programId,
])
);
const marketInfo: Array<[Market, SerumMarketInfo]> = await Promise.all(
markets.map((market) =>
this.getMarketInfo(
connections[0],
market.coin,
market.priceCurrency,
marketAddresses[market.key()],
addressProgramIds[marketAddresses[market.key()].toBase58()]
)
)
);
return new this(
this.exchange,
connections,
Object.fromEntries(
marketInfo.map(([market, info]) => [market.key(), info])
),
markets,
marketAddresses,
addressProgramIds,
this.url
);
}
}

21
src/exchange/config.ts Normal file
View File

@ -0,0 +1,21 @@
import { getLayoutVersion, MARKETS, TOKEN_MINTS } from "@project-serum/serum";
import { HARD_CODED_MINTS } from "../config";
import { Market } from "./types";
export const MARKET_PARAMS = MARKETS.map((marketInfo) => {
const [coin, priceCurrency] = marketInfo.name.split("/");
return {
address: marketInfo.address,
market: new Market(coin, priceCurrency),
programId: marketInfo.programId,
version: getLayoutVersion(marketInfo.programId),
};
});
export const HARD_CODED_COINS = new Set(Object.keys(HARD_CODED_MINTS));
export const COIN_MINTS = Object.fromEntries(
TOKEN_MINTS.filter(mint => !(mint.name in HARD_CODED_MINTS))
.map((mint) => [mint.name, mint.address.toBase58()])
.concat(Object.entries(HARD_CODED_MINTS))
);

0
src/exchange/index.ts Normal file
View File

110
src/exchange/solana.ts Normal file
View File

@ -0,0 +1,110 @@
import { Account, Blockhash, Connection, Transaction } from "@solana/web3.js";
import fetch, { Response } from "node-fetch";
import jayson from "jayson/lib/client/browser";
import { sleep } from "../utils";
import { struct } from "superstruct";
export async function signAndSerializeTransaction(
connection: Connection,
transaction: Transaction,
signers: Array<Account>,
blockhash: Blockhash
): Promise<Buffer> {
transaction.recentBlockhash = blockhash;
transaction.sign(...signers);
return transaction.serialize();
}
export type RpcRequest = (methodName: string, args: Array<any>) => any;
function jsonRpcResult(resultDescription: any) {
const jsonRpcVersion = struct.literal("2.0");
return struct.union([
struct({
jsonrpc: jsonRpcVersion,
id: "string",
error: "any",
}),
struct({
jsonrpc: jsonRpcVersion,
id: "string",
error: "null?",
result: resultDescription,
}),
]);
}
function jsonRpcResultAndContext(resultDescription: any) {
return jsonRpcResult({
context: struct({
slot: "number",
}),
value: resultDescription,
});
}
const AccountInfoResult = struct({
executable: "boolean",
owner: "string",
lamports: "number",
data: "any",
rentEpoch: "number?",
});
export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext(
struct.array([struct.union(["null", AccountInfoResult])])
);
export function createRpcRequest(url: string): RpcRequest {
const server = new jayson(async (request, callback) => {
const options = {
method: "POST",
body: request,
headers: {
"Content-Type": "application/json",
},
};
try {
let too_many_requests_retries = 5;
let res: Response = {};
let waitTime = 500;
for (;;) {
res = await fetch(url, options);
if (res.status !== 429 /* Too many requests */) {
break;
}
too_many_requests_retries -= 1;
if (too_many_requests_retries === 0) {
break;
}
console.log(
`Server responded with ${res.status} ${res.statusText}. Retrying after ${waitTime}ms delay...`
);
await sleep(waitTime);
waitTime *= 2;
}
const text = await res.text();
if (res.ok) {
callback(null, text);
} else {
callback(new Error(`${res.status} ${res.statusText}: ${text}`));
}
} catch (err) {
callback(err);
}
}, {});
return (method, args) => {
return new Promise((resolve, reject) => {
server.request(method, args, (err, response) => {
if (err) {
reject(err);
return;
}
resolve(response);
});
});
};
}

215
src/exchange/types.ts Normal file
View File

@ -0,0 +1,215 @@
import { PublicKey } from "@solana/web3.js";
import { Order as SerumOwnOrder } from "@project-serum/serum/lib/market";
import BN from "bn.js";
export type Coin = string;
export type Exchange = string;
export class Market {
coin;
priceCurrency;
constructor(coin: Coin, priceCurrency: Coin) {
this.coin = coin;
this.priceCurrency = priceCurrency;
}
static key(coin: Coin, priceCurrency: Coin): string {
return `${coin}/${priceCurrency}`;
}
key(): string {
return Market.key(this.coin, this.priceCurrency);
}
static fromKey(key: string): Market {
const [coin, priceCurrency] = key.split("/");
return new Market(coin, priceCurrency);
}
equals(other: Market): boolean {
return (
other.coin === this.coin && other.priceCurrency === this.priceCurrency
);
}
}
export enum Dir {
B = 1,
S = -1,
}
export enum OrderType {
limit = "limit",
ioc = "ioc",
postOnly = "postOnly",
}
export enum Liquidity {
T = "T",
M = "M",
}
export class Order<T = any> {
exchange: Exchange;
coin: Coin;
priceCurrency: Coin;
side: Dir;
price: number;
quantity: number;
info: T;
constructor(
exchange: Exchange,
coin: Coin,
priceCurrency: Coin,
side: Dir,
price: number,
quantity: number,
info: T
) {
this.exchange = exchange;
this.coin = coin;
this.priceCurrency = priceCurrency;
this.side = side;
this.price = price;
this.quantity = quantity;
this.info = info;
}
}
export interface Trade<T = any> {
exchange: Exchange;
coin: Coin;
priceCurrency: Coin;
id: string;
orderId: string;
price: number;
quantity: number;
time: number;
side: Dir;
info?: T;
}
export interface Fill<T = any> {
exchange: Exchange;
coin: Coin;
priceCurrency: Coin;
side: Dir;
price: number;
quantity: number;
time: number;
orderId: string;
fee: number;
feeCurrency: Coin;
liquidity: Liquidity;
info?: T;
}
export interface L2OrderBook {
bids: [number, number][];
asks: [number, number][];
market: Market;
validAt: number;
receivedAt: number;
}
export interface OwnOrders<T = Order> {
[orderId: string]: T;
}
export interface SerumMarketInfo {
address: PublicKey;
baseMint: PublicKey;
quoteMint: PublicKey;
minOrderSize: number;
tickSize: number;
programId: PublicKey;
[propName: string]: unknown;
}
export interface SerumFill {
size: number;
price: number;
side: string;
eventFlags: {
fill: boolean;
out: boolean;
bid: boolean;
maker: boolean;
};
orderId: BN;
openOrders: PublicKey;
openOrdersSlot: number;
feeTier: number;
nativeQuantityReleased: BN;
nativeQuantityPaid: BN;
nativeFeeOrRebate: BN;
}
export class SerumOrder {
orderId: string;
openOrdersAddress: string;
openOrdersSlot: number;
price: number;
priceLots: string;
size: number;
sizeLots: string;
side: "buy" | "sell";
clientId: string;
feeTier: number;
constructor(
orderId: string,
openOrdersAddress: string,
openOrdersSlot: number,
price: number,
priceLots: string,
size: number,
sizeLots: string,
side: "buy" | "sell",
clientId: string,
feeTier: number
) {
this.orderId = orderId;
this.openOrdersAddress = openOrdersAddress;
this.openOrdersSlot = openOrdersSlot;
this.price = price;
this.priceLots = priceLots;
this.size = size;
this.sizeLots = sizeLots;
this.side = side;
this.clientId = clientId;
this.feeTier = feeTier;
}
static fromSerumOrder(order: SerumOwnOrder): SerumOrder {
return new SerumOrder(
order.orderId.toString(),
order.openOrdersAddress.toBase58(),
order.openOrdersSlot,
order.price,
order.priceLots.toString(),
order.size,
order.sizeLots.toString(),
order.side,
order.clientId ? order.clientId.toString() : "",
order.feeTier
);
}
toSerumOrder(): SerumOwnOrder {
return {
orderId: new BN(this.orderId),
openOrdersAddress: new PublicKey(this.openOrdersAddress),
openOrdersSlot: this.openOrdersSlot,
price: this.price,
priceLots: new BN(this.priceLots),
size: this.size,
sizeLots: new BN(this.sizeLots),
side: this.side,
clientId: new BN(this.clientId),
feeTier: this.feeTier,
};
}
}

76
src/exchange/utils.ts Normal file
View File

@ -0,0 +1,76 @@
import {Account, Connection, PublicKey, SystemProgram, Transaction} from "@solana/web3.js";
import BufferLayout from "buffer-layout";
import { TokenInstructions } from "@project-serum/serum";
export const ACCOUNT_LAYOUT = BufferLayout.struct([
BufferLayout.blob(32, "mint"),
BufferLayout.blob(32, "owner"),
BufferLayout.nu64("amount"),
BufferLayout.blob(93),
]);
export function parseTokenAccountData(
data: Buffer
): { mint: PublicKey; owner: PublicKey; amount: number } {
const { mint, owner, amount } = ACCOUNT_LAYOUT.decode(data);
return {
mint: new PublicKey(mint),
owner: new PublicKey(owner),
amount,
};
}
export const MINT_LAYOUT = BufferLayout.struct([
BufferLayout.blob(4),
BufferLayout.blob(32, "mintAuthority"),
BufferLayout.blob(8, "supply"),
BufferLayout.u8("decimals"),
BufferLayout.u8("isInitialized"),
BufferLayout.blob(4, "freezeAuthorityOption"),
BufferLayout.blob(32, "freezeAuthority"),
]);
export function parseMintData(
data: Buffer
): { mintAuthority: PublicKey; supply: number; decimals: number } {
const { mintAuthority, supply, decimals } = MINT_LAYOUT.decode(data);
return {
mintAuthority: new PublicKey(mintAuthority),
supply,
decimals,
};
}
export async function createAndInitializeTokenAccount({
connection,
payer,
mintPublicKey,
newAccount,
}: {
connection: Connection;
payer: Account;
mintPublicKey: PublicKey;
newAccount: Account;
}): Promise<string> {
const transaction = new Transaction();
const createAccountInstr = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: newAccount.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
ACCOUNT_LAYOUT.span
),
space: ACCOUNT_LAYOUT.span,
programId: TokenInstructions.TOKEN_PROGRAM_ID,
});
transaction.add(createAccountInstr);
transaction.add(
TokenInstructions.initializeAccount({
account: newAccount.publicKey,
mint: mintPublicKey,
owner: payer.publicKey,
})
);
const signers = [payer, newAccount];
return await connection.sendTransaction(transaction, signers);
}

View File

@ -1,6 +1,10 @@
import express from "express";
import {SerumApi} from "./exchange/api";
import expressAsyncHandler from "express-async-handler";
import { logger } from "./utils";
const router = express.Router();
let api: SerumApi;
router.get("/", (req, res, next) => {
res.send(
@ -8,4 +12,26 @@ router.get("/", (req, res, next) => {
);
});
router.use(
"/",
expressAsyncHandler(async (req, res, next) => {
if (!api) {
logger.debug("Creating api.");
api = await SerumApi.create();
}
next();
})
);
router.get(
"/market_info",
expressAsyncHandler(async (req, res, next) => {
logger.info("Received request to get market_info");
api
.getMarketInfo()
.then((marketInfo) => res.send({ status: "ok", data: marketInfo }))
.catch((err) => next(err));
})
);
export { router as default };