diff --git a/ts/scripts/marketMaking.ts b/ts/scripts/marketMaking.ts index e067fa6..ff610b4 100644 --- a/ts/scripts/marketMaking.ts +++ b/ts/scripts/marketMaking.ts @@ -51,12 +51,17 @@ const main = async () => { console.log(`Funded owner with ${baseCoin.symbol} and ${quoteCoin.symbol}`); dex.runMarketMaker(market, owner, { - durationInSecs: 15, + durationInSecs: 30, orderCount: 3, initialBidSize: 1000, baseGeckoSymbol: "solana", quoteGeckoSymbol: "usd", }); + + dex.runCrank(market, owner, { + durationInSecs: 20, + verbose: true, + }); }; const runMain = async () => { diff --git a/ts/src/dex.ts b/ts/src/dex.ts index 0b0b501..4567ab6 100644 --- a/ts/src/dex.ts +++ b/ts/src/dex.ts @@ -41,6 +41,11 @@ export type MarketMakerOpts = { quoteGeckoSymbol: string; }; +export type CrankOpts = { + durationInSecs: number; + verbose: boolean; +}; + /** * Dex is a wrapper class for a deployed Serum Dex program. */ @@ -263,7 +268,7 @@ export class Dex { }); console.log( - `Process ${child.pid}: Running Market Maker for ${market.baseCoin.symbol}/${market.quoteCoin.symbol}. Note: No crank running`, + `Process ${child.pid}: Running Market Maker for ${market.baseCoin.symbol}/${market.quoteCoin.symbol}.`, ); // unref doesn't seem to be making a difference for a forked process. @@ -289,4 +294,38 @@ export class Dex { return child; } + + public runCrank( + market: DexMarket, + owner: FileKeypair, + opts: CrankOpts, + ): ChildProcess { + if (opts.durationInSecs < 0) + throw new Error("Duration must be greater than 0."); + + const child = fork(`${__dirname}/scripts/cranker`, null, { + // https://nodejs.org/api/child_process.html#optionsdetached + // detached also doesn't seem to be making a difference. + detached: true, + stdio: ["pipe", 0, 0, "ipc"], + }); + + console.log( + `Process ${child.pid}: Running Crank for ${market.baseCoin.symbol}/${market.quoteCoin.symbol}.`, + ); + + child.send({ + action: "start", + args: { + marketAddress: market.address.toString(), + programID: this.address.toString(), + rpcEndpoint: this.connection.rpcEndpoint, + ownerFilePath: owner.filePath, + duration: opts.durationInSecs * 1000, + verbose: opts.verbose ? "true" : "false", + }, + }); + + return child; + } } diff --git a/ts/src/index.ts b/ts/src/index.ts index 823ee18..f2f6a8e 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -2,4 +2,4 @@ export * from "./dex"; export * from "./coin"; export * from "./market"; export * from "./fileKeypair"; -export * from "./types"; +export { OrderType, TransactionWithSigners } from "./types"; diff --git a/ts/src/market.ts b/ts/src/market.ts index 05af787..4a9915c 100644 --- a/ts/src/market.ts +++ b/ts/src/market.ts @@ -19,7 +19,7 @@ import { import { Market as SerumMarket } from "@project-serum/serum"; import { Coin } from "./coin"; import { getDecimalCount, withAssociatedTokenAccount } from "./utils"; -import { OrderType, TransactionWithSigners } from "./types"; +import { OrderType, SelfTradeBehaviour, TransactionWithSigners } from "./types"; const REQUEST_QUEUE_SIZE = 5120 + 12; // https://github.com/mithraiclabs/psyoptions/blob/f0c9f73408a27676e0c7f156f5cae71f73f59c3f/programs/psy_american/src/lib.rs#L1003 const EVENT_QUEUE_SIZE = 262144 + 12; // https://github.com/mithraiclabs/psyoptions-ts/blob/ba1888ea83e634e1c7a8dad820fe67d053cf3f5c/packages/psy-american/src/instructions/initializeSerumMarket.ts#L84 @@ -289,6 +289,7 @@ export class DexMarket { orderType: OrderType, size: number, price: number, + selfTradeBehaviour?: SelfTradeBehaviour, ): Promise { try { DexMarket.sanityCheck(serumMarket, price, size); @@ -335,6 +336,7 @@ export class DexMarket { orderType, feeDiscountPubkey: null, openOrdersAddressKey: openOrders.address, + selfTradeBehavior: selfTradeBehaviour, }; const { transaction: placeOrderTx, signers: placeOrderSigners } = diff --git a/ts/src/scripts/cranker.ts b/ts/src/scripts/cranker.ts new file mode 100644 index 0000000..ee4c9b4 --- /dev/null +++ b/ts/src/scripts/cranker.ts @@ -0,0 +1,70 @@ +import { MessageType } from "../types"; +import { Market as SerumMarket } from "@project-serum/serum"; +import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { delay, logIfVerbose } from "../utils"; +import { FileKeypair } from "../fileKeypair"; + +const MAX_OPEN_ORDERS = 10; + +process.on("message", async (message: MessageType) => { + if (message.action === "start") { + await basicCranker(message.args); + } +}); + +const crank = async ( + market: SerumMarket, + owner: Keypair, + connection: Connection, + isVerbose: boolean, +) => { + const eventQueue = await market.loadEventQueue(connection); + logIfVerbose(`EventQueue length: ${eventQueue.length}`, isVerbose); + + if (eventQueue.length > 0) { + const orderedAccounts: PublicKey[] = eventQueue + .slice(0, MAX_OPEN_ORDERS) + .map((e) => e.openOrders) + .sort((a, b) => a.toBuffer().swap64().compare(b.toBuffer().swap64())); + + const tx = new Transaction(); + tx.add(market.makeConsumeEventsInstruction(orderedAccounts, 65535)); + + try { + const sig = await connection.sendTransaction(tx, [owner]); + await connection.confirmTransaction(sig, "confirmed"); + logIfVerbose(`ConsumeEvents: ${sig}`, isVerbose); + logIfVerbose(`------ ConsumeEvents Confirmed ------`, isVerbose); + } catch (err) { + logIfVerbose(`Error: ${err}`, isVerbose); + logIfVerbose(`------ ConsumeEvents Failed ------`, isVerbose); + } + } +}; + +const basicCranker = async (args) => { + setTimeout(() => { + console.log(`Exiting Cranker @ ${process.pid}`); + process.exit(0); + }, Number.parseInt(args.duration)); + + const isVerbose = args.verbose === "true"; + + const owner = FileKeypair.load(args.ownerFilePath); + const connection = new Connection(args.rpcEndpoint, "confirmed"); + + const serumMarket = await SerumMarket.load( + connection, + new PublicKey(args.marketAddress), + { commitment: "confirmed" }, + new PublicKey(args.programID), + ); + + await crank(serumMarket, owner.keypair, connection, isVerbose); + + do { + await delay(2000); + await crank(serumMarket, owner.keypair, connection, isVerbose); + // eslint-disable-next-line no-constant-condition + } while (true); +}; diff --git a/ts/src/scripts/marketMaker.ts b/ts/src/scripts/marketMaker.ts index 00a3f4c..1bb3889 100644 --- a/ts/src/scripts/marketMaker.ts +++ b/ts/src/scripts/marketMaker.ts @@ -10,12 +10,7 @@ import { FileKeypair } from "../fileKeypair"; import { DexMarket } from "../market"; import axios from "axios"; import { getDecimalCount, roundToDecimal } from "../utils"; - -type MessageType = { - action: "start"; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: any; -}; +import { MessageType } from "../types"; process.on("message", async (message: MessageType) => { if (message.action === "start") { @@ -121,6 +116,7 @@ const placeOrders = async ( "postOnly", buySize, buyPrice, + "decrementTake", ); tx.add(buyTransaction); signersArray.push(buySigners); @@ -136,6 +132,7 @@ const placeOrders = async ( "postOnly", sellSize, sellPrice, + "decrementTake", ); tx.add(sellTransaction); signersArray.push(sellSigners); @@ -169,7 +166,7 @@ const marketMaker = async (args) => { const owner = FileKeypair.load(args.ownerFilePath); - placeOrders(owner.keypair, serumMarket, connection, { + await placeOrders(owner.keypair, serumMarket, connection, { orderCount: Number.parseInt(args.orderCount), initialBidSize: Number.parseInt(args.initialBidSize), baseGeckoSymbol: args.baseGeckoSymbol, diff --git a/ts/src/types.ts b/ts/src/types.ts index 10e1450..4ce41ac 100644 --- a/ts/src/types.ts +++ b/ts/src/types.ts @@ -6,3 +6,13 @@ export type TransactionWithSigners = { }; export type OrderType = "limit" | "ioc" | "postOnly"; +export type SelfTradeBehaviour = + | "decrementTake" + | "cancelProvide" + | "abortTransaction"; + +export type MessageType = { + action: "start"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any; +}; diff --git a/ts/src/utils.ts b/ts/src/utils.ts index 7a3aa0f..db07944 100644 --- a/ts/src/utils.ts +++ b/ts/src/utils.ts @@ -77,3 +77,13 @@ export function roundToDecimal( ) { return decimals ? Math.round(value * 10 ** decimals) / 10 ** decimals : value; } + +export function logIfVerbose(message: string, isVerbose: boolean) { + if (isVerbose) { + console.log(message); + } +} + +export function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +}