import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; import chunk from 'lodash/chunk'; import range from 'lodash/range'; import { Group } from '../../src/accounts/group'; import { FillEvent, OutEvent, PerpEventQueue } from '../../src/accounts/perp'; import { MangoClient } from '../../src/client'; import { MANGO_V4_ID } from '../../src/constants'; import { sendTransaction } from '../../src/utils/rpc'; // Env vars const CLUSTER: Cluster = (process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta'; const CLUSTER_URL = process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; const USER_KEYPAIR = process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || ''; const INTERVAL_UPDATE_BANKS = Number(process.env.INTERVAL_UPDATE_BANKS || 60); const INTERVAL_CONSUME_EVENTS = Number( process.env.INTERVAL_CONSUME_EVENTS || 5, ); const INTERVAL_UPDATE_FUNDING = Number( process.env.INTERVAL_UPDATE_FUNDING || 5, ); const INTERVAL_CHECK_NEW_LISTINGS_AND_ABORT = Number( process.env.INTERVAL_CHECK_NEW_LISTINGS_AND_ABORT || 120, ); async function updateBanks(client: MangoClient, group: Group): Promise { console.log('Starting updateBanks loop'); // eslint-disable-next-line no-constant-condition while (true) { const tokenIndices = Array.from(group.banksMapByTokenIndex.keys()); const tokenIndicesByChunks = chunk(tokenIndices, 10); tokenIndicesByChunks.map(async (tokenIndices) => { const ixs = await Promise.all( tokenIndices.map((ti) => client.tokenUpdateIndexAndRateIx( group, group.getFirstBankByTokenIndex(ti).mint, ), ), ); try { const sig = await sendTransaction( client.program.provider as AnchorProvider, ixs, group.addressLookupTablesList, { prioritizationFee: true }, ); console.log( ` - Token update index and rate success, tokenIndices - ${tokenIndices}, sig https://explorer.solana.com/tx/${sig}`, ); } catch (e) { console.log( ` - Token update index and rate error, tokenIndices - ${tokenIndices}, e - ${e}`, ); } }); await new Promise((r) => setTimeout(r, INTERVAL_UPDATE_BANKS * 1000)); } } async function consumeEvents(client: MangoClient, group: Group): Promise { console.log('Starting consumeEvents loop'); // eslint-disable-next-line no-constant-condition while (true) { const perpMarketIndices = Array.from( group.perpMarketsMapByMarketIndex.keys(), ); for (const perpMarketIndex of perpMarketIndices) { for (const unused of range(0, 10)) { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const eq = await perpMarket.loadEventQueue(client); const events = eq.getUnconsumedEvents().slice(0, 10); const accounts: Set = new Set(); for (const event of events) { if (event.eventType === PerpEventQueue.FILL_EVENT_TYPE) { accounts.add((event as FillEvent).maker); accounts.add((event as FillEvent).taker); } else if (event.eventType === PerpEventQueue.OUT_EVENT_TYPE) { accounts.add((event as OutEvent).owner); } else if (event.eventType === PerpEventQueue.LIQUIDATE_EVENT_TYPE) { // pass } } try { const sig = await sendTransaction( client.program.provider as AnchorProvider, [ await client.perpConsumeEventsIx( group, perpMarketIndex, Array.from(accounts), 10, ), ], group.addressLookupTablesList, { prioritizationFee: true }, ); console.log( ` - Consume events success, perpMarketIndex - ${perpMarketIndex}, sig https://explorer.solana.com/tx/${sig}`, ); } catch (e) { console.log( ` - Consume events error, perpMarketIndex - ${perpMarketIndex}, e - ${e}`, ); } } } await new Promise((r) => setTimeout(r, INTERVAL_CONSUME_EVENTS * 1000)); } } async function updateFunding(client: MangoClient, group: Group): Promise { console.log('Starting updateFunding loop'); // eslint-disable-next-line no-constant-condition while (true) { const perpMarketIndices = Array.from( group.perpMarketsMapByMarketIndex.keys(), ); for (const perpMarketIndex of perpMarketIndices) { try { const sig = await sendTransaction( client.program.provider as AnchorProvider, [ await client.perpUpdateFundingIx( group, group.getPerpMarketByMarketIndex(perpMarketIndex), ), ], group.addressLookupTablesList, { prioritizationFee: true }, ); console.log( ` - Update funding success, perpMarketIndex - ${perpMarketIndex}, sig https://explorer.solana.com/tx/${sig}`, ); } catch (e) { console.log( ` - Update funding error, perpMarketIndex - ${perpMarketIndex}, e - ${e}`, ); } } await new Promise((r) => setTimeout(r, INTERVAL_UPDATE_FUNDING * 1000)); } } async function checkNewListingsAndAbort( client: MangoClient, group: Group, ): Promise { console.log('Starting checkNewListingsAndAbort loop'); // eslint-disable-next-line no-constant-condition while (true) { const freshlyFetchedGroup = await client.getGroup(group.publicKey); if ( freshlyFetchedGroup.banksMapByTokenIndex.size != group.banksMapByTokenIndex.size || freshlyFetchedGroup.perpMarketsMapByMarketIndex.size != group.perpMarketsMapByMarketIndex.size ) { process.exit(); } await new Promise((r) => setTimeout(r, INTERVAL_CHECK_NEW_LISTINGS_AND_ABORT * 1000), ); } } async function keeper(): Promise { // Load client const options = AnchorProvider.defaultOptions(); const connection = new Connection(CLUSTER_URL!, options); const user = Keypair.fromSecretKey( Buffer.from( JSON.parse( process.env.KEYPAIR || fs.readFileSync(USER_KEYPAIR!, 'utf-8'), ), ), ); const userWallet = new Wallet(user); const userProvider = new AnchorProvider(connection, userWallet, options); const client = await MangoClient.connect( userProvider, CLUSTER, MANGO_V4_ID[CLUSTER], { idsSource: 'get-program-accounts', }, ); const mangoAccount = await client.getMangoAccount( new PublicKey(MANGO_ACCOUNT_PK), ); const group = await client.getGroup(mangoAccount.group); await group.reloadAll(client); updateBanks(client, group); consumeEvents(client, group); updateFunding(client, group); checkNewListingsAndAbort(client, group); } keeper();