411 lines
16 KiB
TypeScript
411 lines
16 KiB
TypeScript
import { MangoClient, MangoGroup } from '../src/client';
|
|
import { Account, Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionSignature } from '@solana/web3.js';
|
|
import { Market, TokenInstructions, OpenOrders, Orderbook } from '@project-serum/serum';
|
|
import { Order } from '@project-serum/serum/lib/market';
|
|
import { token } from '@project-serum/common';
|
|
import { u64, NATIVE_MINT } from "@solana/spl-token";
|
|
import { sleep, getDecimalCount } from '../src/utils';
|
|
|
|
console.log = function () {}; // NOTE: Disable all unnecessary logging
|
|
|
|
const FAUCET_PROGRAM_ID = new PublicKey(
|
|
"4bXpkKSV8swHSnwqtzuboGPaPDeEgAn4Vt8GfarV5rZt"
|
|
);
|
|
|
|
const getPDA = () => {
|
|
return PublicKey.findProgramAddress([Buffer.from("faucet")], FAUCET_PROGRAM_ID);
|
|
}
|
|
|
|
export async function _sendTransaction (
|
|
connection: Connection,
|
|
transaction: Transaction,
|
|
signers: Account[],
|
|
): Promise<TransactionSignature> {
|
|
await sleep(1000)
|
|
const signature = await connection.sendTransaction(transaction, signers);
|
|
try {
|
|
await connection.confirmTransaction(signature);
|
|
} catch (e) {
|
|
console.info("Error while confirming, trying again");
|
|
await connection.confirmTransaction(signature);
|
|
}
|
|
return signature;
|
|
}
|
|
|
|
export async function createTokenAccountInstrs (
|
|
connection: Connection,
|
|
newAccountPubkey: PublicKey,
|
|
mint: PublicKey,
|
|
ownerPk: PublicKey,
|
|
lamports?: number,
|
|
): Promise<TransactionInstruction[]> {
|
|
if (lamports === undefined) lamports = await connection.getMinimumBalanceForRentExemption(165);
|
|
return [
|
|
SystemProgram.createAccount({
|
|
fromPubkey: ownerPk,
|
|
newAccountPubkey,
|
|
space: 165,
|
|
lamports,
|
|
programId: TokenInstructions.TOKEN_PROGRAM_ID,
|
|
}),
|
|
TokenInstructions.initializeAccount({
|
|
account: newAccountPubkey,
|
|
mint,
|
|
owner: ownerPk,
|
|
}),
|
|
];
|
|
}
|
|
|
|
export async function createWrappedNativeAccount (
|
|
connection: Connection,
|
|
owner: Account,
|
|
amount: number
|
|
): Promise<PublicKey> {
|
|
// Allocate memory for the account
|
|
const balanceNeeded = await connection.getMinimumBalanceForRentExemption(165);
|
|
const newAccount = new Account();
|
|
const tx = new Transaction();
|
|
tx.add(SystemProgram.createAccount({
|
|
fromPubkey: owner.publicKey,
|
|
newAccountPubkey: newAccount.publicKey,
|
|
lamports: balanceNeeded,
|
|
space: 165,
|
|
programId: TokenInstructions.TOKEN_PROGRAM_ID,
|
|
})); // Send lamports to it (these will be wrapped into native tokens by the token program)
|
|
tx.add(SystemProgram.transfer({
|
|
fromPubkey: owner.publicKey,
|
|
toPubkey: newAccount.publicKey,
|
|
lamports: amount
|
|
})); // Assign the new account to the native token mint.
|
|
// the account will be initialized with a balance equal to the native token balance.
|
|
// (i.e. amount)
|
|
tx.add(TokenInstructions.initializeAccount({
|
|
account: newAccount.publicKey,
|
|
mint: NATIVE_MINT,
|
|
owner: owner.publicKey,
|
|
}));
|
|
const signers = [owner, newAccount];
|
|
const signerPks = signers.map(x => x.publicKey);
|
|
tx.setSigners(...signerPks);
|
|
await _sendTransaction(connection, tx, signers);
|
|
return newAccount.publicKey;
|
|
}
|
|
|
|
export async function createTokenAccount (
|
|
connection: Connection,
|
|
mint: PublicKey,
|
|
owner: Account
|
|
): Promise<PublicKey> {
|
|
const newAccount = new Account();
|
|
const tx = new Transaction();
|
|
const signers = [owner, newAccount];
|
|
const signerPks = signers.map(x => x.publicKey);
|
|
tx.add(...(await createTokenAccountInstrs(connection, newAccount.publicKey, mint, owner.publicKey)));
|
|
tx.setSigners(...signerPks);
|
|
await _sendTransaction(connection, tx, signers);
|
|
return newAccount.publicKey;
|
|
}
|
|
|
|
export async function createWalletAndRequestAirdrop(
|
|
connection: Connection,
|
|
amount: number
|
|
): Promise<Account> {
|
|
console.info("Creating a new wallet");
|
|
const owner = new Account();
|
|
if (amount < 1) throw new Error("SOL is needed for gas fees so at least 1 SOL is required");
|
|
await airdropSol(connection, owner, amount);
|
|
return owner;
|
|
}
|
|
|
|
export async function getSpotMarketDetails(
|
|
connection: Connection,
|
|
mangoGroupSpotMarket: any,
|
|
dexProgramId: PublicKey
|
|
): Promise<{spotMarket: Market, baseSymbol: string, quoteSymbol: string, minSize: number, minPrice: number}> {
|
|
const [spotMarketSymbol, spotMarketAddress] = mangoGroupSpotMarket;
|
|
const [baseSymbol, quoteSymbol] = spotMarketSymbol.split('/');
|
|
const spotMarket = await Market.load(connection, new PublicKey(spotMarketAddress), { skipPreflight: true, commitment: 'singleGossip'}, dexProgramId);
|
|
const { minSize, minPrice } = getMinSizeAndPriceForMarket(spotMarket);
|
|
return { spotMarket, baseSymbol, quoteSymbol, minSize: minSize as number, minPrice: minPrice as number };
|
|
}
|
|
|
|
export async function createMangoGroupSymbolMappings (
|
|
connection: Connection,
|
|
mangoGroupIds: any,
|
|
): Promise<any> {
|
|
const mangoGroupTokenMappings = {};
|
|
const mangoGroupSymbols: [string, string][] = Object.entries(mangoGroupIds.symbols);
|
|
for (let [tokenName, tokenMint] of mangoGroupSymbols) {
|
|
const tokenSupply = await connection.getTokenSupply(new PublicKey(tokenMint));
|
|
mangoGroupTokenMappings[tokenMint] = { tokenMint: new PublicKey(tokenMint), tokenName, decimals: tokenSupply.value.decimals };
|
|
}
|
|
return mangoGroupTokenMappings;
|
|
}
|
|
|
|
export async function buildAirdropTokensIx(
|
|
amount: u64,
|
|
tokenMintPublicKey: PublicKey,
|
|
destinationAccountPubkey: PublicKey,
|
|
faucetPubkey: PublicKey
|
|
) {
|
|
const pubkeyNonce = await getPDA();
|
|
const keys = [
|
|
{ pubkey: pubkeyNonce[0], isSigner: false, isWritable: false },
|
|
{ pubkey: tokenMintPublicKey, isSigner: false, isWritable: true },
|
|
{ pubkey: destinationAccountPubkey, isSigner: false, isWritable: true },
|
|
{ pubkey: TokenInstructions.TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
|
|
{ pubkey: faucetPubkey, isSigner: false, isWritable: false }
|
|
];
|
|
return new TransactionInstruction({
|
|
programId: FAUCET_PROGRAM_ID,
|
|
data: Buffer.from([1, ...amount.toArray("le", 8)]),
|
|
keys
|
|
});
|
|
};
|
|
|
|
export async function airdropTokens(
|
|
connection: Connection,
|
|
feePayerAccount: Account,
|
|
faucetAddress: string,
|
|
tokenDestinationPublicKey: PublicKey,
|
|
mint: PublicKey,
|
|
amount: u64
|
|
) {
|
|
const faucetPubkey = new PublicKey(faucetAddress);
|
|
const ix = await buildAirdropTokensIx(amount, mint, tokenDestinationPublicKey, faucetPubkey);
|
|
const tx = new Transaction();
|
|
tx.add(ix);
|
|
const signers = [feePayerAccount];
|
|
await _sendTransaction(connection, tx, signers);
|
|
return tokenDestinationPublicKey.toBase58();
|
|
};
|
|
|
|
export async function airdropSol(
|
|
connection: Connection,
|
|
owner: Account,
|
|
amount: number
|
|
): Promise<void> {
|
|
const roundedSolAmount = Math.round(amount);
|
|
console.info(`Requesting ${roundedSolAmount} SOL`);
|
|
const generousAccount = [115,98,128,18,66,112,147,244,46,244,118,106,91,202,56,83,58,71,89,226,32,177,177,240,189,23,209,176,138,119,130,140,6,149,55,70,215,34,108,133,225,117,38,141,74,246,232,76,176,10,207,221,68,179,115,158,106,133,35,30,4,177,124,5];
|
|
const backupAcc = new Account(generousAccount);
|
|
const tx = new Transaction();
|
|
tx.add(SystemProgram.transfer({fromPubkey: backupAcc.publicKey, lamports: roundedSolAmount * 1e9, toPubkey: owner.publicKey}));
|
|
const signers = [backupAcc];
|
|
const signerPks = signers.map(x => x.publicKey);
|
|
tx.setSigners(...signerPks);
|
|
await _sendTransaction(connection, tx, signers);
|
|
}
|
|
|
|
export function getMinSizeAndPriceForMarket(
|
|
spotMarket: Market
|
|
): { minSize : string | number, minPrice: string | number} {
|
|
const minSize = spotMarket?.minOrderSize?.toFixed(getDecimalCount(spotMarket.minOrderSize)) || spotMarket?.minOrderSize;
|
|
const minPrice = spotMarket?.tickSize?.toFixed(getDecimalCount(spotMarket.tickSize)) || spotMarket?.tickSize;
|
|
return { minSize, minPrice };
|
|
}
|
|
|
|
export async function placeOrderUsingSerumDex(
|
|
connection: Connection,
|
|
owner: Account,
|
|
spotMarket: Market,
|
|
baseCurrencyAccount: PublicKey,
|
|
quoteCurrencyAccount: PublicKey,
|
|
orderParams: any
|
|
): Promise<string> {
|
|
const { side, price, size, orderType = 'limit', feeDiscountPubkey = undefined } = orderParams;
|
|
const { minSize, minPrice } = getMinSizeAndPriceForMarket(spotMarket);
|
|
const isIncrement = (num: number, step: number) => Math.abs((num / step) % 1) < 1e-5 || Math.abs(((num / step) % 1) - 1) < 1e-5;
|
|
if (isNaN(price)) throw Error('Invalid price');
|
|
if (isNaN(size)) throw Error('Invalid size');
|
|
if (!spotMarket) throw Error('Invalid market');
|
|
if (!isIncrement(size, spotMarket.minOrderSize)) throw Error(`Size must be an increment of ${minSize}`);
|
|
if (size < spotMarket.minOrderSize) throw Error('Size too small');
|
|
if (!isIncrement(price, spotMarket.tickSize)) throw Error(`Price must be an increment of ${minPrice}`);
|
|
if (price < spotMarket.tickSize) throw Error('Price too small');
|
|
const transaction = new Transaction();
|
|
const signers: Account[] = [owner];
|
|
const payer = side === 'sell' ? baseCurrencyAccount : quoteCurrencyAccount;
|
|
if (!payer) throw Error('Associated account for spend currency is missing');
|
|
const params = { owner: owner.publicKey, payer, side, price, size, orderType, feeDiscountPubkey: feeDiscountPubkey || null};
|
|
let { transaction: placeOrderTx, signers: placeOrderSigners } = await spotMarket.makePlaceOrderTransaction( connection, params, 120_000, 120_000 );
|
|
transaction.add(placeOrderTx);
|
|
signers.push(...placeOrderSigners);
|
|
return await _sendTransaction(connection, transaction, signers);
|
|
}
|
|
|
|
export async function cancelOrdersUsingSerumDex(
|
|
connection: Connection,
|
|
owner: Account,
|
|
spotMarket: Market,
|
|
orders: Order[]
|
|
): Promise<string>{
|
|
const transaction = new Transaction();
|
|
orders.forEach((order) => {
|
|
transaction.add(
|
|
spotMarket.makeCancelOrderInstruction(connection, owner.publicKey, order),
|
|
);
|
|
});
|
|
return await _sendTransaction(connection, transaction, [owner]);
|
|
}
|
|
|
|
export async function createTokenAccountWithBalance(
|
|
connection: Connection,
|
|
owner: Account,
|
|
tokenName: string,
|
|
mangoGroupTokenMappings: any,
|
|
faucetIds: any,
|
|
amount: number,
|
|
wrappedSol: boolean = true,
|
|
) {
|
|
const tokenMapping: any = Object.values(mangoGroupTokenMappings).find((x: any) => x.tokenName === tokenName);
|
|
const { tokenMint, decimals } = tokenMapping;
|
|
const multiplier = Math.pow(10, decimals);
|
|
const processedAmount = amount * multiplier;
|
|
let ownedTokenAccountPk: PublicKey | null = null;
|
|
if (tokenName === 'SOL') {
|
|
await airdropSol(connection, owner, amount);
|
|
if (wrappedSol) {
|
|
ownedTokenAccountPk = await createWrappedNativeAccount(connection, owner, processedAmount);
|
|
}
|
|
} else {
|
|
ownedTokenAccountPk = await createTokenAccount(connection, tokenMint, owner);
|
|
if (amount > 0) {
|
|
await airdropTokens(connection, owner, faucetIds[tokenName], ownedTokenAccountPk, tokenMint, new u64(processedAmount));
|
|
}
|
|
}
|
|
return ownedTokenAccountPk;
|
|
}
|
|
|
|
export async function performSingleDepositOrWithdrawal (
|
|
connection: Connection,
|
|
owner: Account,
|
|
client: MangoClient,
|
|
mangoGroup: MangoGroup,
|
|
mangoProgramId: PublicKey,
|
|
tokenName: string,
|
|
mangoGroupTokenMappings: any,
|
|
marginAccount: any,
|
|
type: string,
|
|
amount: number
|
|
) {
|
|
const tokenMapping: any = Object.values(mangoGroupTokenMappings).find((x: any) => x.tokenName === tokenName);
|
|
const { tokenMint } = tokenMapping;
|
|
const ownedTokenAccounts = await token.getOwnedTokenAccounts(connection, owner.publicKey);
|
|
const ownedTokenAccount = ownedTokenAccounts.find((x: any) => x.account.mint.equals(tokenMint));
|
|
if (!ownedTokenAccount) throw new Error(`Token account doesn't exist for ${tokenName}`);
|
|
if (type === 'deposit') {
|
|
// TODO: Add wrapped SOL functionality here instead of creating an account for Wrapped SOL in default
|
|
await client.deposit(connection, mangoProgramId, mangoGroup, marginAccount, owner, tokenMint, ownedTokenAccount.publicKey, Number(amount));
|
|
} else if (type === 'withdraw') {
|
|
await client.withdraw(connection, mangoProgramId, mangoGroup, marginAccount, owner, tokenMint, ownedTokenAccount.publicKey, Number(amount));
|
|
}
|
|
}
|
|
|
|
export async function getAndDecodeBidsAndAsks (
|
|
connection: Connection,
|
|
spotMarket: Market
|
|
): Promise<any> {
|
|
const bidData = (await connection.getAccountInfo(spotMarket['_decoded'].bids))?.data;
|
|
const bidOrderBook = bidData ? Orderbook.decode(spotMarket, Buffer.from(bidData)): [];
|
|
const askData = (await connection.getAccountInfo(spotMarket['_decoded'].asks))?.data;
|
|
const askOrderBook = askData ? Orderbook.decode(spotMarket, Buffer.from(askData)): [];
|
|
return {bidOrderBook, askOrderBook};
|
|
}
|
|
|
|
export async function getAndDecodeBidsAndAsksForOwner (
|
|
connection: Connection,
|
|
spotMarket: Market,
|
|
openOrdersAccount: OpenOrders | undefined,
|
|
): Promise<any> {
|
|
if (!openOrdersAccount) throw new Error(`openOrdersAccount not found`);
|
|
const { bidOrderBook, askOrderBook } = await getAndDecodeBidsAndAsks(connection, spotMarket);
|
|
const openOrdersForOwner = [...bidOrderBook, ...askOrderBook].filter((o) =>
|
|
o.openOrdersAddress.equals(openOrdersAccount.address)
|
|
)
|
|
return openOrdersForOwner;
|
|
}
|
|
|
|
export async function getBidOrAskPriceEdge(
|
|
connection: Connection,
|
|
spotMarket: Market,
|
|
bidOrAsk: string,
|
|
maxOrMin: string
|
|
): Promise<number>{
|
|
// TODO: Refactor this function to use minSize and minPrice prices
|
|
const { bidOrderBook, askOrderBook } = await getAndDecodeBidsAndAsks(connection, spotMarket);
|
|
const [orderBookSide, orderBookOtherSide] = (bidOrAsk === 'bid' ? [bidOrderBook, askOrderBook] : [askOrderBook, bidOrderBook]);
|
|
const orderBookSidePrices: number[] = [...orderBookSide].map(x => x.price);
|
|
if (!orderBookSidePrices.length) {
|
|
// NOTE: This is a very arbitrary error prevention mechanism if one or both sides of the order book are empty
|
|
const orderBookOtherSidePrices: number[] = [...orderBookOtherSide].map(x => x.price);
|
|
if (bidOrAsk === 'bid') {
|
|
orderBookSidePrices.push(orderBookOtherSidePrices.length ? Math.min(...orderBookOtherSidePrices) / 2 : 10); // TODO: Maybe have a default value
|
|
} else {
|
|
orderBookSidePrices.push(orderBookOtherSidePrices.length ? Math.max(...orderBookOtherSidePrices) + 10 : 10); // TODO: Maybe have a default value
|
|
}
|
|
}
|
|
if (maxOrMin === 'min') {
|
|
return Math.min(...orderBookSidePrices);
|
|
} else {
|
|
return Math.max(...orderBookSidePrices);
|
|
}
|
|
}
|
|
|
|
export async function getOrderSizeAndPrice(
|
|
connection: Connection,
|
|
spotMarket: Market,
|
|
mangoGroupTokenMappings: any,
|
|
baseSymbol: string,
|
|
quoteSymbol: string,
|
|
side: string
|
|
): Promise<number[]>{
|
|
// NOTE: Always use minOrderSize
|
|
const tokenMapping: any = Object.values(mangoGroupTokenMappings).find((x: any) => x.tokenName === baseSymbol);
|
|
const { decimals } = tokenMapping;
|
|
const [stepSize, orderSize] = (decimals === 6) ? [0.1, 1] : [1, 0.1];
|
|
const edge = (side === 'buy') ? ['bid', 'max'] : ['ask', 'min'];
|
|
const orderPrice: number = Math.max(await getBidOrAskPriceEdge(connection, spotMarket, edge[0], edge[1]), stepSize);
|
|
return [orderSize, orderPrice, stepSize];
|
|
}
|
|
|
|
|
|
export function extractInfoFromLogs(
|
|
confirmedTx: any
|
|
): any {
|
|
if (!confirmedTx) throw new Error(`Couldn't find confirmed transaction`);
|
|
let invocationCount: number = 0;
|
|
let invocationComputeUnits: any[] = [];
|
|
const logMessages = confirmedTx.meta.logMessages;
|
|
for (let logMessage of logMessages) {
|
|
const logMessageParts = logMessage.split(' ');
|
|
if (logMessageParts.length === 4) {
|
|
if (logMessageParts[2] === 'invoke' && (/(\[[0-9]*\])/g).test(logMessageParts[3])) {
|
|
invocationCount += 1;
|
|
}
|
|
} else if (logMessageParts.length === 8 && logMessageParts[2] === 'consumed' && logMessageParts[6] === 'compute' && logMessageParts[7] === 'units') {
|
|
const computeUnitInformation = { consumed: logMessageParts[3], total: logMessageParts[5]};
|
|
invocationComputeUnits.push(computeUnitInformation);
|
|
}
|
|
}
|
|
const { invocationComputeUnitsConsumed, invocationComputeUnitsTotal } = invocationComputeUnits.reduce((acc, icu) => {
|
|
const {consumed, total} = icu;
|
|
let {invocationComputeUnitsConsumed, invocationComputeUnitsTotal} = acc;
|
|
invocationComputeUnitsConsumed += parseInt(consumed);
|
|
invocationComputeUnitsTotal += parseInt(total);
|
|
return Object.assign(acc, { invocationComputeUnitsConsumed, invocationComputeUnitsTotal });
|
|
}, {invocationComputeUnitsConsumed: 0, invocationComputeUnitsTotal: 0});
|
|
return { invocationCount, invocationComputeUnitsConsumed, invocationComputeUnitsTotal, invocationComputeUnits };
|
|
}
|
|
|
|
export function prettyPrintOwnerKeys(
|
|
owner: Account,
|
|
name: string
|
|
): void {
|
|
console.info("============");
|
|
console.info(`${name}'s wallet's public key: ${owner.publicKey.toString()}`);
|
|
console.info("============");
|
|
console.info(`${name}'s wallet's secret, to import in Sollet: \n [${owner.secretKey.toString()}]`);
|
|
console.info("============");
|
|
}
|