import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor'; import { getFeeRates, getFeeTier, Market } from '@project-serum/serum'; import { Order } from '@project-serum/serum/lib/market'; import * as spl from '@solana/spl-token'; import { AccountMeta, Keypair, MemcmpFilter, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, TransactionSignature, } from '@solana/web3.js'; import bs58 from 'bs58'; import { Bank, getMintInfoForTokenIndex } from './accounts/bank'; import { Group } from './accounts/group'; import { I80F48 } from './accounts/I80F48'; import { MangoAccount } from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; import { OrderType, PerpMarket, Side } from './accounts/perp'; import { Serum3Market, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side, } from './accounts/serum3'; import { IDL, MangoV4 } from './mango_v4'; export const MANGO_V4_ID = new PublicKey( 'm43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD', ); export class MangoClient { constructor(public program: Program, public devnet?: boolean) {} /// public // Group public async createGroup(groupNum: number): Promise { const adminPk = (this.program.provider as AnchorProvider).wallet.publicKey; return await this.program.methods .createGroup(groupNum) .accounts({ admin: adminPk, payer: adminPk, }) .rpc(); } public async getGroup(groupPk: PublicKey): Promise { const groupAccount = await this.program.account.group.fetch(groupPk); const group = Group.from(groupPk, groupAccount); await group.reload(this); return group; } public async getGroupForAdmin( adminPk: PublicKey, groupNum?: number, ): Promise { const filters: MemcmpFilter[] = [ { memcmp: { bytes: adminPk.toBase58(), offset: 8, }, }, ]; if (groupNum) { const bbuf = Buffer.alloc(4); bbuf.writeUInt32LE(groupNum); filters.push({ memcmp: { bytes: bs58.encode(bbuf), offset: 44, }, }); } const groups = (await this.program.account.group.all(filters)).map( (tuple) => Group.from(tuple.publicKey, tuple.account), ); await groups[0].reload(this); return groups[0]; } // Tokens/Banks public async registerToken( group: Group, mintPk: PublicKey, oraclePk: PublicKey, tokenIndex: number, name: string, util0: number, rate0: number, util1: number, rate1: number, maxRate: number, loanFeeRate: number, loanOriginationFeeRate: number, maintAssetWeight: number, initAssetWeight: number, maintLiabWeight: number, initLiabWeight: number, liquidationFee: number, ): Promise { return await this.program.methods .registerToken( tokenIndex, name, { util0, rate0, util1, rate1, maxRate }, loanFeeRate, loanOriginationFeeRate, maintAssetWeight, initAssetWeight, maintLiabWeight, initLiabWeight, liquidationFee, ) .accounts({ group: group.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey, mint: mintPk, oracle: oraclePk, payer: (this.program.provider as AnchorProvider).wallet.publicKey, rent: SYSVAR_RENT_PUBKEY, }) .rpc(); } public async getBanksForGroup(group: Group): Promise { return ( await this.program.account.bank.all([ { memcmp: { bytes: group.publicKey.toBase58(), offset: 24, }, }, ]) ).map((tuple) => Bank.from(tuple.publicKey, tuple.account)); } // Stub Oracle public async createStubOracle( group: Group, mintPk: PublicKey, price: number, ): Promise { return await this.program.methods .createStubOracle({ val: I80F48.fromNumber(price).getData() }) .accounts({ group: group.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey, tokenMint: mintPk, payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) .rpc(); } public async setStubOracle( group: Group, mintPk: PublicKey, price: number, ): Promise { return await this.program.methods .setStubOracle({ val: I80F48.fromNumber(price).getData() }) .accounts({ group: group.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey, oracle: mintPk, payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) .rpc(); } public async getStubOracle( group: Group, mintPk: PublicKey, ): Promise { const stubOracles = ( await this.program.account.stubOracle.all([ { memcmp: { bytes: group.publicKey.toBase58(), offset: 8, }, }, { memcmp: { bytes: mintPk.toBase58(), offset: 40, }, }, ]) ).map((pa) => StubOracle.from(pa.publicKey, pa.account)); return stubOracles[0]; } // MangoAccount public async getOrCreateMangoAccount( group: Group, ownerPk: PublicKey, accountNumber?: number, name?: string, ): Promise { let mangoAccounts = await this.getMangoAccountForOwner(group, ownerPk); if (mangoAccounts.length === 0) { await this.createMangoAccount(group, accountNumber ?? 0, name ?? ''); mangoAccounts = await this.getMangoAccountForOwner(group, ownerPk); } return mangoAccounts[0]; } public async createMangoAccount( group: Group, accountNumber: number, name?: string, ): Promise { return await this.program.methods .createAccount(accountNumber, name ?? '') .accounts({ group: group.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) .rpc(); } public async getMangoAccount(mangoAccount: MangoAccount) { return MangoAccount.from( mangoAccount.publicKey, await this.program.account.mangoAccount.fetch(mangoAccount.publicKey), ); } public async getMangoAccountForOwner( group: Group, ownerPk: PublicKey, ): Promise { return ( await this.program.account.mangoAccount.all([ { memcmp: { bytes: group.publicKey.toBase58(), offset: 40, }, }, { memcmp: { bytes: ownerPk.toBase58(), offset: 72, }, }, ]) ).map((pa) => { return MangoAccount.from(pa.publicKey, pa.account); }); } public async closeMangoAccount( mangoAccount: MangoAccount, ): Promise { return await this.program.methods .closeAccount() .accounts({ account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, solDestination: (this.program.provider as AnchorProvider).wallet .publicKey, }) .rpc(); } public async deposit( group: Group, mangoAccount: MangoAccount, tokenName: string, amount: number, ) { const bank = group.banksMap.get(tokenName)!; const tokenAccountPk = await spl.getAssociatedTokenAddress( bank.mint, mangoAccount.owner, ); const healthRemainingAccounts: PublicKey[] = await this.buildHealthRemainingAccounts(group, mangoAccount, bank); return await this.program.methods .deposit(new BN(amount)) .accounts({ group: group.publicKey, account: mangoAccount.publicKey, bank: bank.publicKey, vault: bank.vault, tokenAccount: tokenAccountPk, tokenAuthority: (this.program.provider as AnchorProvider).wallet .publicKey, }) .remainingAccounts( healthRemainingAccounts.map( (pk) => ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), ), ) .rpc(); } public async withdraw( group: Group, mangoAccount: MangoAccount, tokenName: string, amount: number, allowBorrow: boolean, ) { const bank = group.banksMap.get(tokenName)!; const tokenAccountPk = await spl.getAssociatedTokenAddress( bank.mint, mangoAccount.owner, ); const healthRemainingAccounts: PublicKey[] = await this.buildHealthRemainingAccounts(group, mangoAccount, bank); return await this.program.methods .withdraw(new BN(amount), allowBorrow) .accounts({ group: group.publicKey, account: mangoAccount.publicKey, bank: bank.publicKey, vault: bank.vault, tokenAccount: tokenAccountPk, }) .remainingAccounts( healthRemainingAccounts.map( (pk) => ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), ), ) .rpc(); } // Serum public async serum3RegisterMarket( group: Group, serum3ProgramId: PublicKey, serum3MarketExternalPk: PublicKey, baseBank: Bank, quoteBank: Bank, marketIndex: number, name: string, ): Promise { return await this.program.methods .serum3RegisterMarket(marketIndex, name) .accounts({ group: group.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey, serumProgram: serum3ProgramId, serumMarketExternal: serum3MarketExternalPk, baseBank: baseBank.publicKey, quoteBank: quoteBank.publicKey, payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) .rpc(); } public async serum3GetMarket( group: Group, baseTokenIndex?: number, quoteTokenIndex?: number, ): Promise { const bumpfbuf = Buffer.alloc(1); bumpfbuf.writeUInt8(255); const filters: MemcmpFilter[] = [ { memcmp: { bytes: group.publicKey.toBase58(), offset: 24, }, }, ]; if (baseTokenIndex) { const bbuf = Buffer.alloc(2); bbuf.writeUInt16LE(baseTokenIndex); filters.push({ memcmp: { bytes: bs58.encode(bbuf), offset: 122, }, }); } if (quoteTokenIndex) { const qbuf = Buffer.alloc(2); qbuf.writeUInt16LE(quoteTokenIndex); filters.push({ memcmp: { bytes: bs58.encode(qbuf), offset: 124, }, }); } return (await this.program.account.serum3Market.all(filters)).map((tuple) => Serum3Market.from(tuple.publicKey, tuple.account), ); } public async serum3CreateOpenOrders( group: Group, mangoAccount: MangoAccount, marketName: string, ): Promise { const serum3Market: Serum3Market = group.serum3MarketsMap.get(marketName)!; return await this.program.methods .serum3CreateOpenOrders() .accounts({ group: group.publicKey, account: mangoAccount.publicKey, serumMarket: serum3Market.publicKey, serumProgram: serum3Market.serumProgram, serumMarketExternal: serum3Market.serumMarketExternal, owner: (this.program.provider as AnchorProvider).wallet.publicKey, payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) .rpc(); } public async serum3PlaceOrder( group: Group, mangoAccount: MangoAccount, serum3ProgramId: PublicKey, serum3MarketName: string, side: Serum3Side, price: number, size: number, selfTradeBehavior: Serum3SelfTradeBehavior, orderType: Serum3OrderType, clientOrderId: number, limit: number, ) { const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) { await this.serum3CreateOpenOrders(group, mangoAccount, 'BTC/USDC'); mangoAccount = await this.getMangoAccount(mangoAccount); } const serum3MarketExternal = await Market.load( this.program.provider.connection, serum3Market.serumMarketExternal, { commitment: this.program.provider.connection.commitment }, serum3ProgramId, ); const serum3MarketExternalVaultSigner = await PublicKey.createProgramAddress( [ serum3Market.serumMarketExternal.toBuffer(), serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike( Buffer, 'le', 8, ), ], serum3ProgramId, ); const healthRemainingAccounts: PublicKey[] = await this.buildHealthRemainingAccounts(group, mangoAccount); const limitPrice = serum3MarketExternal.priceNumberToLots(price); const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size); const feeTier = getFeeTier(0, 0 /** TODO: fix msrm/srm balance */); const rates = getFeeRates(feeTier); const maxQuoteQuantity = new BN( serum3MarketExternal.decoded.quoteLotSize.toNumber() * (1 + rates.taker) /** TODO: fix taker/maker */, ).mul( serum3MarketExternal .baseSizeNumberToLots(size) .mul(serum3MarketExternal.priceNumberToLots(price)), ); return await this.program.methods .serum3PlaceOrder( side, limitPrice, maxBaseQuantity, maxQuoteQuantity, selfTradeBehavior, orderType, new BN(clientOrderId), limit, ) .accounts({ group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex) ?.openOrders, serumMarket: serum3Market.publicKey, serumProgram: serum3ProgramId, serumMarketExternal: serum3Market.serumMarketExternal, marketBids: serum3MarketExternal.bidsAddress, marketAsks: serum3MarketExternal.asksAddress, marketEventQueue: serum3MarketExternal.decoded.eventQueue, marketRequestQueue: serum3MarketExternal.decoded.requestQueue, marketBaseVault: serum3MarketExternal.decoded.baseVault, marketQuoteVault: serum3MarketExternal.decoded.quoteVault, marketVaultSigner: serum3MarketExternalVaultSigner, quoteBank: group.findBank(serum3Market.quoteTokenIndex)?.publicKey, quoteVault: group.findBank(serum3Market.quoteTokenIndex)?.vault, baseBank: group.findBank(serum3Market.baseTokenIndex)?.publicKey, baseVault: group.findBank(serum3Market.baseTokenIndex)?.vault, }) .remainingAccounts( healthRemainingAccounts.map( (pk) => ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), ), ) .rpc(); } async serum3SettleFunds( group: Group, mangoAccount: MangoAccount, serum3ProgramId: PublicKey, serum3MarketName: string, ): Promise { const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; const serum3MarketExternal = await Market.load( this.program.provider.connection, serum3Market.serumMarketExternal, { commitment: this.program.provider.connection.commitment }, serum3ProgramId, ); const serum3MarketExternalVaultSigner = // TODO: put into a helper method, and remove copy pasta await PublicKey.createProgramAddress( [ serum3Market.serumMarketExternal.toBuffer(), serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike( Buffer, 'le', 8, ), ], serum3ProgramId, ); return await this.program.methods .serum3SettleFunds() .accounts({ group: group.publicKey, account: mangoAccount.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey, openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex) ?.openOrders, serumMarket: serum3Market.publicKey, serumProgram: serum3ProgramId, serumMarketExternal: serum3Market.serumMarketExternal, marketBaseVault: serum3MarketExternal.decoded.baseVault, marketQuoteVault: serum3MarketExternal.decoded.quoteVault, marketVaultSigner: serum3MarketExternalVaultSigner, quoteBank: group.findBank(serum3Market.quoteTokenIndex)?.publicKey, quoteVault: group.findBank(serum3Market.quoteTokenIndex)?.vault, baseBank: group.findBank(serum3Market.baseTokenIndex)?.publicKey, baseVault: group.findBank(serum3Market.baseTokenIndex)?.vault, }) .rpc(); } async serum3CancelOrder( group: Group, mangoAccount: MangoAccount, serum3ProgramId: PublicKey, serum3MarketName: string, side: Serum3Side, orderId: BN, ): Promise { const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; const serum3MarketExternal = await Market.load( this.program.provider.connection, serum3Market.serumMarketExternal, { commitment: this.program.provider.connection.commitment }, serum3ProgramId, ); return await this.program.methods .serum3CancelOrder(side, orderId) .accounts({ group: group.publicKey, account: mangoAccount.publicKey, openOrders: mangoAccount.findSerum3Account(serum3Market.marketIndex) ?.openOrders, serumMarket: serum3Market.publicKey, serumProgram: serum3ProgramId, serumMarketExternal: serum3Market.serumMarketExternal, marketBids: serum3MarketExternal.bidsAddress, marketAsks: serum3MarketExternal.asksAddress, marketEventQueue: serum3MarketExternal.decoded.eventQueue, }) .rpc(); } async getSerum3Orders( group: Group, serum3ProgramId: PublicKey, serum3MarketName: string, ): Promise { const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; const serum3MarketExternal = await Market.load( this.program.provider.connection, serum3Market.serumMarketExternal, { commitment: this.program.provider.connection.commitment }, serum3ProgramId, ); // TODO: filter for mango account return await serum3MarketExternal.loadOrdersForOwner( this.program.provider.connection, group.publicKey, ); } /// perps async perpCreateMarket( group: Group, oraclePk: PublicKey, perpMarketIndex: number, name: string, baseTokenIndex: number, quoteTokenIndex: number, quoteLotSize: number, baseLotSize: number, maintAssetWeight: number, initAssetWeight: number, maintLiabWeight: number, initLiabWeight: number, liquidationFee: number, makerFee: number, takerFee: number, minFunding: number, maxFunding: number, impactQuantity: number, ): Promise { const bids = new Keypair(); const asks = new Keypair(); const eventQueue = new Keypair(); console.log( (this.program.provider as AnchorProvider).wallet.publicKey.toBase58(), ); return await this.program.methods .perpCreateMarket( perpMarketIndex, name, baseTokenIndex, quoteTokenIndex, new BN(quoteLotSize), new BN(baseLotSize), maintAssetWeight, initAssetWeight, maintLiabWeight, initLiabWeight, liquidationFee, makerFee, takerFee, minFunding, maxFunding, new BN(impactQuantity), ) .accounts({ group: group.publicKey, admin: (this.program.provider as AnchorProvider).wallet.publicKey, oracle: oraclePk, bids: bids.publicKey, asks: asks.publicKey, eventQueue: eventQueue.publicKey, payer: (this.program.provider as AnchorProvider).wallet.publicKey, }) .preInstructions([ SystemProgram.createAccount({ programId: this.program.programId, space: 8 + 90152, lamports: await this.program.provider.connection.getMinimumBalanceForRentExemption( 90160, ), fromPubkey: (this.program.provider as AnchorProvider).wallet .publicKey, newAccountPubkey: bids.publicKey, }), SystemProgram.createAccount({ programId: this.program.programId, space: 8 + 90152, lamports: await this.program.provider.connection.getMinimumBalanceForRentExemption( 90160, ), fromPubkey: (this.program.provider as AnchorProvider).wallet .publicKey, newAccountPubkey: asks.publicKey, }), SystemProgram.createAccount({ programId: this.program.programId, space: 8 + 102424, lamports: await this.program.provider.connection.getMinimumBalanceForRentExemption( 102432, ), fromPubkey: (this.program.provider as AnchorProvider).wallet .publicKey, newAccountPubkey: eventQueue.publicKey, }), ]) .signers([bids, asks, eventQueue]) .rpc(); } public async perpGetMarket( group: Group, baseTokenIndex?: number, quoteTokenIndex?: number, ): Promise { const bumpfbuf = Buffer.alloc(1); bumpfbuf.writeUInt8(255); const filters: MemcmpFilter[] = [ { memcmp: { bytes: group.publicKey.toBase58(), offset: 24, }, }, ]; if (baseTokenIndex) { const bbuf = Buffer.alloc(2); bbuf.writeUInt16LE(baseTokenIndex); filters.push({ memcmp: { bytes: bs58.encode(bbuf), offset: 428, }, }); } if (quoteTokenIndex) { const qbuf = Buffer.alloc(2); qbuf.writeUInt16LE(quoteTokenIndex); filters.push({ memcmp: { bytes: bs58.encode(qbuf), offset: 430, }, }); } return (await this.program.account.perpMarket.all(filters)).map((tuple) => PerpMarket.from(tuple.publicKey, tuple.account), ); } async perpPlaceOrder( group: Group, mangoAccount: MangoAccount, perpMarketName: string, side: Side, priceLots: number, maxBaseLots: number, maxQuoteLots: number, clientOrderId: number, orderType: OrderType, expiryTimestamp: number, limit: number, ) { const perpMarket = group.perpMarketsMap.get(perpMarketName)!; await this.program.methods .perpPlaceOrder( side, new BN((priceLots * perpMarket.baseLotSize) / perpMarket.quoteLotSize), new BN(maxBaseLots), new BN(maxQuoteLots), new BN(clientOrderId), orderType, new BN(expiryTimestamp), limit, ) .accounts({ group: group.publicKey, account: mangoAccount.publicKey, perpMarket: perpMarket.publicKey, asks: perpMarket.asks, bids: perpMarket.bids, eventQueue: perpMarket.eventQueue, oracle: perpMarket.oracle, owner: (this.program.provider as AnchorProvider).wallet.publicKey, }) .rpc(); } /// static static connect(provider?: Provider, devnet?: boolean): MangoClient { // TODO: use IDL on chain or in repository? decide... // Alternatively we could fetch IDL from chain. // const idl = await Program.fetchIdl(MANGO_V4_ID, provider); let idl = IDL; return new MangoClient( new Program(idl as MangoV4, MANGO_V4_ID, provider), devnet, ); } /// private private async buildHealthRemainingAccounts( group: Group, mangoAccount: MangoAccount, bank?: Bank /** TODO for serum3PlaceOrde we are just ingoring this atm */, ) { const healthRemainingAccounts: PublicKey[] = []; const tokenIndices = mangoAccount.tokens .filter((token) => token.tokenIndex !== 65535) .map((token) => token.tokenIndex); if (bank) { tokenIndices.push(bank.tokenIndex); } const mintInfos = await Promise.all( [...new Set(tokenIndices)].map(async (tokenIndex) => getMintInfoForTokenIndex(this, group.publicKey, tokenIndex), ), ); healthRemainingAccounts.push( ...mintInfos.flatMap((mintinfos) => { return mintinfos.flatMap((mintinfo) => { return mintinfo.bank; }); }), ); healthRemainingAccounts.push( ...mintInfos.flatMap((mintinfos) => { return mintinfos.flatMap((mintinfo) => { return mintinfo.oracle; }); }), ); healthRemainingAccounts.push( ...mangoAccount.serum3 .filter((serum3Account) => serum3Account.marketIndex !== 65535) .map((serum3Account) => serum3Account.openOrders), ); healthRemainingAccounts.push( ...mangoAccount.perps .filter((perp) => perp.marketIndex !== 65535) .map( (perp) => Array.from(group.perpMarketsMap.values()).filter( (perpMarket) => perpMarket.perpMarketIndex === perp.marketIndex, )[0].publicKey, ), ); return healthRemainingAccounts; } }