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 { 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 { 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 { // 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 { 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 { 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 { 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 { 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 { 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{ 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 { 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 { 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{ // 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{ // 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("============"); }