ts client support for perps

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-05-11 13:33:01 +02:00
parent b7eda83e1b
commit 81f0f38188
15 changed files with 3713 additions and 3339 deletions

View File

@ -154,6 +154,9 @@ fn main() -> Result<(), anyhow::Error> {
let mango_client = Arc::new(MangoClient::new(cluster, commitment, payer, admin));
log::info!("Program Id {}", &mango_client.program().id());
log::info!("Admin {}", &mango_client.admin.to_base58_string());
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()

View File

@ -2,7 +2,7 @@
set -e pipefail
rg m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD -l | xargs -I % sed -i '' 's/m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD/5V2zCYCQkm4sZc3WctiwQEAzvfAiFxyjbwCvzQnmtmkM/g' %;
# rg m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD -l | xargs -I % sed -i '' 's/m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD/5V2zCYCQkm4sZc3WctiwQEAzvfAiFxyjbwCvzQnmtmkM/g' %;
WALLET_WITH_FUNDS=~/.config/solana/mango-devnet.json
PROGRAM_ID=5V2zCYCQkm4sZc3WctiwQEAzvfAiFxyjbwCvzQnmtmkM

View File

@ -20,6 +20,7 @@ pub struct MarginTrade<'info> {
pub owner: Signer<'info>,
}
// TODO: add loan fees
pub fn margin_trade<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, MarginTrade<'info>>,
banks_len: usize,

View File

@ -2,6 +2,7 @@ use anchor_lang::prelude::*;
use bytemuck::{cast, cast_mut, cast_ref};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use static_assertions::const_assert_eq;
use crate::state::orderbook::bookside_iterator::BookSideIter;
@ -44,6 +45,11 @@ pub struct BookSide {
pub leaf_count: usize,
pub nodes: [AnyNode; MAX_BOOK_NODES],
}
const_assert_eq!(
std::mem::size_of::<BookSide>(),
8 + 8 * 2 + 4 + 4 + 8 + 88 * 1024
);
const_assert_eq!(std::mem::size_of::<BookSide>() % 8, 0);
impl BookSide {
/// Iterate over all entries in the book filtering out invalid orders

View File

@ -153,6 +153,8 @@ impl QueueHeader for EventQueueHeader {
}
pub type EventQueue = Queue<EventQueueHeader>;
const_assert_eq!(std::mem::size_of::<EventQueue>(), 8 * 3 + 512 * 200);
const_assert_eq!(std::mem::size_of::<EventQueue>() % 8, 0);
const EVENT_SIZE: usize = 200;
#[derive(Copy, Clone, Debug, Pod)]

View File

@ -92,7 +92,6 @@ impl<'info> LoadZeroCopy for AccountInfo<'info> {
pub fn fill16_from_str(name: String) -> Result<[u8; 16]> {
let name_bytes = name.as_bytes();
msg!("{}", name);
require!(name_bytes.len() < 16, MangoError::SomeError);
let mut name_ = [0u8; 16];
name_[..name_bytes.len()].copy_from_slice(name_bytes);
@ -101,7 +100,6 @@ pub fn fill16_from_str(name: String) -> Result<[u8; 16]> {
pub fn fill32_from_str(name: String) -> Result<[u8; 32]> {
let name_bytes = name.as_bytes();
msg!("{}", name);
require!(name_bytes.len() < 32, MangoError::SomeError);
let mut name_ = [0u8; 32];
name_[..name_bytes.len()].copy_from_slice(name_bytes);

View File

@ -151,10 +151,10 @@ async fn test_perp() -> Result<(), BanksClientError> {
quote_token_index: tokens[1].index,
quote_lot_size: 10,
base_lot_size: 100,
init_asset_weight: 0.95,
maint_asset_weight: 0.975,
init_liab_weight: 1.05,
init_asset_weight: 0.95,
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,

View File

@ -1,11 +1,12 @@
import { PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
import { Bank } from './bank';
import { PerpMarket } from './perp';
import { Serum3Market } from './serum3';
export class Group {
static from(publicKey: PublicKey, obj: { admin: PublicKey }): Group {
return new Group(publicKey, obj.admin, new Map(), new Map());
return new Group(publicKey, obj.admin, new Map(), new Map(), new Map());
}
constructor(
@ -13,6 +14,7 @@ export class Group {
public admin: PublicKey,
public banksMap: Map<string, Bank>,
public serum3MarketsMap: Map<string, Serum3Market>,
public perpMarketsMap: Map<string, PerpMarket>,
) {}
public findBank(tokenIndex: number): Bank | undefined {
@ -24,6 +26,7 @@ export class Group {
public async reload(client: MangoClient) {
await this.reloadBanks(client);
await this.reloadSerum3Markets(client);
await this.reloadPerpMarkets(client);
}
public async reloadBanks(client: MangoClient) {
@ -37,4 +40,11 @@ export class Group {
serum3Markets.map((serum3Market) => [serum3Market.name, serum3Market]),
);
}
public async reloadPerpMarkets(client: MangoClient) {
const perpMarkets = await client.perpGetMarket(this);
this.perpMarketsMap = new Map(
perpMarkets.map((perpMarket) => [perpMarket.name, perpMarket]),
);
}
}

View File

@ -1,3 +1,4 @@
import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
@ -6,6 +7,7 @@ import { I80F48, I80F48Dto } from './I80F48';
export class MangoAccount {
public tokens: TokenAccount[];
public serum3: Serum3Account[];
public perps: PerpAccount[];
public name: string;
static from(
@ -33,7 +35,7 @@ export class MangoAccount {
obj.delegate,
obj.tokens as { values: TokenAccountDto[] },
obj.serum3 as { values: Serum3AccountDto[] },
obj.perps,
obj.perps as { accounts: PerpAccountDto[] },
obj.beingLiquidated,
obj.isBankrupt,
obj.accountNum,
@ -50,7 +52,7 @@ export class MangoAccount {
public delegate: PublicKey,
tokens: { values: TokenAccountDto[] },
serum3: { values: Serum3AccountDto[] },
perps: unknown,
perps: { accounts: PerpAccountDto[] },
beingLiquidated: number,
isBankrupt: number,
accountNum: number,
@ -60,6 +62,7 @@ export class MangoAccount {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.tokens = tokens.values.map((dto) => TokenAccount.from(dto));
this.serum3 = serum3.values.map((dto) => Serum3Account.from(dto));
this.perps = perps.accounts.map((dto) => PerpAccount.from(dto));
}
async reload(client: MangoClient) {
@ -83,9 +86,40 @@ export class MangoAccount {
const ta = this.findToken(bank.tokenIndex);
return bank.borrowIndex.mul(ta?.indexedValue!);
}
toString(): string {
return (
'tokens:' +
JSON.stringify(
this.tokens.filter(
(token) => token.tokenIndex != TokenAccount.TokenIndexUnset,
),
null,
4,
) +
'\nserum:' +
JSON.stringify(
this.serum3.filter(
(serum3) =>
serum3.marketIndex != Serum3Account.Serum3MarketIndexUnset,
),
null,
4,
) +
'\nperps:' +
JSON.stringify(
this.perps.filter(
(perp) => perp.marketIndex != PerpAccount.PerpMarketIndexUnset,
),
null,
4,
)
);
}
}
export class TokenAccount {
static TokenIndexUnset: number = 65535;
static from(dto: TokenAccountDto) {
return new TokenAccount(
I80F48.from(dto.indexedValue),
@ -138,3 +172,41 @@ export class Serum3AccountDto {
public reserved: number[],
) {}
}
export class PerpAccount {
static PerpMarketIndexUnset = 65535;
static from(dto: PerpAccountDto) {
return new PerpAccount(
dto.marketIndex,
dto.basePositionLots.toNumber(),
dto.quotePositionNative.val.toNumber(),
dto.bidsBaseLots.toNumber(),
dto.asksBaseLots.toNumber(),
dto.takerBaseLots.toNumber(),
dto.takerQuoteLots.toNumber(),
);
}
constructor(
public marketIndex: number,
public basePositionLots: number,
public quotePositionNative: number,
public bidsBaseLots: number,
public asksBaseLots: number,
public takerBaseLots: number,
public takerQuoteLots: number,
) {}
}
export class PerpAccountDto {
constructor(
public marketIndex: number,
public reserved: [],
public basePositionLots: BN,
public quotePositionNative: { val: BN },
public bidsBaseLots: BN,
public asksBaseLots: BN,
public takerBaseLots: BN,
public takerQuoteLots: BN,
) {}
}

View File

@ -0,0 +1,130 @@
import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import { I80F48, I80F48Dto } from './I80F48';
export class PerpMarket {
public name: string;
public quoteLotSize: number;
public baseLotSize: number;
public maintAssetWeight: I80F48;
public initAssetWeight: I80F48;
public maintLiabWeight: I80F48;
public initLiabWeight: I80F48;
public liquidationFee: I80F48;
public makerFee: I80F48;
public takerFee: I80F48;
public openInterest: number;
public seqNum: number;
public feesAccrued: I80F48;
static from(
publicKey: PublicKey,
obj: {
name: number[];
group: PublicKey;
oracle: PublicKey;
bids: PublicKey;
asks: PublicKey;
eventQueue: PublicKey;
quoteLotSize: BN;
baseLotSize: BN;
maintAssetWeight: I80F48Dto;
initAssetWeight: I80F48Dto;
maintLiabWeight: I80F48Dto;
initLiabWeight: I80F48Dto;
liquidationFee: I80F48Dto;
makerFee: I80F48Dto;
takerFee: I80F48Dto;
openInterest: BN;
seqNum: any; // TODO: ts complains that this is unknown for whatever reason
feesAccrued: I80F48Dto;
bump: number;
reserved: number[];
perpMarketIndex: number;
baseTokenIndex: number;
quoteTokenIndex: number;
},
): PerpMarket {
return new PerpMarket(
publicKey,
obj.name,
obj.group,
obj.oracle,
obj.bids,
obj.asks,
obj.eventQueue,
obj.quoteLotSize,
obj.baseLotSize,
obj.maintAssetWeight,
obj.initAssetWeight,
obj.maintLiabWeight,
obj.initLiabWeight,
obj.liquidationFee,
obj.makerFee,
obj.takerFee,
obj.openInterest,
obj.seqNum,
obj.feesAccrued,
obj.bump,
obj.reserved,
obj.perpMarketIndex,
obj.baseTokenIndex,
obj.quoteTokenIndex,
);
}
constructor(
public publicKey: PublicKey,
name: number[],
public group: PublicKey,
public oracle: PublicKey,
public bids: PublicKey,
public asks: PublicKey,
public eventQueue: PublicKey,
quoteLotSize: BN,
baseLotSize: BN,
maintAssetWeight: I80F48Dto,
initAssetWeight: I80F48Dto,
maintLiabWeight: I80F48Dto,
initLiabWeight: I80F48Dto,
liquidationFee: I80F48Dto,
makerFee: I80F48Dto,
takerFee: I80F48Dto,
openInterest: BN,
seqNum: BN,
feesAccrued: I80F48Dto,
bump: number,
reserved: number[],
public perpMarketIndex: number,
public baseTokenIndex: number,
public quoteTokenIndex: number,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.quoteLotSize = quoteLotSize.toNumber();
this.baseLotSize = baseLotSize.toNumber();
this.maintAssetWeight = I80F48.from(maintAssetWeight);
this.initAssetWeight = I80F48.from(initAssetWeight);
this.maintLiabWeight = I80F48.from(maintLiabWeight);
this.initLiabWeight = I80F48.from(initLiabWeight);
this.liquidationFee = I80F48.from(liquidationFee);
this.makerFee = I80F48.from(makerFee);
this.takerFee = I80F48.from(takerFee);
this.openInterest = openInterest.toNumber();
this.seqNum = seqNum.toNumber();
this.feesAccrued = I80F48.from(feesAccrued);
}
}
export class Side {
static bid = { bid: {} };
static ask = { ask: {} };
}
export class OrderType {
static limit = { limit: {} };
static immediateOrCancel = { immediateorcancel: {} };
static postOnly = { postonly: {} };
static market = { market: {} };
static postOnlySlide = { postonlyslide: {} };
}

View File

@ -4,8 +4,10 @@ import { Order } from '@project-serum/serum/lib/market';
import * as spl from '@solana/spl-token';
import {
AccountMeta,
Keypair,
MemcmpFilter,
PublicKey,
SystemProgram,
SYSVAR_RENT_PUBKEY,
TransactionSignature,
} from '@solana/web3.js';
@ -15,6 +17,7 @@ import { Group } from './accounts/group';
import { I80F48 } from './accounts/I80F48';
import { MangoAccount } from './accounts/mangoAccount';
import { StubOracle } from './accounts/oracle';
import { OrderType, PerpMarket, Side } from './accounts/perp';
import {
Serum3Market,
Serum3OrderType,
@ -621,6 +624,175 @@ export class MangoClient {
);
}
/// perps
async perpCreateMarket(
group: Group,
oraclePk: PublicKey,
perpMarketIndex: number,
name: string,
baseTokenIndex: number,
quoteTokenIndex: number,
quoteLotSize: number,
baseLotSize: number,
maintAssetWeight: number,
initAssetWeight: number,
maintLiabWeight: number,
initLiabWeight: number,
liquidationFee: number,
makerFee: number,
takerFee: number,
): Promise<TransactionSignature> {
const bids = new Keypair();
const asks = new Keypair();
const eventQueue = new Keypair();
console.log(this.program.provider.wallet.publicKey.toBase58());
return await this.program.methods
.perpCreateMarket(
perpMarketIndex,
name,
baseTokenIndex,
quoteTokenIndex,
new BN(quoteLotSize),
new BN(baseLotSize),
maintAssetWeight,
initAssetWeight,
maintLiabWeight,
initLiabWeight,
liquidationFee,
makerFee,
takerFee,
)
.accounts({
group: group.publicKey,
admin: this.program.provider.wallet.publicKey,
oracle: oraclePk,
bids: bids.publicKey,
asks: asks.publicKey,
eventQueue: eventQueue.publicKey,
payer: this.program.provider.wallet.publicKey,
})
.preInstructions([
SystemProgram.createAccount({
programId: this.program.programId,
space: 8 + 90152,
lamports:
await this.program.provider.connection.getMinimumBalanceForRentExemption(
90160,
),
fromPubkey: this.program.provider.wallet.publicKey,
newAccountPubkey: bids.publicKey,
}),
SystemProgram.createAccount({
programId: this.program.programId,
space: 8 + 90152,
lamports:
await this.program.provider.connection.getMinimumBalanceForRentExemption(
90160,
),
fromPubkey: this.program.provider.wallet.publicKey,
newAccountPubkey: asks.publicKey,
}),
SystemProgram.createAccount({
programId: this.program.programId,
space: 8 + 102424,
lamports:
await this.program.provider.connection.getMinimumBalanceForRentExemption(
102432,
),
fromPubkey: this.program.provider.wallet.publicKey,
newAccountPubkey: eventQueue.publicKey,
}),
])
.signers([bids, asks, eventQueue])
.rpc();
}
public async perpGetMarket(
group: Group,
baseTokenIndex?: number,
quoteTokenIndex?: number,
): Promise<PerpMarket[]> {
const bumpfbuf = Buffer.alloc(1);
bumpfbuf.writeUInt8(255);
const filters: MemcmpFilter[] = [
{
memcmp: {
bytes: group.publicKey.toBase58(),
offset: 24,
},
},
];
if (baseTokenIndex) {
const bbuf = Buffer.alloc(2);
bbuf.writeUInt16LE(baseTokenIndex);
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 348,
},
});
}
if (quoteTokenIndex) {
const qbuf = Buffer.alloc(2);
qbuf.writeUInt16LE(quoteTokenIndex);
filters.push({
memcmp: {
bytes: bs58.encode(qbuf),
offset: 350,
},
});
}
return (await this.program.account.perpMarket.all(filters)).map((tuple) =>
PerpMarket.from(tuple.publicKey, tuple.account),
);
}
async perpPlaceOrder(
group: Group,
mangoAccount: MangoAccount,
perpMarketName: string,
side: Side,
priceLots: number,
maxBaseLots: number,
maxQuoteLots: number,
clientOrderId: number,
orderType: OrderType,
expiryTimestamp: number,
limit: number,
) {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
await this.program.methods
.perpPlaceOrder(
side,
new BN((priceLots * perpMarket.baseLotSize) / perpMarket.quoteLotSize),
new BN(maxBaseLots),
new BN(maxQuoteLots),
new BN(clientOrderId),
orderType,
new BN(expiryTimestamp),
limit,
)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
perpMarket: perpMarket.publicKey,
asks: perpMarket.asks,
bids: perpMarket.bids,
eventQueue: perpMarket.eventQueue,
oracle: perpMarket.oracle,
owner: this.program.provider.wallet.publicKey,
})
.rpc();
}
/// static
static async connect(
@ -703,6 +875,16 @@ export class MangoClient {
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
.map((serum3Account) => serum3Account.openOrders),
);
healthRemainingAccounts.push(
...mangoAccount.perps
.filter((perp) => perp.marketIndex !== 65535)
.map(
(perp) =>
Array.from(group.perpMarketsMap.values()).filter(
(perpMarket) => perpMarket.perpMarketIndex === perp.marketIndex,
)[0].publicKey,
),
);
return healthRemainingAccounts;
}

File diff suppressed because it is too large Load Diff

View File

@ -136,6 +136,36 @@ async function main() {
);
console.log(`...registerd serum3 market ${markets[0].publicKey}`);
// register perp market
console.log(`Registering perp market...`);
try {
await client.perpCreateMarket(
group,
btcDevnetOracle,
0,
'BTC/USDC',
0,
1,
10,
100,
0.975,
0.95,
1.025,
1.05,
0.012,
0.0002,
0.0,
);
} catch (error) {
console.log(error);
}
const perpMarkets = await client.perpGetMarket(
group,
group.banksMap.get('BTC')?.tokenIndex,
group.banksMap.get('USDC')?.tokenIndex,
);
console.log(`...created perp market ${perpMarkets[0].publicKey}`);
process.exit();
}

View File

@ -1,6 +1,7 @@
import { Provider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { OrderType, Side } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
@ -156,6 +157,46 @@ async function main() {
'BTC/USDC',
);
// perps
console.log(`Placing perp bid...`);
await client.perpPlaceOrder(
group,
mangoAccount,
'BTC/USDC',
Side.bid,
1,
1,
65535,
65535,
OrderType.limit,
0,
1,
);
console.log(`Placing perp ask...`);
await client.perpPlaceOrder(
group,
mangoAccount,
'BTC/USDC',
Side.ask,
1,
1,
65535,
65535,
OrderType.limit,
0,
1,
);
while (true) {
// TODO: quotePositionNative might be buggy on program side, investigate...
console.log(
`Waiting for self trade to consume (note: make sure keeper crank is running)...`,
);
await mangoAccount.reload(client);
console.log(mangoAccount.toString());
}
process.exit();
}

View File

@ -6,6 +6,7 @@
"declaration": true,
"declarationDir": "dist",
"declarationMap": true,
"noErrorTruncation": true,
"esModuleInterop": true,
"lib": [
"es2019"