placeOrder works

This commit is contained in:
Sayantan Karmakar 2022-04-23 17:31:59 +05:30
parent 9f83e6cbdd
commit 3c3bb578f4
9 changed files with 260 additions and 47 deletions

View File

@ -23,7 +23,10 @@
]
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.1",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"chai": "^4.3.6",
@ -40,8 +43,6 @@
"@project-serum/serum": "^0.13.62",
"@solana/spl-token": "^0.2.0",
"@solana/web3.js": "^1.37.0",
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"bn.js": "^5.2.0"
}
}

View File

@ -23,29 +23,23 @@ const main = async () => {
);
const dex = new Dex(dexAddress, connection);
const baseCoin = await dex.createCoin(
"SAYA",
9,
owner,
owner.publicKey,
owner.publicKey,
);
const quoteCoin = await dex.createCoin(
"SRM",
9,
owner,
owner.publicKey,
owner.publicKey,
);
const baseCoin = await dex.createCoin("SAYA", 0, owner, owner, owner);
const quoteCoin = await dex.createCoin("SRM", 6, owner, owner, owner);
const market = await dex.initDexMarket(owner, baseCoin, quoteCoin, {
tickSize: 0.001,
lotSize: 10,
tickSize: 0.01,
baseLotSize: new BN(1),
quoteLotSize: new BN(1e4),
feeRate: 10,
quoteDustThreshold: new BN(100),
});
console.log(`Created ${market.marketSymbol} market.`);
await baseCoin.fundAccount(10000, owner, connection);
await quoteCoin.fundAccount(20000, owner, connection);
await market.placeOrder(connection, owner, "buy", 1, 10);
};
const runMain = async () => {

View File

@ -1,7 +1,60 @@
import { PublicKey } from "@solana/web3.js";
import { getOrCreateAssociatedTokenAccount, mintTo } from "@solana/spl-token";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
export interface Coin {
export class Coin {
symbol: string;
decimals: number;
mint: PublicKey;
mintAuthority: Keypair;
freezeAuthority: Keypair;
constructor(
symbol: string,
decimals: number,
mint: PublicKey,
mintAuthority: Keypair,
freezeAuthority: Keypair,
) {
this.symbol = symbol;
this.decimals = decimals;
this.mint = mint;
this.mintAuthority = mintAuthority;
this.freezeAuthority = freezeAuthority;
}
public async fundAccount(
decimalAmount: number,
owner: Keypair,
connection: Connection,
): Promise<void> {
const destination = await getOrCreateAssociatedTokenAccount(
connection,
owner,
this.mint,
owner.publicKey,
true,
"confirmed",
);
const atomicAmount = BigInt(decimalAmount * 10 ** this.decimals);
const txSig = await mintTo(
connection,
owner,
this.mint,
destination.address,
this.mintAuthority,
atomicAmount,
);
await connection.confirmTransaction(txSig);
console.log(
`Funded ${decimalAmount} ${this.symbol} to ${owner.publicKey.toString()}`,
);
}
}

View File

@ -14,7 +14,8 @@ import { Coin } from "./coin";
export type MarketArgs = {
tickSize: number;
lotSize: number;
baseLotSize: BN;
quoteLotSize: BN;
feeRate: number;
quoteDustThreshold: BN;
};
@ -38,22 +39,24 @@ export class Dex {
symbol: string,
decimals: number,
payer: Keypair,
mintAuthority: PublicKey | null,
freezeAuthority: PublicKey | null,
mintAuthority: Keypair | null,
freezeAuthority: Keypair | null,
): Promise<Coin> => {
const mint = await createMint(
this.connection,
payer,
mintAuthority,
freezeAuthority,
mintAuthority ? mintAuthority.publicKey : null,
freezeAuthority ? freezeAuthority.publicKey : null,
decimals,
);
const coin: Coin = {
mint,
decimals,
const coin = new Coin(
symbol,
};
decimals,
mint,
mintAuthority,
freezeAuthority,
);
this.coins.push(coin);
@ -117,14 +120,16 @@ export class Dex {
]);
await this.connection.confirmTransaction(vaultSig);
let baseLotSize;
let quoteLotSize;
if (marketArgs.lotSize > 0) {
baseLotSize = Math.round(10 ** baseCoin.decimals * marketArgs.lotSize);
quoteLotSize = Math.round(
marketArgs.lotSize * 10 ** quoteCoin.decimals * marketArgs.tickSize,
);
}
// let baseLotSize;
// let quoteLotSize;
// if (marketArgs.lotSize > 0) {
// baseLotSize = Math.round(10 ** baseCoin.decimals * marketArgs.lotSize);
// quoteLotSize = Math.round(
// 10 ** quoteCoin.decimals * marketArgs.lotSize * marketArgs.tickSize,
// );
// } else {
// throw new Error("Invalid Lot Size");
// }
const accountsIx = await DexMarket.createMarketAccountsInstructions(
marketAccounts,
@ -143,8 +148,8 @@ export class Dex {
quoteVault: quoteVault.publicKey,
baseMint: baseCoin.mint,
quoteMint: quoteCoin.mint,
baseLotSize: new BN(baseLotSize),
quoteLotSize: new BN(quoteLotSize),
baseLotSize: marketArgs.baseLotSize,
quoteLotSize: marketArgs.quoteLotSize,
feeRateBps: marketArgs.feeRate,
quoteDustThreshold: marketArgs.quoteDustThreshold,
vaultSignerNonce: vaultOwnerNonce,

View File

@ -1,4 +1,5 @@
import { Market } from "@project-serum/serum";
import { OrderParams } from "@project-serum/serum/lib/market";
import {
createInitializeAccountInstruction,
TOKEN_PROGRAM_ID,
@ -12,6 +13,7 @@ import {
TransactionInstruction,
} from "@solana/web3.js";
import { Coin } from "./coin";
import { getDecimalCount, withAssociatedTokenAccount } from "./utils";
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
@ -152,4 +154,106 @@ export class DexMarket {
return [marketIx, requestQueueIx, eventQueueIx, bidsIx, asksIx];
}
private sanityCheck(price: number, size: number) {
const formattedMinOrderSize =
this.serumMarket.minOrderSize?.toFixed(
getDecimalCount(this.serumMarket.minOrderSize),
) || this.serumMarket.minOrderSize;
const formattedTickSize =
this.serumMarket.tickSize?.toFixed(
getDecimalCount(this.serumMarket.tickSize),
) || this.serumMarket.tickSize;
const isIncrement = (num, step) =>
Math.abs((num / step) % 1) < 1e-5 ||
Math.abs(((num / step) % 1) - 1) < 1e-5;
if (isNaN(price)) throw new Error("Invalid Price");
if (isNaN(size)) throw new Error("Invalid Size");
if (!isIncrement(size, this.serumMarket.minOrderSize))
throw new Error(`Size must be an increment of ${formattedMinOrderSize}`);
if (size < this.serumMarket.minOrderSize)
throw new Error(`Size must be greater than ${formattedMinOrderSize}`);
if (!isIncrement(price, this.serumMarket.tickSize))
throw new Error(
`Price: ${price} must be an increment of ${formattedTickSize}`,
);
if (price < this.serumMarket.tickSize)
throw new Error(`Price must be greater than ${formattedTickSize}`);
}
public async placeOrder(
connection: Connection,
owner: Keypair,
side: "buy" | "sell",
size: number,
price: number,
) {
try {
this.sanityCheck(price, size);
} catch (e) {
console.log(e);
throw new Error("Sanity check failed");
}
const transaction = new Transaction();
const signers: Keypair[] = [];
signers.push(owner);
const baseCurrencyAccount = await withAssociatedTokenAccount(
connection,
this.baseCoin.mint,
owner,
transaction,
);
const quoteCurrencyAccount = await withAssociatedTokenAccount(
connection,
this.quoteCoin.mint,
owner,
transaction,
);
const payer = side === "sell" ? baseCurrencyAccount : quoteCurrencyAccount;
if (!payer) {
throw new Error("Need an SPL token account for cost currency as payer");
}
const params: OrderParams<PublicKey> = {
owner: owner.publicKey,
payer,
side,
price,
size,
orderType: "limit",
feeDiscountPubkey: null,
};
const { transaction: placeOrderTx, signers: placeOrderSigners } =
await this.serumMarket.makePlaceOrderTransaction(
connection,
params,
120_000,
120_000,
);
transaction.add(placeOrderTx);
signers.push(
...placeOrderSigners.map((signer) =>
Keypair.fromSecretKey(signer.secretKey),
),
);
const txSig = await connection.sendTransaction(transaction, signers);
await connection.confirmTransaction(txSig, "confirmed");
console.log(`Order placed: ${txSig}`);
}
}

View File

@ -1,4 +1,9 @@
import { PublicKey } from "@solana/web3.js";
import {
createAssociatedTokenAccountInstruction,
getAccount,
getAssociatedTokenAddress,
} from "@solana/spl-token";
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
import BN from "bn.js";
export async function getVaultOwnerAndNonce(
@ -19,3 +24,49 @@ export async function getVaultOwnerAndNonce(
}
}
}
export function getDecimalCount(value): number {
if (
!isNaN(value) &&
Math.floor(value) !== value &&
value.toString().includes(".")
)
return value.toString().split(".")[1].length || 0;
if (
!isNaN(value) &&
Math.floor(value) !== value &&
value.toString().includes("e")
)
return parseInt(value.toString().split("e-")[1] || "0");
return 0;
}
export async function withAssociatedTokenAccount(
connection: Connection,
mint: PublicKey,
owner: Keypair,
transaction: Transaction,
): Promise<PublicKey> {
const ataAddress = await getAssociatedTokenAddress(
mint,
owner.publicKey,
true,
);
try {
await getAccount(connection, ataAddress, "confirmed");
} catch (e) {
transaction.add(
await createAssociatedTokenAccountInstruction(
owner.publicKey,
ataAddress,
owner.publicKey,
mint,
),
);
}
return ataAddress;
}
export function getUnixTs() {
return new Date().getTime() / 1000;
}

View File

@ -35,7 +35,7 @@ describe("Serum Dev Tools", () => {
assert.equal(coin.symbol, "SAYA");
});
it("can create dex accounts", async () => {
it("can init dex market", async () => {
await dex.createCoin("SRM", 6, owner, owner.publicKey, null);
const dexMarket = await dex.initDexMarket(

View File

@ -2,11 +2,11 @@
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"lib": ["es2015"],
"lib": ["es2015", "dom"],
"declaration": true,
"outDir": "./dist",
"typeRoots": ["./node_modules/@types"],
"types": ["mocha", "chai"],
"types": ["mocha", "chai", "node"],
// for BN constructor
"esModuleInterop": true
},

View File

@ -234,10 +234,10 @@
dependencies:
"@types/node" "*"
"@types/chai@^4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
"@types/chai@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.1.tgz#e2c6e73e0bdeb2521d00756d099218e9f5d90a04"
integrity sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==
"@types/connect@^3.4.33":
version "3.4.35"
@ -285,6 +285,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.47.tgz#ca9237d51f2a2557419688511dab1c8daf475188"
integrity sha512-BzcaRsnFuznzOItW1WpQrDHM7plAa7GIDMZ6b5pnMbkqEtM/6WCOhvZar39oeMQP79gwvFUWjjptE7/KGcNqFg==
"@types/node@^17.0.25":
version "17.0.25"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448"
integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"