publish as a repo
This commit is contained in:
commit
ee877e69d0
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
CLUSTER=devnet
|
||||
DEX_PROGRAM_ID=9MVDeYQnJmN2Dt7H44Z8cob4bET2ysdNu2uFJcatDJno
|
||||
KEYPAIR=~/.config/solana/id.json
|
||||
BTCUSD=D9CBrNncuERiTzQwUVkxCevrM6irHJybeU2NQXtgXRbq
|
||||
BTC_WALLET=HLoPtihB8oETm1kkTpx17FEnXm7afQdS4hojTNvbg3Rg
|
||||
USDC_WALLET=GBBtcVE7WA8qdrHyhWTZkYDaz71EVHsg7wVaca9iq9xs
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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,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)
|
||||
|
||||
}
|
|
@ -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"]
|
||||
}
|
Loading…
Reference in New Issue