publish as a repo

This commit is contained in:
Maximilian Schneider 2021-02-07 01:48:58 +01:00
commit ee877e69d0
11 changed files with 6549 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea
lib

6
devnet.env Normal file
View File

@ -0,0 +1,6 @@
CLUSTER=devnet
DEX_PROGRAM_ID=9MVDeYQnJmN2Dt7H44Z8cob4bET2ysdNu2uFJcatDJno
KEYPAIR=~/.config/solana/id.json
BTCUSD=D9CBrNncuERiTzQwUVkxCevrM6irHJybeU2NQXtgXRbq
BTC_WALLET=HLoPtihB8oETm1kkTpx17FEnXm7afQdS4hojTNvbg3Rg
USDC_WALLET=GBBtcVE7WA8qdrHyhWTZkYDaz71EVHsg7wVaca9iq9xs

71
package.json Normal file
View File

@ -0,0 +1,71 @@
{
"name": "@mango/client",
"version": "0.1.0",
"description": "Library for interacting with Mango Margin solana smart contracts.",
"repository": "blockworks-foundation/mango",
"author": {
"name": "Blockworks Foundation",
"email": "hello@blockworks.foundation",
"url": "https://blockworks.foundation"
},
"main": "lib/index.js",
"source": "src/index.js",
"types": "lib/index.d.ts",
"license": "MIT",
"engines": {
"node": ">=10"
},
"scripts": {
"build": "tsc",
"start": "ts-node src/index.ts",
"clean": "rm -rf lib",
"prepare": "run-s clean build",
"shell": "node -e \"$(< shell)\" -i --experimental-repl-await",
"test": "run-s test:unit test:lint test:build",
"test:build": "run-s build",
"test:lint": "eslint src",
"test:unit": "jest",
"test:watch": "jest --watch"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.0",
"@types/bn.js": "^4.11.6",
"@types/chai": "^4.2.14",
"@types/jest": "^26.0.9",
"@types/mocha": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"babel-eslint": "^10.0.3",
"chai": "^4.2.0",
"cross-env": "^7.0.2",
"eslint": "^7.6.0",
"eslint-config-prettier": "^6.11.0",
"jest": "^26.6.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
"ts-jest": "^26.2.0",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
},
"files": [
"lib"
],
"prettier": {
"singleQuote": true,
"trailingComma": "all"
},
"dependencies": {
"@project-serum/serum": "^0.13.18",
"@solana/web3.js": "^0.90.0",
"bn.js": "^5.1.2",
"buffer-layout": "^1.2.0",
"double.js": "^1.0.14",
"@project-serum/sol-wallet-adapter": "^0.1.4"
},
"browserslist": [
">0.2%",
"not dead",
"not op_mini all",
"maintained node versions"
]
}

738
src/client.ts Normal file
View File

@ -0,0 +1,738 @@
import {
Account,
AccountInfo,
Connection,
PublicKey,
sendAndConfirmRawTransaction,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
Transaction,
TransactionInstruction,
TransactionSignature,
} from '@solana/web3.js';
import {
encodeMangoInstruction,
MangoGroupLayout,
MarginAccountLayout, NUM_TOKENS,
WideBits,
} from './layout';
import BN from 'bn.js';
import {
createAccountInstruction,
decodeAggregatorInfo,
getMintDecimals,
nativeToUi,
uiToNative,
zeroKey,
} from './utils';
import { Market, OpenOrders } from '@project-serum/serum';
import { Wallet } from '@project-serum/sol-wallet-adapter';
import { TOKEN_PROGRAM_ID } from '@project-serum/serum/lib/token-instructions';
import { Order } from '@project-serum/serum/lib/market';
export class MangoGroup {
publicKey: PublicKey;
mintDecimals: number[];
accountFlags!: WideBits;
tokens!: PublicKey[];
vaults!: PublicKey[];
indexes!: { lastUpdate: BN, borrow: number, deposit: number };
spotMarkets!: PublicKey[];
oracles!: PublicKey[];
signerNonce!: BN;
signerKey!: PublicKey;
dexProgramId!: PublicKey;
totalDeposits!: number[];
totalBorrows!: number[];
maintCollRatio!: number;
initCollRatio!: number;
constructor(publicKey: PublicKey, mintDecimals: number[], decoded: any) {
this.publicKey = publicKey;
this.mintDecimals = mintDecimals;
Object.assign(this, decoded);
}
async getPrices(
connection: Connection,
): Promise<number[]> {
const oracleAccs = await getMultipleAccounts(connection, this.oracles);
return oracleAccs.map((oa) => decodeAggregatorInfo(oa.accountInfo).submissionValue).concat(1.0)
}
getMarketIndex(spotMarket: Market): number {
for (let i = 0; i < this.spotMarkets.length; i++) {
if (this.spotMarkets[i].equals(spotMarket.publicKey)) {
return i
}
}
throw new Error("This Market does not belong to this MangoGroup")
}
getTokenIndex(token: PublicKey): number {
for (let i = 0; i < this.tokens.length; i++) {
if (this.tokens[i].equals(token)) {
return i
}
}
throw new Error("This token does not belong in this MangoGroup")
}
getBorrowRate(tokenIndex: number): number {
return 0.0 // TODO
}
getDepositRate(tokenIndex: number): number {
return 0.0 // TODO
}
}
export class MarginAccount {
publicKey: PublicKey;
accountFlags!: WideBits;
mangoGroup!: PublicKey;
owner!: PublicKey;
deposits!: number[];
borrows!: number[];
openOrders!: PublicKey[];
openOrdersAccounts: undefined | (OpenOrders | undefined)[] // undefined if an openOrdersAccount not yet initialized and has zeroKey
constructor(publicKey: PublicKey, decoded: any) {
this.publicKey = publicKey;
Object.assign(this, decoded);
}
getNativeDeposit(mangoGroup: MangoGroup, tokenIndex: number): number { // insufficient precision
return Math.round(mangoGroup.indexes[tokenIndex].deposit * this.deposits[tokenIndex])
}
getNativeBorrow(mangoGroup: MangoGroup, tokenIndex: number): number { // insufficient precision
return Math.round(mangoGroup.indexes[tokenIndex].borrow * this.borrows[tokenIndex])
}
getUiDeposit(mangoGroup: MangoGroup, tokenIndex: number): number { // insufficient precision
return nativeToUi(this.getNativeDeposit(mangoGroup, tokenIndex), mangoGroup.mintDecimals[tokenIndex])
}
getUiBorrow(mangoGroup: MangoGroup, tokenIndex: number): number { // insufficient precision
return nativeToUi(this.getNativeBorrow(mangoGroup, tokenIndex), mangoGroup.mintDecimals[tokenIndex])
}
async loadOpenOrders(
connection: Connection,
dexProgramId: PublicKey
): Promise<(OpenOrders | undefined)[]> {
const promises: Promise<OpenOrders | undefined>[] = []
for (let i = 0; i < this.openOrders.length; i++) {
if (this.openOrders[i].equals(zeroKey)) {
promises.push(promiseUndef())
} else {
promises.push(OpenOrders.load(connection, this.openOrders[i], dexProgramId))
}
}
return Promise.all(promises)
}
toPrettyString(
mangoGroup: MangoGroup
): string {
const lines = [
`MarginAccount: ${this.publicKey.toBase58()}`,
`Asset Deposits Borrows`,
]
const tokenNames = ["BTC", "ETH", "USDC"] // TODO pull this from somewhere
for (let i = 0; i < mangoGroup.tokens.length; i++) {
lines.push(
`${tokenNames[i]} ${this.getUiDeposit(mangoGroup, i)} ${this.getUiBorrow(mangoGroup, i)}`
)
}
return lines.join('\n')
}
async getValue(
connection: Connection,
mangoGroup: MangoGroup
): Promise<number> {
const prices = await mangoGroup.getPrices(connection)
let value = 0
for (let i = 0; i < this.deposits.length; i++) {
value += (this.getUiDeposit(mangoGroup, i) - this.getUiBorrow(mangoGroup, i)) * prices[i]
}
if (this.openOrdersAccounts == undefined) {
return value
}
for (let i = 0; i < this.openOrdersAccounts.length; i++) {
const oos = this.openOrdersAccounts[i]
if (oos != undefined) {
value += nativeToUi(oos.baseTokenTotal.toNumber(), mangoGroup.mintDecimals[i]) * prices[i]
value += nativeToUi(oos.quoteTokenTotal.toNumber(), mangoGroup.mintDecimals[NUM_TOKENS-1])
}
}
return value
}
}
export class MangoClient {
async initMangoGroup() {
throw new Error("Not Implemented");
}
async sendTransaction(
connection: Connection,
transaction: Transaction,
payer: Account | Wallet,
additionalSigners: Account[]
): Promise<TransactionSignature> {
transaction.recentBlockhash = (await connection.getRecentBlockhash('max')).blockhash
transaction.setSigners(payer.publicKey, ...additionalSigners.map( a => a.publicKey ))
// TODO test on mainnet
// if Wallet was provided, sign with wallet
if ((typeof payer) === Wallet) { // this doesn't work. Need to copy over from Omega
// TODO test with wallet
if (additionalSigners.length > 0) {
transaction.partialSign(...additionalSigners)
}
transaction = payer.signTransaction(transaction)
} else {
// otherwise sign with the payer account
const signers = [payer].concat(additionalSigners)
transaction.sign(...signers)
}
const rawTransaction = transaction.serialize()
return await sendAndConfirmRawTransaction(connection, rawTransaction, {skipPreflight: true})
}
async initMarginAccount(
connection: Connection,
programId: PublicKey,
dexProgramId: PublicKey, // Serum DEX program ID
mangoGroup: MangoGroup,
payer: Account | Wallet
): Promise<PublicKey> {
// Create a Solana account for the MarginAccount and allocate space
const accInstr = await createAccountInstruction(connection, payer.publicKey, MarginAccountLayout.span, programId)
// Specify the accounts this instruction takes in (see program/src/instruction.rs)
const keys = [
{ isSigner: false, isWritable: false, pubkey: mangoGroup.publicKey},
{ isSigner: false, isWritable: true, pubkey: accInstr.account.publicKey },
{ isSigner: true, isWritable: false, pubkey: payer.publicKey },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_RENT_PUBKEY }
]
// Encode and create instruction for actual initMarginAccount instruction
const data = encodeMangoInstruction({ InitMarginAccount: {} })
const initMarginAccountInstruction = new TransactionInstruction( { keys, data, programId })
// Add all instructions to one atomic transaction
const transaction = new Transaction()
transaction.add(accInstr.instruction)
transaction.add(initMarginAccountInstruction)
// Specify signers in addition to the wallet
const additionalSigners = [
accInstr.account,
]
// sign, send and confirm transaction
await this.sendTransaction(connection, transaction, payer, additionalSigners)
return accInstr.account.publicKey
}
async deposit(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
marginAccount: MarginAccount,
owner: Account | Wallet,
token: PublicKey,
tokenAcc: PublicKey,
quantity: number
): Promise<TransactionSignature> {
const tokenIndex = mangoGroup.getTokenIndex(token)
const nativeQuantity = uiToNative(quantity, mangoGroup.mintDecimals[tokenIndex])
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey},
{ isSigner: false, isWritable: true, pubkey: marginAccount.publicKey },
{ isSigner: true, isWritable: false, pubkey: owner.publicKey },
{ isSigner: false, isWritable: false, pubkey: token },
{ isSigner: false, isWritable: true, pubkey: tokenAcc },
{ isSigner: false, isWritable: true, pubkey: mangoGroup.vaults[tokenIndex] },
{ isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY }
]
const data = encodeMangoInstruction({Deposit: {quantity: nativeQuantity}})
const instruction = new TransactionInstruction( { keys, data, programId })
const transaction = new Transaction()
transaction.add(instruction)
const additionalSigners = []
return await this.sendTransaction(connection, transaction, owner, additionalSigners)
}
async withdraw(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
marginAccount: MarginAccount,
owner: Account | Wallet,
token: PublicKey,
tokenAcc: PublicKey,
quantity: number
): Promise<TransactionSignature> {
const tokenIndex = mangoGroup.getTokenIndex(token)
const nativeQuantity = uiToNative(quantity, mangoGroup.mintDecimals[tokenIndex])
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey},
{ isSigner: false, isWritable: true, pubkey: marginAccount.publicKey },
{ isSigner: true, isWritable: false, pubkey: owner.publicKey },
{ isSigner: false, isWritable: true, pubkey: tokenAcc },
{ isSigner: false, isWritable: true, pubkey: mangoGroup.vaults[tokenIndex] },
{ isSigner: false, isWritable: false, pubkey: mangoGroup.signerKey },
{ isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
...marginAccount.openOrders.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
...mangoGroup.oracles.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
...mangoGroup.tokens.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
]
const data = encodeMangoInstruction({Withdraw: {tokenIndex, quantity: nativeQuantity}})
const instruction = new TransactionInstruction( { keys, data, programId })
const transaction = new Transaction()
transaction.add(instruction)
const additionalSigners = []
return await this.sendTransaction(connection, transaction, owner, additionalSigners)
}
async borrow(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
marginAccount: MarginAccount,
owner: Account | Wallet,
token: PublicKey,
quantity: number
): Promise<TransactionSignature> {
const tokenIndex = mangoGroup.getTokenIndex(token)
const nativeQuantity = uiToNative(quantity, mangoGroup.mintDecimals[tokenIndex])
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey},
{ isSigner: false, isWritable: true, pubkey: marginAccount.publicKey },
{ isSigner: true, isWritable: false, pubkey: owner.publicKey },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
...marginAccount.openOrders.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
...mangoGroup.oracles.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
...mangoGroup.tokens.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
]
const data = encodeMangoInstruction({Borrow: {tokenIndex, quantity: nativeQuantity}})
const instruction = new TransactionInstruction( { keys, data, programId })
const transaction = new Transaction()
transaction.add(instruction)
const additionalSigners = []
return await this.sendTransaction(connection, transaction, owner, additionalSigners)
}
async settleBorrow(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
marginAccount: MarginAccount,
owner: Account | Wallet,
token: PublicKey,
quantity: number
): Promise<TransactionSignature> {
const tokenIndex = mangoGroup.getTokenIndex(token)
const nativeQuantity = uiToNative(quantity, mangoGroup.mintDecimals[tokenIndex])
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey},
{ isSigner: false, isWritable: true, pubkey: marginAccount.publicKey },
{ isSigner: true, isWritable: false, pubkey: owner.publicKey },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY }
]
const data = encodeMangoInstruction({SettleBorrow: {tokenIndex: new BN(tokenIndex), quantity: nativeQuantity}})
const instruction = new TransactionInstruction( { keys, data, programId })
const transaction = new Transaction()
transaction.add(instruction)
const additionalSigners = []
return await this.sendTransaction(connection, transaction, owner, additionalSigners)
}
async liquidate(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
liqeeMarginAccount: MarginAccount, // liquidatee marginAccount
liqor: Account | Wallet, // liquidator
tokenAccs: PublicKey[],
depositQuantities: number[]
): Promise<TransactionSignature> {
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey},
{ isSigner: true, isWritable: false, pubkey: liqor.publicKey },
{ isSigner: false, isWritable: true, pubkey: liqeeMarginAccount.publicKey },
{ isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
...liqeeMarginAccount.openOrders.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
...mangoGroup.oracles.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
...mangoGroup.vaults.map( (pubkey) => ( { isSigner: false, isWritable: true, pubkey })),
...tokenAccs.map( (pubkey) => ( { isSigner: false, isWritable: true, pubkey })),
...mangoGroup.tokens.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
]
const data = encodeMangoInstruction({Liquidate: {depositQuantities}})
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 placeOrder(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
marginAccount: MarginAccount,
spotMarket: Market,
owner: Account | Wallet,
side: 'buy' | 'sell',
price: number,
size: number,
orderType?: 'limit' | 'ioc' | 'postOnly',
clientId?: BN,
): Promise<TransactionSignature> {
// TODO allow wrapped SOL wallets
// TODO allow fee discounts
orderType = orderType == undefined ? 'limit' : orderType
// orderType = orderType ?? 'limit'
const limitPrice = spotMarket.priceNumberToLots(price)
const maxQuantity = spotMarket.baseSizeNumberToLots(size)
if (maxQuantity.lte(new BN(0))) {
throw new Error('size too small')
}
if (limitPrice.lte(new BN(0))) {
throw new Error('invalid price')
}
const selfTradeBehavior = 'decrementTake'
const marketIndex = mangoGroup.getMarketIndex(spotMarket)
const vaultIndex = (side === 'buy') ? mangoGroup.vaults.length - 1 : marketIndex
// Add all instructions to one atomic transaction
const transaction = new Transaction()
// Specify signers in addition to the wallet
const additionalSigners: Account[] = []
// Create a Solana account for the open orders account if it's missing
const openOrdersKeys: PublicKey[] = [];
for (let i = 0; i < marginAccount.openOrders.length; i++) {
if (i === marketIndex && marginAccount.openOrders[marketIndex].equals(zeroKey)) {
// open orders missing for this market; create a new one now
const openOrdersSpace = OpenOrders.getLayout(mangoGroup.dexProgramId).span
const openOrdersLamports = await connection.getMinimumBalanceForRentExemption(openOrdersSpace, 'singleGossip')
const accInstr = await createAccountInstruction(
connection, owner.publicKey, openOrdersSpace, mangoGroup.dexProgramId, openOrdersLamports
)
transaction.add(accInstr.instruction)
additionalSigners.push(accInstr.account)
openOrdersKeys.push(accInstr.account.publicKey)
} else {
openOrdersKeys.push(marginAccount.openOrders[i])
}
}
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey},
{ isSigner: true, isWritable: false, pubkey: owner.publicKey },
{ isSigner: false, isWritable: true, pubkey: marginAccount.publicKey },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
{ isSigner: false, isWritable: false, pubkey: spotMarket.programId },
{ isSigner: false, isWritable: true, pubkey: spotMarket.publicKey },
{ isSigner: false, isWritable: true, pubkey: spotMarket['_decoded'].requestQueue },
{ isSigner: false, isWritable: true, pubkey: mangoGroup.vaults[vaultIndex] },
{ isSigner: false, isWritable: false, pubkey: mangoGroup.signerKey },
{ isSigner: false, isWritable: true, pubkey: spotMarket['_decoded'].baseVault },
{ isSigner: false, isWritable: true, pubkey: spotMarket['_decoded'].quoteVault },
{ isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_RENT_PUBKEY },
...openOrdersKeys.map( (pubkey) => ( { isSigner: false, isWritable: true, pubkey })),
...mangoGroup.oracles.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
...mangoGroup.tokens.map( (pubkey) => ( { isSigner: false, isWritable: false, pubkey })),
]
const data = encodeMangoInstruction(
{
PlaceOrder:
clientId
? { side, limitPrice, maxQuantity, orderType, clientId, selfTradeBehavior }
: { side, limitPrice, maxQuantity, orderType, selfTradeBehavior }
}
)
const placeOrderInstruction = new TransactionInstruction( { keys, data, programId })
transaction.add(placeOrderInstruction)
// sign, send and confirm transaction
return await this.sendTransaction(connection, transaction, owner, additionalSigners)
}
async settleFunds(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
marginAccount: MarginAccount,
owner: Account | Wallet,
spotMarket: Market,
): Promise<TransactionSignature> {
const marketIndex = mangoGroup.getMarketIndex(spotMarket)
const dexSigner = await PublicKey.createProgramAddress(
[
spotMarket.publicKey.toBuffer(),
spotMarket['_decoded'].vaultSignerNonce.toArrayLike(Buffer, 'le', 8)
],
spotMarket.programId
)
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey},
{ isSigner: true, isWritable: false, pubkey: owner.publicKey },
{ isSigner: false, isWritable: true, pubkey: marginAccount.publicKey },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
{ isSigner: false, isWritable: false, pubkey: spotMarket.programId },
{ isSigner: false, isWritable: true, pubkey: spotMarket.publicKey },
{ isSigner: false, isWritable: true, pubkey: marginAccount.openOrders[marketIndex] },
{ isSigner: false, isWritable: false, pubkey: mangoGroup.signerKey },
{ isSigner: false, isWritable: true, pubkey: spotMarket['_decoded'].baseVault },
{ isSigner: false, isWritable: true, pubkey: spotMarket['_decoded'].quoteVault },
{ isSigner: false, isWritable: true, pubkey: mangoGroup.vaults[marketIndex] },
{ isSigner: false, isWritable: true, pubkey: mangoGroup.vaults[mangoGroup.vaults.length - 1] },
{ isSigner: false, isWritable: false, pubkey: dexSigner },
{ isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
]
const data = encodeMangoInstruction( {SettleFunds: {}} )
const instruction = new TransactionInstruction( { keys, data, programId })
// Add all instructions to one atomic transaction
const transaction = new Transaction()
transaction.add(instruction)
// Specify signers in addition to the wallet
const additionalSigners = []
// sign, send and confirm transaction
return await this.sendTransaction(connection, transaction, owner, additionalSigners)
}
async cancelOrder(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
marginAccount: MarginAccount,
owner: Account | Wallet,
spotMarket: Market,
order: Order,
): Promise<TransactionSignature> {
const keys = [
{ isSigner: false, isWritable: true, pubkey: mangoGroup.publicKey},
{ isSigner: true, isWritable: false, pubkey: owner.publicKey },
{ isSigner: false, isWritable: true, pubkey: marginAccount.publicKey },
{ isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
{ isSigner: false, isWritable: false, pubkey: mangoGroup.dexProgramId },
{ isSigner: false, isWritable: true, pubkey: spotMarket.publicKey },
{ isSigner: false, isWritable: true, pubkey: order.openOrdersAddress },
{ isSigner: false, isWritable: true, pubkey: spotMarket['_decoded'].requestQueue },
{ isSigner: false, isWritable: false, pubkey: mangoGroup.signerKey },
]
const data = encodeMangoInstruction({
CancelOrder: {
side: order.side,
orderId: order.orderId,
openOrders: order.openOrdersAddress,
openOrdersSlot: order.openOrdersSlot
}
})
const instruction = new TransactionInstruction( { keys, data, programId })
const transaction = new Transaction()
transaction.add(instruction)
const additionalSigners = []
return await this.sendTransaction(connection, transaction, owner, additionalSigners)
}
async getMangoGroup(
connection: Connection,
mangoGroupPk: PublicKey
): Promise<MangoGroup> {
const acc = await connection.getAccountInfo(mangoGroupPk);
const decoded = MangoGroupLayout.decode(acc == null ? undefined : acc.data);
const mintDecimals: number[] = await Promise.all(decoded.tokens.map( (pk) => getMintDecimals(connection, pk) ))
return new MangoGroup(mangoGroupPk, mintDecimals, decoded);
}
async getMarginAccount(
connection: Connection,
marginAccountPk: PublicKey
): Promise<MarginAccount> {
const acc = await connection.getAccountInfo(marginAccountPk, 'singleGossip')
return new MarginAccount(marginAccountPk, MarginAccountLayout.decode(acc == null ? undefined : acc.data))
}
async getAllMarginAccounts(
connection: Connection,
programId: PublicKey,
mangoGroupPk: PublicKey
): Promise<MarginAccount[]>{
const filters = [
{
memcmp: {
offset: MarginAccountLayout.offsetOf('mangoGroup'),
bytes: mangoGroupPk.toBase58(),
}
},
{
dataSize: MarginAccountLayout.span,
},
];
const accounts = await getFilteredProgramAccounts(connection, programId, filters);
return accounts.map(
({ publicKey, accountInfo }) =>
new MarginAccount(publicKey, MarginAccountLayout.decode(accountInfo == null ? undefined : accountInfo.data))
);
}
async getMarginAccountsForOwner(
connection: Connection,
programId: PublicKey,
mangoGroup: MangoGroup,
owner: Account | Wallet
): Promise<MarginAccount[]> {
const filters = [
{
memcmp: {
offset: MarginAccountLayout.offsetOf('mangoGroup'),
bytes: mangoGroup.publicKey.toBase58(),
},
},
{
memcmp: {
offset: MarginAccountLayout.offsetOf('owner'),
bytes: owner.publicKey.toBase58(),
}
},
{
dataSize: MarginAccountLayout.span,
},
];
const accounts = await getFilteredProgramAccounts(connection, programId, filters);
return accounts.map(
({ publicKey, accountInfo }) =>
new MarginAccount(publicKey, MarginAccountLayout.decode(accountInfo == null ? undefined : accountInfo.data))
);
}
}
async function getMultipleAccounts(
connection: Connection,
publicKeys: PublicKey[]
): Promise<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }[]> {
const publickKeyStrs = publicKeys.map((pk) => (pk.toBase58()));
// @ts-ignore
const resp = await connection._rpcRequest('getMultipleAccounts', [publickKeyStrs]);
if (resp.error) {
throw new Error(resp.error.message);
}
return resp.result.value.map(
({ data, executable, lamports, owner } , i) => ({
publicKey: publicKeys[i],
accountInfo: {
data: Buffer.from(data[0], 'base64'),
executable,
owner: new PublicKey(owner),
lamports,
},
}),
);
}
async function getFilteredProgramAccounts(
connection: Connection,
programId: PublicKey,
filters,
): Promise<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }[]> {
// @ts-ignore
const resp = await connection._rpcRequest('getProgramAccounts', [
programId.toBase58(),
{
commitment: connection.commitment,
filters,
encoding: 'base64',
},
]);
if (resp.error) {
throw new Error(resp.error.message);
}
return resp.result.map(
({ pubkey, account: { data, executable, owner, lamports } }) => ({
publicKey: new PublicKey(pubkey),
accountInfo: {
data: Buffer.from(data[0], 'base64'),
executable,
owner: new PublicKey(owner),
lamports,
},
}),
);
}
async function promiseUndef(): Promise<undefined> {
return undefined
}

70
src/ids.json Normal file
View File

@ -0,0 +1,70 @@
{
"cluster_urls": {
"devnet": "https://devnet.solana.com",
"localnet": "http://127.0.0.1:8899",
"mainnet": "https://api.mainnet-beta.solana.com"
},
"mainnet-beta": {
"dex_program_id": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o",
"spot_markets": {
"BTC/USDC": "CVfYa8RGXnuDBeGmniCcdkBwoLqVxh92xB1JqgRQx3F",
"ETH/USDC": "H5uzEytiByuXt964KampmuNCurNDwkVVypkym75J2DQW"
}
},
"devnet": {
"dex_program_id": "9MVDeYQnJmN2Dt7H44Z8cob4bET2ysdNu2uFJcatDJno",
"mango_groups": {
"BTC_ETH_USDC": {
"mango_group_pk": "FB2f2xo4fotH8m4crEquj3siUbLqxWhSFi2X72iQ441v",
"mint_pks": [
"C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"Fq939Y5hycK62ZGwBjftLY2VyxqAQ8f1MxRqBMdAaBS7"
],
"oracle_pks": [
"DkwTe15VtMMSEn3HiLPtBRk4pzFq3EWXoH6cbqU5n5Cz",
"8mRBudYbSsUH9PjkburCAp7xTSsVWasa3y8frBgCJG36"
],
"spot_market_pks": [
"D9CBrNncuERiTzQwUVkxCevrM6irHJybeU2NQXtgXRbq",
"AxZKYcPs7VRdukHEWqHggvNrm5MwQ44uPzkiEYVUwqov"
],
"vault_pks": [
"58EL4VjB1JXRhu4oZtY3q8aznQasejcegCdcFrjorffM",
"4qCDMYhpwc9oTyJkW1uhzRQDrR4x9YAVM6d9Q9fFYWZY",
"5Q8FKkZ2sXuMMNUthL4wrZX4F64sdZ1R5C5xB6885kAx"
]
}
},
"mango_program_id": "EkZhmqe3joHMsFEDbrqJ4vDot5e8r8ht3oEpeTXPAueh",
"oracles": {
"BTC/USDC": "DkwTe15VtMMSEn3HiLPtBRk4pzFq3EWXoH6cbqU5n5Cz",
"ETH/USDC": "8mRBudYbSsUH9PjkburCAp7xTSsVWasa3y8frBgCJG36"
},
"spot_markets": {
"BTC/USDC": "D9CBrNncuERiTzQwUVkxCevrM6irHJybeU2NQXtgXRbq",
"ETH/USDC": "AxZKYcPs7VRdukHEWqHggvNrm5MwQ44uPzkiEYVUwqov"
},
"symbols": {
"BTC": "C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"ETH": "8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"USDC": "Fq939Y5hycK62ZGwBjftLY2VyxqAQ8f1MxRqBMdAaBS7"
}
},
"localnet": {
"dex_program_id": "",
"mango_groups": {
"BTC_ETH_USDC": {}
},
"mango_program_id": "",
"spot_markets": {
"BTC_USDC": "",
"ETH_USDC": ""
},
"symbols": {
"BTC": "",
"ETH": "",
"USDC": ""
}
}
}

101
src/index.ts Normal file
View File

@ -0,0 +1,101 @@
import { MangoClient, MangoGroup } from './client';
import { Account, Connection, PublicKey } from '@solana/web3.js';
import * as fs from 'fs';
import { Market } from '@project-serum/serum';
import { NUM_TOKENS } from './layout';
export { MangoClient, MangoGroup, MarginAccount } from './client';
export { MangoIndexLayout, MarginAccountLayout, MangoGroupLayout } from './layout';
export { NUM_TOKENS } from './layout';
import IDS from "./ids.json";
export { IDS }
//
// async function main() {
// const cluster = "devnet";
// 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_USDC.mango_group_pk);
// const mangoProgramId = new PublicKey(clusterIds.mango_program_id);
//
// const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk);
//
// const keyPairPath = '/home/dd/.config/solana/id.json'
// const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
//
// // TODO auto fetch
// const marginAccountPk = new PublicKey("58hhPAgRgk1BHM1UkvYnJfxpMcoyi3DSoKnkwxuFe47")
// let marginAccount = await client.getMarginAccount(connection, marginAccountPk)
//
// console.log(marginAccount.toPrettyString(mangoGroup))
//
// const marketIndex = 0 // index for BTC/USDC
// const spotMarket = await Market.load(
// connection,
// mangoGroup.spotMarkets[marketIndex],
// {skipPreflight: true, commitment: 'singleGossip'},
// mangoGroup.dexProgramId
// )
//
// // margin short 0.1 BTC
// await client.placeOrder(
// connection,
// mangoProgramId,
// mangoGroup,
// marginAccount,
// spotMarket,
// payer,
// 'sell',
// 30000,
// 0.1
// )
//
// await spotMarket.matchOrders(connection, payer, 10)
//
// await client.settleFunds(
// connection,
// mangoProgramId,
// mangoGroup,
// marginAccount,
// payer,
// spotMarket
// )
//
// await client.settleBorrow(connection, mangoProgramId, mangoGroup, marginAccount, payer, mangoGroup.tokens[2], 5000)
// await client.settleBorrow(connection, mangoProgramId, mangoGroup, marginAccount, payer, mangoGroup.tokens[0], 1.0)
//
// marginAccount = await client.getMarginAccount(connection, marginAccount.publicKey)
// console.log(marginAccount.toPrettyString(mangoGroup))
// }
//
// async function testAll() {
// const cluster = "devnet"
// 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_USDC.mango_group_pk);
// const mangoProgramId = new PublicKey(clusterIds.mango_program_id);
//
// const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk);
//
// const keyPairPath = '/home/dd/.config/solana/id.json'
// const payer = new Account(JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')))
//
// // TODO auto fetch
// const marginAccounts = await client.getMarginAccountsForOwner(connection, mangoProgramId, mangoGroup, payer)
// for (const x of marginAccounts) {
// // get value of each margin account and select highest
//
// console.log(x.publicKey.toBase58())
// }
//
// }
//
//
//
//
// testAll()

234
src/layout.ts Normal file
View File

@ -0,0 +1,234 @@
import {struct, u8, blob, union, u32, Layout, bits, Blob, seq, BitStructure, UInt } from 'buffer-layout';
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
export const NUM_TOKENS = 3;
export const NUM_MARKETS = NUM_TOKENS - 1;
class PublicKeyLayout extends Blob {
constructor(property) {
super(32, property);
}
decode(b, offset) {
return new PublicKey(super.decode(b, offset));
}
encode(src, b, offset) {
return super.encode(src.toBuffer(), b, offset);
}
}
export function publicKeyLayout(property = "") {
return new PublicKeyLayout(property);
}
class BNLayout extends Blob {
constructor(number: number, property) {
super(number, property);
// restore prototype chain
Object.setPrototypeOf(this, new.target.prototype)
}
decode(b, offset) {
return new BN(super.decode(b, offset), 10, 'le');
}
encode(src, b, offset) {
return super.encode(src.toArrayLike(Buffer, 'le', this['span']), b, offset);
}
}
export function u64(property = "") {
return new BNLayout(8, property);
}
export function u128(property = "") {
return new BNLayout(16, property);
}
class U64F64Layout extends Blob {
constructor(property: string) {
super(16, property);
}
decode(b, offset) {
const raw = new BN(super.decode(b, offset), 10, 'le');
// @ts-ignore
return raw / Math.pow(2, 64);
}
encode(src, b, offset) {
return super.encode(src.toArrayLike(Buffer, 'le', this['span']), b, offset);
}
}
export function U64F64(property = "") {
return new U64F64Layout(property)
}
export class WideBits extends Layout {
_lower: BitStructure;
_upper: BitStructure;
constructor(property) {
super(8, property);
this._lower = bits(u32(), false);
this._upper = bits(u32(), false);
}
addBoolean(property) {
if (this._lower.fields.length < 32) {
this._lower.addBoolean(property);
} else {
this._upper.addBoolean(property);
}
}
decode(b, offset = 0) {
const lowerDecoded = this._lower.decode(b, offset);
const upperDecoded = this._upper.decode(b, offset + this._lower.span);
return { ...lowerDecoded, ...upperDecoded };
}
replicate(property: string) {
return super.replicate(property);
}
encode(src, b, offset = 0) {
return (
this._lower.encode(src, b, offset) +
this._upper.encode(src, b, offset + this._lower.span)
);
}
}
const ACCOUNT_FLAGS_LAYOUT = new WideBits(undefined);
ACCOUNT_FLAGS_LAYOUT.addBoolean('Initialized');
ACCOUNT_FLAGS_LAYOUT.addBoolean('MangoGroup');
ACCOUNT_FLAGS_LAYOUT.addBoolean('MarginAccount');
export function accountFlagsLayout(property = 'accountFlags') {
return ACCOUNT_FLAGS_LAYOUT.replicate(property); // TODO: when ts check is on, replicate throws error, doesn't compile
}
export const MangoIndexLayout = struct([
u64('lastUpdate'),
U64F64('borrow'), // U64F64
U64F64('deposit') // U64F64
]);
export const MangoGroupLayout = struct([
accountFlagsLayout('accountFlags'),
seq(publicKeyLayout(), NUM_TOKENS, 'tokens'),
seq(publicKeyLayout(), NUM_TOKENS, 'vaults'),
seq(MangoIndexLayout.replicate(), NUM_TOKENS, 'indexes'),
seq(publicKeyLayout(), NUM_MARKETS, 'spotMarkets'),
seq(publicKeyLayout(), NUM_MARKETS, 'oracles'),
u64('signerNonce'),
publicKeyLayout('signerKey'),
publicKeyLayout('dexProgramId'),
seq(U64F64(), NUM_TOKENS, 'totalDeposits'),
seq(U64F64(), NUM_TOKENS, 'totalBorrows'),
U64F64('maintCollRatio'),
U64F64('initCollRatio')
]);
export const MarginAccountLayout = struct([
accountFlagsLayout('accountFlags'),
publicKeyLayout('mangoGroup'),
publicKeyLayout('owner'),
seq(U64F64(), NUM_TOKENS, 'deposits'),
seq(U64F64(), NUM_TOKENS, 'borrows'),
// seq(u64(), NUM_TOKENS, 'positions'),
seq(publicKeyLayout(), NUM_MARKETS, 'openOrders')
]);
class EnumLayout extends UInt {
values: any;
constructor(values, span, property) {
super(span, property);
this.values = values
}
encode(src, b, offset) {
if (this.values[src] !== undefined) {
return super.encode(this.values[src], b, offset);
}
throw new Error('Invalid ' + this['property']);
}
decode(b, offset) {
const decodedValue = super.decode(b, offset);
const entry = Object.entries(this.values).find(
([, value]) => value === decodedValue,
);
if (entry) {
return entry[0];
}
throw new Error('Invalid ' + this['property']);
}
}
export function sideLayout(property) {
return new EnumLayout({ buy: 0, sell: 1 }, 4, property);
}
export function orderTypeLayout(property) {
return new EnumLayout({ limit: 0, ioc: 1, postOnly: 2 }, 4, property);
}
export function selfTradeBehaviorLayout(property) {
return new EnumLayout({ decrementTake: 0, cancelProvide: 1 }, 4, property);
}
export const MangoInstructionLayout = union(u32('instruction'))
MangoInstructionLayout.addVariant(0, struct([]), 'InitMangoGroup')
MangoInstructionLayout.addVariant(1, struct([]), 'InitMarginAccount')
MangoInstructionLayout.addVariant(2, struct([u64('quantity')]), 'Deposit')
MangoInstructionLayout.addVariant(3, struct([u64('tokenIndex'), u64('quantity')]), 'Withdraw')
MangoInstructionLayout.addVariant(4, struct([u64('tokenIndex'), u64('quantity')]), 'Borrow')
MangoInstructionLayout.addVariant(5, struct([u64('tokenIndex'), u64('quantity')]), 'SettleBorrow')
MangoInstructionLayout.addVariant(6, struct([seq(u64(), NUM_TOKENS, 'depositQuantities')]), 'Liquidate')
MangoInstructionLayout.addVariant(7,
struct(
[
sideLayout('side'),
u64('limitPrice'),
u64('maxQuantity'),
orderTypeLayout('orderType'),
u64('clientId'),
selfTradeBehaviorLayout('selfTradeBehavior')
]
),
'PlaceOrder'
)
MangoInstructionLayout.addVariant(8, struct([]), 'SettleFunds')
MangoInstructionLayout.addVariant(9,
struct(
[
sideLayout('side'),
u128('orderId'),
publicKeyLayout('openOrders'),
u8('openOrdersSlot')
]
),
'CancelOrder'
)
MangoInstructionLayout.addVariant(10, struct([]), 'CancelOrderByClientId')
// @ts-ignore
const instructionMaxSpan = Math.max(...Object.values(MangoInstructionLayout.registry).map((r) => r.span));
export function encodeMangoInstruction(data) {
const b = Buffer.alloc(instructionMaxSpan);
const span = MangoInstructionLayout.encode(data, b);
return b.slice(0, span);
}

0
src/markets.ts Normal file
View File

136
src/utils.ts Normal file
View File

@ -0,0 +1,136 @@
import {Account, Connection, PublicKey, SystemProgram, TransactionInstruction} from "@solana/web3.js";
import { publicKeyLayout, u64 } from './layout';
import BN from 'bn.js';
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions';
import { blob, struct, u8 } from 'buffer-layout';
export const zeroKey = new PublicKey(new Uint8Array(32))
export async function createAccountInstruction(
connection: Connection,
payer: PublicKey,
space: number,
owner: PublicKey,
lamports?: number
): Promise<{ account: Account, instruction: TransactionInstruction }> {
const account = new Account();
const instruction = SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: account.publicKey,
lamports: lamports ? lamports : await connection.getMinimumBalanceForRentExemption(space),
space,
programId: owner
})
return { account, instruction };
}
export function getMedian(submissions: number[]): number {
const values = submissions
.filter((s: any) => s.value != 0)
.map((s: any) => s.value)
.sort((a, b) => a - b)
const len = values.length
if (len == 0) {
return 0
} else if (len == 1) {
return values[0]
} else {
const i = len / 2
return len % 2 == 0 ? (values[i] + values[i-1])/2 : values[i]
}
}
export const AggregatorLayout = struct([
blob(4, "submitInterval"),
u64("minSubmissionValue"),
u64("maxSubmissionValue"),
blob(32, "description"),
u8("isInitialized"),
publicKeyLayout('owner'),
blob(576, "submissions")
]);
export const SubmissionLayout = struct([
u64("time"),
u64("value"),
publicKeyLayout('oracle'),
]);
export function decodeAggregatorInfo(accountInfo) {
const aggregator = AggregatorLayout.decode(accountInfo.data);
const minSubmissionValue = aggregator.minSubmissionValue;
const maxSubmissionValue = aggregator.maxSubmissionValue;
const submitInterval = aggregator.submitInterval;
const description = (aggregator.description.toString() as string).trim()
// decode oracles
const submissions: any[] = []
const submissionSpace = SubmissionLayout.span
let latestUpdateTime = new BN(0);
for (let i = 0; i < aggregator.submissions.length / submissionSpace; i++) {
const submission = SubmissionLayout.decode(
aggregator.submissions.slice(i*submissionSpace, (i+1)*submissionSpace)
)
submission.value = submission.value / 100.0;
if (!submission.oracle.equals(new PublicKey(0))) {
submissions.push(submission)
}
if (submission.time > latestUpdateTime) {
latestUpdateTime = submission.time
}
}
return {
minSubmissionValue: minSubmissionValue,
maxSubmissionValue: maxSubmissionValue,
submissionValue: getMedian(submissions),
submitInterval,
description,
oracles: submissions.map(s => s.oracle.toString()),
latestUpdateTime: new Date(Number(latestUpdateTime)*1000),
}
}
const MINT_LAYOUT = struct([blob(44), u8('decimals'), blob(37)]);
export async function getMintDecimals(
connection: Connection,
mint: PublicKey,
): Promise<number> {
if (mint.equals(WRAPPED_SOL_MINT)) {
return 9;
}
const { data } = throwIfNull(
await connection.getAccountInfo(mint),
'mint not found',
);
const { decimals } = MINT_LAYOUT.decode(data);
return decimals;
}
function throwIfNull<T>(value: T | null, message = 'account not found'): T {
if (value === null) {
throw new Error(message);
}
return value;
}
export function uiToNative(amount: number, decimals: number): BN {
return new BN(Math.round(amount * Math.pow(10, decimals)))
}
export function nativeToUi(amount: number, decimals: number): number {
return amount / Math.pow(10, decimals)
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"outDir": "./lib",
"allowJs": true,
"checkJs": true,
"declaration": true,
"declarationMap": true,
"noImplicitAny": false,
"resolveJsonModule": true,
"sourceMap": true
},
"include": ["./src/**/*"],
"exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"]
}

5151
yarn.lock Normal file

File diff suppressed because it is too large Load Diff