diff --git a/src/client.ts b/src/client.ts index db4e837..69fc597 100644 --- a/src/client.ts +++ b/src/client.ts @@ -39,7 +39,11 @@ import { getFeeRates, getFeeTier, Market, OpenOrders, Orderbook } from '@project import { SRM_DECIMALS } from '@project-serum/serum/lib/token-instructions'; import { Order } from '@project-serum/serum/lib/market'; import Wallet from '@project-serum/sol-wallet-adapter'; -import { makeCancelOrderInstruction, makeSettleFundsInstruction } from './instruction'; +import { + makeCancelOrderInstruction, + makeForceCancelOrdersInstruction, makePartialLiquidateInstruction, + makeSettleFundsInstruction, +} from './instruction'; import { Aggregator } from './schema'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; @@ -164,6 +168,8 @@ export class MarginAccount { owner!: PublicKey; deposits!: number[]; borrows!: number[]; + beingLiquidated!: boolean; + openOrders!: PublicKey[]; openOrdersAccounts: (OpenOrders | undefined)[] // undefined if an openOrdersAccount not yet initialized and has zeroKey // TODO keep updated with websocket @@ -188,8 +194,6 @@ export class MarginAccount { return nativeToUi(this.getNativeBorrow(mangoGroup, tokenIndex), mangoGroup.mintDecimals[tokenIndex]) } - - async loadOpenOrders( connection: Connection, dexProgramId: PublicKey @@ -399,6 +403,7 @@ export class MangoClient { const signers = [payer].concat(additionalSigners) transaction.sign(...signers) const rawTransaction = transaction.serialize() + console.log('Transaction size:', rawTransaction.length) const startTime = getUnixTs(); const txid: TransactionSignature = await connection.sendRawTransaction( @@ -772,10 +777,88 @@ export class MangoClient { ...tokenAccs.map( (pubkey) => ( { isSigner: false, isWritable: true, pubkey })), ] const data = encodeMangoInstruction({Liquidate: {depositQuantities: depositsBN}}) - - const instruction = new TransactionInstruction( { keys, data, programId }) + const transaction = new Transaction() + transaction.add(instruction) + const additionalSigners = [] + return await this.sendTransaction(connection, transaction, liqor, additionalSigners) + } + + async forceCancelOrders( + connection: Connection, + programId: PublicKey, + mangoGroup: MangoGroup, + liqeeMarginAccount: MarginAccount, + liqor: Account, + spotMarket: Market, + limit: number + ): Promise { + + const limitBn = new BN(limit) + const marketIndex = mangoGroup.getMarketIndex(spotMarket) + const dexSigner = await PublicKey.createProgramAddress( + [ + spotMarket.publicKey.toBuffer(), + spotMarket['_decoded'].vaultSignerNonce.toArrayLike(Buffer, 'le', 8) + ], + spotMarket.programId + ) + + const instruction = makeForceCancelOrdersInstruction( + programId, + mangoGroup.publicKey, + liqor.publicKey, + liqeeMarginAccount.publicKey, + mangoGroup.vaults[marketIndex], + mangoGroup.vaults[NUM_TOKENS-1], + spotMarket.publicKey, + spotMarket.bidsAddress, + spotMarket.asksAddress, + mangoGroup.signerKey, + spotMarket['_decoded'].eventQueue, + spotMarket['_decoded'].baseVault, + spotMarket['_decoded'].quoteVault, + dexSigner, + mangoGroup.dexProgramId, + liqeeMarginAccount.openOrders, + mangoGroup.oracles, + limitBn + ) + + const transaction = new Transaction() + transaction.add(instruction) + const additionalSigners = [] + return await this.sendTransaction(connection, transaction, liqor, additionalSigners) + } + + async partialLiquidate( + connection: Connection, + programId: PublicKey, + mangoGroup: MangoGroup, + liqeeMarginAccount: MarginAccount, + liqor: Account, + liqorInTokenWallet: PublicKey, + liqorOutTokenWallet: PublicKey, + inTokenIndex: number, + outTokenIndex: number, + maxDeposit: number + ): Promise { + const maxDepositBn: BN = uiToNative(maxDeposit, mangoGroup.mintDecimals[inTokenIndex]) + const instruction = makePartialLiquidateInstruction( + programId, + mangoGroup.publicKey, + liqor.publicKey, + liqorInTokenWallet, + liqorOutTokenWallet, + liqeeMarginAccount.publicKey, + mangoGroup.vaults[inTokenIndex], + mangoGroup.vaults[outTokenIndex], + mangoGroup.signerKey, + liqeeMarginAccount.openOrders, + mangoGroup.oracles, + maxDepositBn + ) const transaction = new Transaction() transaction.add(instruction) const additionalSigners = [] diff --git a/src/index.ts b/src/index.ts index f78315a..f8e2387 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,141 +1,87 @@ -import fs from 'fs'; -import { homedir } from 'os'; import { MangoClient, MangoGroup } from './client'; import IDS from './ids.json'; -import { Connection, PublicKey, Account } from '@solana/web3.js'; -import { nativeToUi } from './utils'; +import { Account, Connection, PublicKey } from '@solana/web3.js'; + +export { MangoClient, MangoGroup, MarginAccount, tokenToDecimals } from './client'; +export { MangoIndexLayout, MarginAccountLayout, MangoGroupLayout } from './layout'; +export * from './layout'; +export * from './utils' + +export { IDS } + +import { homedir } from 'os' +import * as fs from 'fs'; +import { Aggregator } from './schema'; +import { nativeToUi, sleep, uiToNative } from './utils'; import { NUM_MARKETS, NUM_TOKENS } from './layout'; -export { - MangoClient, - MangoGroup, - MarginAccount, - tokenToDecimals, -} from './client'; -export { - MangoIndexLayout, - MarginAccountLayout, - MangoGroupLayout, -} from './layout'; -export * from './layout'; -export * from './utils'; - -export { IDS }; // async function tests() { -// const cluster = 'devnet'; +// const cluster = "mainnet-beta"; // const client = new MangoClient(); -// const clusterIds = IDS[cluster]; - -// const connection = new Connection(IDS.cluster_urls[cluster], 'singleGossip'); -// const mangoGroupPk = new PublicKey( -// clusterIds.mango_groups['BTC_ETH_USDT'].mango_group_pk, -// ); +// const clusterIds = IDS[cluster] +// +// const connection = new Connection(IDS.cluster_urls[cluster], 'singleGossip') +// const mangoGroupPk = new PublicKey(clusterIds.mango_groups['BTC_ETH_USDT'].mango_group_pk); // const mangoProgramId = new PublicKey(clusterIds.mango_program_id); - -// const keyPairPath = -// process.env.KEYPAIR || homedir() + '/.config/solana/id.json'; -// const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8'))); - -// async function testSolink() { - -// const oraclePk = new PublicKey(IDS[cluster].oracles['ETH/USDT']) -// const agg = await Aggregator.loadWithConnection(oraclePk, connection) - -// // const agg = await Aggregator.loadWithConnection(oraclePk, connection) -// console.log(agg.answer.median.toNumber(), agg.answer.updatedAt.toNumber(), agg.round.id.toNumber()) - -// } - -// async function testDepositSrm() { -// const srmVaultPk = new PublicKey(clusterIds['mango_groups']['BTC_ETH_USDT']['srm_vault_pk']) -// const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk, srmVaultPk) -// const srmAccountPk = new PublicKey("6utvndL8EEjpwK5QVtguErncQEPVbkuyABmXu6FeygeV") -// const mangoSrmAccountPk = await client.depositSrm(connection, mangoProgramId, mangoGroup, payer, srmAccountPk, 100) -// console.log(mangoSrmAccountPk.toBase58()) -// await sleep(2000) -// const mangoSrmAccount = await client.getMangoSrmAccount(connection, mangoSrmAccountPk) -// const txid = await client.withdrawSrm(connection, mangoProgramId, mangoGroup, mangoSrmAccount, payer, srmAccountPk, 50) -// console.log('success', txid) -// } - -// async function getMarginAccountDetails() { -// const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); -// const marginAccountPk = new PublicKey( -// '5pmKq6D67vdUam6KMoMNa1euaqDCScqCehtcTyhC4Koh', -// ); - -// console.log('ma:', mangoGroup.dexProgramId.toString()); -// const marginAccount = await client.getMarginAccount( -// connection, -// marginAccountPk, -// mangoGroup.dexProgramId, -// ); -// const prices = await mangoGroup.getPrices(connection); - -// console.log(marginAccount.toPrettyString(mangoGroup, prices)); - -// for (let i = 0; i < NUM_TOKENS; i++) { -// console.log( -// i, -// marginAccount.getUiDeposit(mangoGroup, i), -// marginAccount.getUiBorrow(mangoGroup, i), -// ); +// +// const keyPairPath = process.env.KEYPAIR || homedir() + '/.config/solana/id.json' +// const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8'))) +// +// +// async function testSolink() { +// +// const oraclePk = new PublicKey(IDS[cluster].oracles['ETH/USDT']) +// const agg = await Aggregator.loadWithConnection(oraclePk, connection) +// +// // const agg = await Aggregator.loadWithConnection(oraclePk, connection) +// console.log(agg.answer.median.toNumber(), agg.answer.updatedAt.toNumber(), agg.round.id.toNumber()) +// // } -// for (let i = 0; i < NUM_MARKETS; i++) { -// let openOrdersAccount = marginAccount.openOrdersAccounts[i]; -// if (openOrdersAccount === undefined) { -// continue; +// +// async function testDepositSrm() { +// const srmVaultPk = new PublicKey(clusterIds['mango_groups']['BTC_ETH_USDT']['srm_vault_pk']) +// const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk, srmVaultPk) +// const srmAccountPk = new PublicKey("6utvndL8EEjpwK5QVtguErncQEPVbkuyABmXu6FeygeV") +// const mangoSrmAccountPk = await client.depositSrm(connection, mangoProgramId, mangoGroup, payer, srmAccountPk, 100) +// console.log(mangoSrmAccountPk.toBase58()) +// await sleep(2000) +// const mangoSrmAccount = await client.getMangoSrmAccount(connection, mangoSrmAccountPk) +// const txid = await client.withdrawSrm(connection, mangoProgramId, mangoGroup, mangoSrmAccount, payer, srmAccountPk, 50) +// console.log('success', txid) +// } +// +// async function getMarginAccountDetails() { +// const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); +// const marginAccountPk = new PublicKey("BSFaizvArm1dpVGwJvrsqbWTpT8nh3xD7ERrdHuaY1C1") +// const marginAccount = await client.getMarginAccount(connection, marginAccountPk, mangoGroup.dexProgramId) +// const prices = await mangoGroup.getPrices(connection) +// +// console.log(marginAccount.toPrettyString(mangoGroup, prices)) +// +// for (let i = 0; i < NUM_MARKETS; i++) { +// let openOrdersAccount = marginAccount.openOrdersAccounts[i] +// if (openOrdersAccount === undefined) { +// continue +// } +// +// for (const oid of openOrdersAccount.orders) { +// console.log(oid.toString()) +// } +// console.log(i, +// nativeToUi(openOrdersAccount.quoteTokenTotal.toNumber(), mangoGroup.mintDecimals[NUM_MARKETS]), +// nativeToUi(openOrdersAccount.quoteTokenFree.toNumber(), mangoGroup.mintDecimals[NUM_MARKETS]), +// +// nativeToUi(openOrdersAccount.baseTokenTotal.toNumber(), mangoGroup.mintDecimals[i]), +// nativeToUi(openOrdersAccount.baseTokenFree.toNumber(), mangoGroup.mintDecimals[i]) +// +// ) // } - -// console.log( -// i, -// nativeToUi( -// openOrdersAccount.quoteTokenTotal.toNumber(), -// mangoGroup.mintDecimals[NUM_MARKETS], -// ), -// nativeToUi( -// openOrdersAccount.baseTokenTotal.toNumber(), -// mangoGroup.mintDecimals[i], -// ), -// ); +// // } +// await getMarginAccountDetails() +// // await testSolink() +// // testDepositSrm() // } - -// async function testDeposit() { -// const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); -// const marginAccountPk = new PublicKey( -// '5pmKq6D67vdUam6KMoMNa1euaqDCScqCehtcTyhC4Koh', -// ); - -// const marginAccount = await client.getMarginAccount( -// connection, -// marginAccountPk, -// mangoGroup.dexProgramId, -// ); - -// console.log('starting deposit'); -// try { -// await client.deposit( -// connection, -// mangoProgramId, -// mangoGroup, -// marginAccount, -// payer, -// new PublicKey('7KBVenLz5WNH4PA5MdGkJNpDDyNKnBQTwnz1UqJv9GUm'), -// new PublicKey('FNYRWcdcJn1YjWstQ7SGmSizgmERECfoknwk2cf1sQMe'), -// 1000, -// ); -// } catch (e) { -// console.log('deposit error:', e); -// } -// console.log('deposit complete'); -// } - -// await testDeposit(); -// await getMarginAccountDetails(); -// await testSolink() -// testDepositSrm() -// } - -// tests(); +// +// tests() \ No newline at end of file diff --git a/src/instruction.ts b/src/instruction.ts index 162e6ff..8fd29b8 100644 --- a/src/instruction.ts +++ b/src/instruction.ts @@ -5,7 +5,7 @@ import { TransactionInstruction, } from '@solana/web3.js'; import { Order } from '@project-serum/serum/lib/market'; -import { encodeMangoInstruction } from './layout'; +import { encodeMangoInstruction, NUM_TOKENS } from './layout'; import { TOKEN_PROGRAM_ID } from '@project-serum/serum/lib/token-instructions'; import { uiToNative } from './utils'; @@ -134,3 +134,82 @@ export function makeSettleBorrowInstruction( }); return new TransactionInstruction({ keys, data, programId }); } + +export function makeForceCancelOrdersInstruction( + programId: PublicKey, + mangoGroup: PublicKey, + liqor: PublicKey, + liqeeMarginAccount: PublicKey, + baseVault: PublicKey, + quoteVault: PublicKey, + spotMarket: PublicKey, + bids: PublicKey, + asks: PublicKey, + signerKey: PublicKey, + dexEventQueue: PublicKey, + dexBaseVault: PublicKey, + dexQuoteVault: PublicKey, + dexSigner: PublicKey, + dexProgramId: PublicKey, + openOrders: PublicKey[], + oracles: PublicKey[], + limit: BN +): TransactionInstruction { + + const keys = [ + { isSigner: false, isWritable: true, pubkey: mangoGroup}, + { isSigner: true, isWritable: false, pubkey: liqor }, + { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount }, + { isSigner: false, isWritable: true, pubkey: baseVault }, + { isSigner: false, isWritable: true, pubkey: quoteVault }, + { isSigner: false, isWritable: true, pubkey: spotMarket }, + { isSigner: false, isWritable: true, pubkey: bids }, + { isSigner: false, isWritable: true, pubkey: asks }, + { isSigner: false, isWritable: false, pubkey: signerKey }, + { isSigner: false, isWritable: true, pubkey: dexEventQueue }, + { isSigner: false, isWritable: true, pubkey: dexBaseVault }, + { isSigner: false, isWritable: true, pubkey: dexQuoteVault }, + { isSigner: false, isWritable: false, pubkey: dexSigner }, + { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID }, + { isSigner: false, isWritable: false, pubkey: dexProgramId }, + { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY }, + ...openOrders.map( (pubkey) => ( { isSigner: false, isWritable: true, pubkey })), + ...oracles.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })), + ] + + const data = encodeMangoInstruction({ForceCancelOrders: { limit }}) + return new TransactionInstruction( { keys, data, programId }) +} + +export function makePartialLiquidateInstruction( + programId: PublicKey, + mangoGroup: PublicKey, + liqor: PublicKey, + liqorInTokenWallet: PublicKey, + liqorOutTokenWallet: PublicKey, + liqeeMarginAccount: PublicKey, + inTokenVault: PublicKey, + outTokenVault: PublicKey, + signerKey: PublicKey, + openOrders: PublicKey[], + oracles: PublicKey[], + maxDeposit: BN +): TransactionInstruction { + const keys = [ + { isSigner: false, isWritable: true, pubkey: mangoGroup }, + { isSigner: true, isWritable: false, pubkey: liqor }, + { isSigner: false, isWritable: true, pubkey: liqorInTokenWallet }, + { isSigner: false, isWritable: true, pubkey: liqorOutTokenWallet }, + { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount }, + { isSigner: false, isWritable: true, pubkey: inTokenVault }, + { isSigner: false, isWritable: true, pubkey: outTokenVault }, + { isSigner: false, isWritable: false, pubkey: signerKey }, + { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID }, + { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY }, + ...openOrders.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })), + ...oracles.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })), + ] + const data = encodeMangoInstruction({PartialLiquidate: { maxDeposit }}) + + return new TransactionInstruction( { keys, data, programId }) +} \ No newline at end of file diff --git a/src/layout.ts b/src/layout.ts index e7e360c..cf8c01f 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -155,7 +155,8 @@ export const MarginAccountLayout = struct([ seq(U64F64(), NUM_TOKENS, 'deposits'), seq(U64F64(), NUM_TOKENS, 'borrows'), seq(publicKeyLayout(), NUM_MARKETS, 'openOrders'), - seq(u8(), 8, 'padding') + u8('beingLiquidated'), + seq(u8(), 7, 'padding') ]); export const MangoSrmAccountLayout = struct([ @@ -165,6 +166,19 @@ export const MangoSrmAccountLayout = struct([ u64('amount') ]); +export const AccountLayout = struct([ + publicKeyLayout('mint'), + publicKeyLayout('owner'), + u64('amount'), + u32('delegateOption'), + publicKeyLayout('delegate'), + u8('state'), + u32('isNativeOption'), + u64('isNative'), + u64('delegatedAmount'), + u32('closeAuthorityOption'), + publicKeyLayout('closeAuthority') +]); class EnumLayout extends UInt { values: any; @@ -260,6 +274,8 @@ MangoInstructionLayout.addVariant(14, ), 'PlaceAndSettle' ) +MangoInstructionLayout.addVariant(15, struct([u8('limit')]), 'ForceCancelOrders') +MangoInstructionLayout.addVariant(16, struct([u64('maxDeposit')]), 'PartialLiquidate') // @ts-ignore const instructionMaxSpan = Math.max(...Object.values(MangoInstructionLayout.registry).map((r) => r.span)); @@ -268,3 +284,5 @@ export function encodeMangoInstruction(data) { const span = MangoInstructionLayout.encode(data, b); return b.slice(0, span); } + + diff --git a/src/utils.ts b/src/utils.ts index e3affa0..183456a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,6 +11,7 @@ import BN from 'bn.js'; import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'; import { blob, struct, u8, nu64 } from 'buffer-layout'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { AccountLayout } from './layout'; export const zeroKey = new PublicKey(new Uint8Array(32)) @@ -230,6 +231,18 @@ export function parseTokenAccountData( }; } +export function parseTokenAccount( + data: Buffer +): { mint: PublicKey; owner: PublicKey; amount: BN } { + + const decoded = AccountLayout.decode(data) + return { + mint: decoded.mint, + owner: decoded.owner, + amount: decoded.amount + } +} + export async function getMultipleAccounts( connection: Connection, publicKeys: PublicKey[]