Add
This commit is contained in:
parent
5f0eed3851
commit
787a82a9e9
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,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);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in New Issue