diff --git a/Procfile b/Procfile index bdfc18d56..192e6aae3 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ -mm: node dist/cjs/scripts/mm/market-maker.js \ No newline at end of file +mm: node dist/cjs/scripts/mm/market-maker.js +keeper: node dist/cjs/scripts/keeper/keeper.js diff --git a/ts/client/scripts/archive/mb-admin.ts b/ts/client/scripts/archive/mb-admin.ts index 3658618e5..98775174e 100644 --- a/ts/client/scripts/archive/mb-admin.ts +++ b/ts/client/scripts/archive/mb-admin.ts @@ -1,9 +1,4 @@ import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; -import { - ASSOCIATED_TOKEN_PROGRAM_ID, - NATIVE_MINT, - TOKEN_PROGRAM_ID, -} from '../../src/utils/spl'; import { AddressLookupTableProgram, ComputeBudgetProgram, @@ -30,6 +25,11 @@ import { } from '../../src/clientIxParamBuilder'; import { MANGO_V4_ID, OPENBOOK_PROGRAM_ID } from '../../src/constants'; import { buildVersionedTx, toNative } from '../../src/utils'; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + NATIVE_MINT, + TOKEN_PROGRAM_ID, +} from '../../src/utils/spl'; const GROUP_NUM = Number(process.env.GROUP_NUM || 0); @@ -529,7 +529,7 @@ async function makeTokenReduceonly() { await client.tokenEdit( group, bank.mint, - Builder(NullTokenEditParams).reduceOnly(true).build(), + Builder(NullTokenEditParams).reduceOnly(0).build(), ); } diff --git a/ts/client/scripts/keeper/keeper.ts b/ts/client/scripts/keeper/keeper.ts new file mode 100644 index 000000000..c3c0ff1bb --- /dev/null +++ b/ts/client/scripts/keeper/keeper.ts @@ -0,0 +1,211 @@ +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(); diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 3cb100183..09a07ac0a 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -2496,8 +2496,19 @@ export class MangoClient { accounts: PublicKey[], limit: number, ): Promise { + return await this.sendAndConfirmTransactionForGroup(group, [ + await this.perpConsumeEventsIx(group, perpMarketIndex, accounts, limit), + ]); + } + + public async perpConsumeEventsIx( + group: Group, + perpMarketIndex: PerpMarketIndex, + accounts: PublicKey[], + limit: number, + ): Promise { const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); - const ix = await this.program.methods + return await this.program.methods .perpConsumeEvents(new BN(limit)) .accounts({ group: group.publicKey, @@ -2511,7 +2522,6 @@ export class MangoClient { ), ) .instruction(); - return await this.sendAndConfirmTransactionForGroup(group, [ix]); } public async perpConsumeAllEvents( @@ -2734,14 +2744,23 @@ export class MangoClient { ); } - public async updateIndexAndRate( + public async tokenUpdateIndexAndRate( group: Group, mintPk: PublicKey, ): Promise { + return await this.sendAndConfirmTransactionForGroup(group, [ + await this.tokenUpdateIndexAndRateIx(group, mintPk), + ]); + } + + public async tokenUpdateIndexAndRateIx( + group: Group, + mintPk: PublicKey, + ): Promise { const bank = group.getFirstBankByMint(mintPk); const mintInfo = group.mintInfosMapByMint.get(mintPk.toString())!; - const ix = await this.program.methods + return await this.program.methods .tokenUpdateIndexAndRate() .accounts({ group: group.publicKey, @@ -2757,7 +2776,6 @@ export class MangoClient { } as AccountMeta, ]) .instruction(); - return await this.sendAndConfirmTransactionForGroup(group, [ix]); } /// liquidations diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 69c1b91ee..7d8b91ab5 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -9,6 +9,8 @@ }, "include": [ "ts/client/src", - "ts/client/scripts" + "ts/client/scripts", + "ts/client/scripts/mm", + "ts/client/scripts/keeper" ] } \ No newline at end of file