diff --git a/.gitignore b/.gitignore index ab714c4..c3fe115 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,5 @@ dist .idea/ lib/ + +secrets.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3b4e8dc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +lib/ +node_modules/ diff --git a/shell b/shell index 1a2b797..1cba408 100644 --- a/shell +++ b/shell @@ -1,3 +1,5 @@ const lib = require('./lib/index'); const solana = require('@solana/web3.js'); const serum = require('@project-serum/serum'); + +const SerumApi = lib.exchange.SerumApi; diff --git a/src/config.ts b/src/config.ts index 6cac784..07a03b8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,4 @@ import dotenv from "dotenv"; -import {PublicKey} from "@solana/web3.js"; -import {Market} from "./exchange/types"; // use passed port if specified otherwise default to the .env file const PASSED_PORT = process.env.PORT; @@ -20,4 +18,5 @@ export const RESTART_INTERVAL_SEC = parseInt( 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"; +export const SOLANA_URL = + process.env.SOLANA_URL || "http://validator-lb.wirelesstable.net"; diff --git a/src/exchange/api.ts b/src/exchange/api.ts index d7e6d44..0f77356 100644 --- a/src/exchange/api.ts +++ b/src/exchange/api.ts @@ -1,22 +1,56 @@ -import { Connection, PublicKey } from "@solana/web3.js"; -import { Exchange, Market, SerumMarketInfo } from "./types"; +import { Account, Connection, PublicKey } from "@solana/web3.js"; +import { Coin, Exchange, MarketInfo, Pair } from "./types"; import * as config from "../config"; +import { COIN_MINTS, EXCHANGE_ENABLED_MARKETS } from "./config"; +import { getKeys } from "../utils"; +import assert from "assert"; +import { Market } from "@project-serum/serum"; export class SerumApi { static readonly exchange: Exchange = "serum"; - private _connections: Connection[]; static url = config.SOLANA_URL; + readonly exchange: Exchange; + readonly marketInfo: { [market: string]: MarketInfo }; + readonly markets: Pair[]; + readonly addressMarkets: { [address: string]: Market }; + readonly marketAddresses: { [market: string]: PublicKey }; + readonly addressProgramIds: { [address: string]: PublicKey }; + private _connections: Connection[]; + private _publicKey: PublicKey; + private _privateKey: Array; + private _account: Account; + private _wsConnection: Connection; constructor( + exchange: Exchange, conections: Connection[], - marketInfo: { [market: string]: SerumMarketInfo }, - markets: Market[], + marketInfo: { [market: string]: MarketInfo }, + markets: Pair[], marketAddresses: { [market: string]: PublicKey }, addressProgramIds: { [address: string]: PublicKey }, url: string - ) {} + ) { + this.exchange = exchange; + this._connections = conections; + this._privateKey = getKeys([`${this.exchange}_private_key`])[0]; + this._account = new Account(this._privateKey); + this._publicKey = this._account.publicKey; + this.marketInfo = marketInfo; + this.markets = markets; + this.marketAddresses = marketAddresses; + this.addressMarkets = Object.assign( + {}, + ...Object.entries(marketAddresses).map(([market, address]) => ({ + [address.toBase58()]: Pair.fromKey(market), + })) + ); + this.addressProgramIds = addressProgramIds; + this._wsConnection = new Connection(url, "recent"); + } - static async create(options: { [optionName: string]: unknown } = {}): Promise { + static async create( + options: { [optionName: string]: unknown } = {} + ): Promise { const connections: Connection[] = []; for (let i = 0; i < config.NUM_CONNECTIONS; i++) { const url = @@ -28,16 +62,20 @@ export class SerumApi { 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, + EXCHANGE_ENABLED_MARKETS[this.exchange].map((info) => [ + info.market.key(), + info.address, ]) ); - const marketInfo: Array<[Market, SerumMarketInfo]> = await Promise.all( + const markets = EXCHANGE_ENABLED_MARKETS[this.exchange].map( + (marketInfo) => marketInfo.market + ); + const addressProgramIds = Object.fromEntries( + Object.entries( + EXCHANGE_ENABLED_MARKETS[this.exchange] + ).map(([market, info]) => [info.address.toBase58(), info.programId]) + ); + const marketInfo: Array<[Pair, MarketInfo]> = await Promise.all( markets.map((market) => this.getMarketInfo( connections[0], @@ -60,4 +98,62 @@ export class SerumApi { this.url ); } + + static async getMarketInfo( + connection: Connection, + coin: Coin, + priceCurrency: Coin, + marketAddress: PublicKey, + programId: PublicKey + ): Promise { + const market = new Pair(coin, priceCurrency); + const serumMarket = await Market.load( + connection, + marketAddress, + {}, + programId + ); + assert( + serumMarket.baseMintAddress.toBase58() === COIN_MINTS[coin], + `${coin} on ${coin}/${priceCurrency} has wrong mint. Our mint: ${ + COIN_MINTS[coin] + } Serum's mint ${serumMarket.baseMintAddress.toBase58()}` + ); + assert( + serumMarket.quoteMintAddress.toBase58() === COIN_MINTS[priceCurrency], + `${priceCurrency} on ${coin}/${priceCurrency} has wrong mint. Our mint: ${ + COIN_MINTS[priceCurrency] + } Serum's mint ${serumMarket.quoteMintAddress.toBase58()}` + ); + return [ + market, + { + coin: coin, + priceCurrency: priceCurrency, + address: marketAddress, + baseMint: serumMarket.baseMintAddress, + quoteMint: serumMarket.quoteMintAddress, + minOrderSize: serumMarket.minOrderSize, + tickSize: serumMarket.tickSize, + programId: programId, + }, + ]; + } + + async getMarketInfo(): Promise<{ [k: string]: {[prop: string]: string | number}}> { + return Object.fromEntries( + Object.entries(this.marketInfo).map(([market, info]) => [ + market, + { + ...info, + address: info.address.toBase58(), + baseMint: info.baseMint.toBase58(), + quoteMint: info.quoteMint.toBase58(), + programId: info.programId.toBase58(), + minOrderSize: info.minOrderSize, + tickSize: info.tickSize, + }, + ]) + ); + } } diff --git a/src/exchange/config.ts b/src/exchange/config.ts index 466643c..0a67ff1 100644 --- a/src/exchange/config.ts +++ b/src/exchange/config.ts @@ -1,12 +1,13 @@ import { getLayoutVersion, MARKETS, TOKEN_MINTS } from "@project-serum/serum"; import { HARD_CODED_MINTS } from "../config"; -import { Market } from "./types"; +import { Pair } from "./types"; +import { PublicKey } from "@solana/web3.js"; export const MARKET_PARAMS = MARKETS.map((marketInfo) => { const [coin, priceCurrency] = marketInfo.name.split("/"); return { address: marketInfo.address, - market: new Market(coin, priceCurrency), + market: new Pair(coin, priceCurrency), programId: marketInfo.programId, version: getLayoutVersion(marketInfo.programId), }; @@ -14,8 +15,26 @@ export const MARKET_PARAMS = MARKETS.map((marketInfo) => { 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)) +export const COIN_MINTS: { [coin: string]: string } = 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)) ); + +export const MINT_COINS: { [mint: string]: string } = Object.assign( + {}, + ...Object.entries(COIN_MINTS).map(([coin, mint]) => ({ + [mint]: coin, + })) +); + +export const EXCHANGE_ENABLED_MARKETS: { + [exchange: string]: { + address: PublicKey; + market: Pair; + programId: PublicKey; + version: number; + }[]; +} = { + serum: MARKET_PARAMS, +}; diff --git a/src/exchange/index.ts b/src/exchange/index.ts index e69de29..40311b4 100644 --- a/src/exchange/index.ts +++ b/src/exchange/index.ts @@ -0,0 +1,4 @@ +export * from "./api"; +export * from "./config"; +export * from "./solana"; +export * from "./utils"; diff --git a/src/exchange/types.ts b/src/exchange/types.ts index 9119ba2..c594828 100644 --- a/src/exchange/types.ts +++ b/src/exchange/types.ts @@ -5,7 +5,7 @@ import BN from "bn.js"; export type Coin = string; export type Exchange = string; -export class Market { +export class Pair { coin; priceCurrency; @@ -19,15 +19,15 @@ export class Market { } key(): string { - return Market.key(this.coin, this.priceCurrency); + return Pair.key(this.coin, this.priceCurrency); } - static fromKey(key: string): Market { + static fromKey(key: string): Pair { const [coin, priceCurrency] = key.split("/"); - return new Market(coin, priceCurrency); + return new Pair(coin, priceCurrency); } - equals(other: Market): boolean { + equals(other: Pair): boolean { return ( other.coin === this.coin && other.priceCurrency === this.priceCurrency ); @@ -109,7 +109,7 @@ export interface Fill { export interface L2OrderBook { bids: [number, number][]; asks: [number, number][]; - market: Market; + market: Pair; validAt: number; receivedAt: number; } @@ -118,7 +118,7 @@ export interface OwnOrders { [orderId: string]: T; } -export interface SerumMarketInfo { +export interface MarketInfo { address: PublicKey; baseMint: PublicKey; quoteMint: PublicKey; diff --git a/src/exchange/utils.ts b/src/exchange/utils.ts index 9c9bf06..0b63c63 100644 --- a/src/exchange/utils.ts +++ b/src/exchange/utils.ts @@ -1,4 +1,10 @@ -import {Account, Connection, PublicKey, SystemProgram, Transaction} from "@solana/web3.js"; +import { + Account, + Connection, + PublicKey, + SystemProgram, + Transaction, +} from "@solana/web3.js"; import BufferLayout from "buffer-layout"; import { TokenInstructions } from "@project-serum/serum"; @@ -30,7 +36,6 @@ export const MINT_LAYOUT = BufferLayout.struct([ BufferLayout.blob(32, "freezeAuthority"), ]); - export function parseMintData( data: Buffer ): { mintAuthority: PublicKey; supply: number; decimals: number } { diff --git a/src/index.ts b/src/index.ts index 2d66855..cbe0649 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import * as utils from "./utils"; import * as configs from "./config"; +import * as exchange from "./exchange/index"; -export { utils, configs }; +export { utils, configs, exchange }; diff --git a/src/routes.ts b/src/routes.ts index 46d2228..3d16147 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,5 +1,5 @@ import express from "express"; -import {SerumApi} from "./exchange/api"; +import { SerumApi } from "./exchange/api"; import expressAsyncHandler from "express-async-handler"; import { logger } from "./utils"; @@ -7,9 +7,7 @@ const router = express.Router(); let api: SerumApi; router.get("/", (req, res, next) => { - res.send( - "Hello from the Serum rest server!" - ); + res.send("Hello from the Serum rest server!"); }); router.use( diff --git a/src/utils.ts b/src/utils.ts index 81d4ccd..e237b8d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -71,9 +71,7 @@ class MorganStream { } export const morganStream = new MorganStream(); -export const getKeys = ( - keys: string[] -): string[] => { +export const getKeys = (keys: string[]): any[] => { const allSecrets = JSON.parse(readFileSync(SECRETS_FILE, "utf-8")); const secrets: string[] = []; for (const key of keys) {