placeOrder works
This commit is contained in:
parent
9f83e6cbdd
commit
3c3bb578f4
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
104
ts/src/market.ts
104
ts/src/market.ts
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
13
ts/yarn.lock
13
ts/yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue