Merge branch 'dev' of github.com:blockworks-foundation/mango-v4 into dev

This commit is contained in:
Riordan Panayides 2022-11-04 17:21:44 +00:00
commit 2b694e553f
55 changed files with 1695 additions and 2016 deletions

View File

@ -59,8 +59,8 @@ jobs:
tags: |
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:${{ github.sha }}
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:latest
cache-from: type=registry,ref=us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:buildcache
cache-to: type=registry,ref=us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:buildcache,mode=max
cache-from: type=gha
cache-to: type=gha,mode=max
# Build and push the liquidator runtime image
- name: Build and Push Liquidator
uses: docker/build-push-action@v2

5
.gitignore vendored
View File

@ -18,9 +18,4 @@ programs/mango-v4/src/lib-expanded.rs
keeper/.env
ts/client/**/*.js
ts/client/**/*.js.map
migrations/*.js
migrations/*.js.map
ts/client/src/scripts/archive/ts.ts

View File

@ -6,6 +6,8 @@ Update this for each mainnet deployment.
## mainnet
Oct 8, 2022 at 14:38:31 Central European Summer Time
https://explorer.solana.com/tx/3m8EDohkgwJZyiwpGXztBWARWQVxyhnSNDVuH467D7FPS2wxJerr79HhdhDEed5hpConHgGsKHvxtW1HJP6GixX9

4
Cargo.lock generated
View File

@ -3174,9 +3174,9 @@ dependencies = [
[[package]]
name = "lz4-sys"
version = "1.9.3"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7be8908e2ed6f31c02db8a9fa962f03e36c53fbfde437363eae3306b85d7e17"
checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900"
dependencies = [
"cc",
"libc",

View File

@ -1,23 +1,29 @@
# syntax = docker/dockerfile:1.2
# Base image containing all binaries, deployed to gcr.io/mango-markets/mango-v4:latest
FROM rust:1.60 as build
FROM rust:1.60 as base
RUN cargo install cargo-chef --locked
RUN rustup component add rustfmt
RUN apt-get update && apt-get -y install clang cmake
WORKDIR /app
COPY ./ .
FROM base as plan
COPY . .
# Hack to prevent a ghost member lib/init
RUN sed -i 's|lib/\*|lib/checked_math|' Cargo.toml
# Hack to prevent local serum_dex manifests conflicting with cargo dependency
RUN rm -rf anchor/tests
RUN cargo chef prepare --bin keeper --recipe-path recipe-keeper.json
RUN cargo chef prepare --bin liquidator --recipe-path recipe-liquidator.json
# Mount cache for downloaded and compiled dependencies
RUN --mount=type=cache,mode=0777,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \
--mount=type=cache,mode=0777,target=target \
cargo build --release --bins
# Copy bins out of cache
RUN --mount=type=cache,mode=0777,target=target mkdir .bin && cp target/release/keeper target/release/liquidator .bin/
FROM base as build
COPY --from=plan /app/recipe-*.json .
RUN cargo chef cook --release --recipe-path recipe-keeper.json --bin keeper
RUN cargo chef cook --release --recipe-path recipe-liquidator.json --bin liquidator
COPY . .
FROM debian:bullseye-slim as run
RUN apt-get update && apt-get -y install ca-certificates libc6
COPY --from=build /app/.bin/* /usr/local/bin/
COPY --from=build /app/target/release/keeper /usr/local/bin/
COPY --from=build /app/target/release/liquidator /usr/local/bin/
RUN adduser --system --group --no-create-home mangouser
USER mangouser

View File

@ -42,9 +42,8 @@ async function createMints(
program: anchor.Program<MangoV4>,
payer: anchor.web3.Keypair,
admin,
) {
let mintsMap: Partial<Record<keyof typeof MINTS, spl.Token>>;
let mints: spl.Token[] = [];
): Promise<Partial<Record<keyof typeof MINTS, spl.Token>>> {
const mints: spl.Token[] = [];
for (let i = 0; i < 2; i++) {
mints.push(
await spl.Token.createMint(
@ -57,7 +56,7 @@ async function createMints(
),
);
}
mintsMap = {
const mintsMap = {
USDC: mints[0],
BTC: mints[1],
};
@ -72,20 +71,20 @@ async function createUsers(
group: Group,
connection: Connection,
programId: PublicKey,
) {
let users: TestUser[] = [];
): Promise<TestUser[]> {
const users: TestUser[] = [];
for (let i = 0; i < NUM_USERS; i++) {
let user = anchor.web3.Keypair.generate();
const user = anchor.web3.Keypair.generate();
await provider.connection.requestAirdrop(
user.publicKey,
LAMPORTS_PER_SOL * 1000,
);
let tokenAccounts: spl.AccountInfo[] = [];
for (let mintKey in mintsMap) {
let mint: spl.Token = mintsMap[mintKey];
let tokenAccount = await mint.getOrCreateAssociatedAccountInfo(
const tokenAccounts: spl.AccountInfo[] = [];
for (const mintKey in mintsMap) {
const mint: spl.Token = mintsMap[mintKey];
const tokenAccount = await mint.getOrCreateAssociatedAccountInfo(
user.publicKey,
);
await mint.mintTo(tokenAccount.address, payer, [], 1_000_000_000_000_000);
@ -100,14 +99,10 @@ async function createUsers(
),
'devnet',
programId,
{},
'get-program-accounts',
{ idsSource: 'get-program-accounts' },
);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
await mangoAccount!.reload(client);
console.log('created user ' + i);
@ -123,12 +118,12 @@ async function createUsers(
}
describe('mango-v4', () => {
let programId = new PublicKey(PROGRAM_ID);
const programId = new PublicKey(PROGRAM_ID);
// Configure the client to use the local cluster.
const envProvider = anchor.AnchorProvider.env();
anchor.setProvider(envProvider);
let envProviderWallet = envProvider.wallet;
let envProviderPayer = (envProviderWallet as NodeWallet).payer;
const envProviderWallet = envProvider.wallet;
const envProviderPayer = (envProviderWallet as NodeWallet).payer;
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
@ -158,13 +153,9 @@ describe('mango-v4', () => {
// Passing devnet as the cluster here - client cannot accept localnet
// I think this is only for getting the serum market though?
envClient = await MangoClient.connect(
envProvider,
'devnet',
programId,
{},
'get-program-accounts',
);
envClient = await MangoClient.connect(envProvider, 'devnet', programId, {
idsSource: 'get-program-accounts',
});
await envClient.groupCreate(groupNum, true, 1, insuranceMintPk);
group = await envClient.getGroupForCreator(adminPk, groupNum);
@ -358,14 +349,10 @@ describe('mango-v4', () => {
),
'devnet',
programId,
{},
'get-program-accounts',
{ idsSource: 'get-program-accounts' },
);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
users[0].keypair.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
await mangoAccount!.reload(client);
await client.tokenDeposit(
@ -384,7 +371,7 @@ describe('mango-v4', () => {
);
await mangoAccount!.reload(client);
let banks = await envClient.getBanksForGroup(group);
const banks = await envClient.getBanksForGroup(group);
assert.equal(
mangoAccount!.getTokenBalanceUi(banks[0]),
100.5,
@ -524,21 +511,19 @@ describe('mango-v4', () => {
assert.equal(makerOrders.length, 1, 'Maker still has one open order');
assert.equal(makerOrders[0].uiSize, 1.0, 'Size reduced');
let makerPerps = makerAccount.perpActive();
const makerPerps = makerAccount.perpActive();
assert.equal(makerPerps.length, 1, 'Maker has perp position');
assert.equal(makerPerps[0].marketIndex, 0, 'Market index matches');
assert.equal(
makerPerps[0].basePositionLots,
10000,
assert.isTrue(
makerPerps[0].basePositionLots.eq(new anchor.BN(10000)),
'base position correct',
);
let takerPerps = takerAccount.perpActive();
const takerPerps = takerAccount.perpActive();
assert.equal(takerPerps.length, 1, 'Taker has perp position');
assert.equal(takerPerps[0].marketIndex, 0, 'Market index matches');
assert.equal(
takerPerps[0].basePositionLots,
-10000,
assert.isTrue(
takerPerps[0].basePositionLots.eq(new anchor.BN(-10000)),
'base position correct',
);

View File

@ -27,7 +27,7 @@ pub fn new(
n_banks: active_token_len,
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len,
begin_serum3: active_token_len * 2 + active_perp_len * 2,
};
mango_v4::state::new_health_cache(&account.borrow(), &retriever).context("make health cache")
}

View File

@ -197,7 +197,7 @@ async fn main() -> anyhow::Result<()> {
let mut metric_websocket_queue_len = metrics.register_u64("websocket_queue_length".into());
let mut metric_snapshot_queue_len = metrics.register_u64("snapshot_queue_length".into());
let mut metric_mango_accounts = metrics.register_u64("mango_accouns".into());
let mut metric_mango_accounts = metrics.register_u64("mango_accounts".into());
//
// mango client setup

View File

@ -1,6 +1,6 @@
{
"name": "@blockworks-foundation/mango-v4",
"version": "0.0.1-beta.3",
"version": "0.0.1-beta.5",
"description": "Typescript Client for mango-v4 program.",
"repository": "https://github.com/blockworks-foundation/mango-v4",
"author": {
@ -45,6 +45,7 @@
"cross-fetch": "^3.1.5",
"eslint": "^7.28.0",
"eslint-config-prettier": "^7.2.0",
"ftx-api": "^1.1.13",
"mocha": "^9.1.3",
"prettier": "^2.0.5",
"ts-mocha": "^10.0.0",

View File

@ -83,7 +83,7 @@ pub struct MangoAccount {
pub perp_spot_transfers: i64,
/// Init health as calculated during HealthReginBegin, rounded up.
pub health_region_pre_init_health: i64,
pub health_region_begin_init_health: i64,
pub reserved: [u8; 240],
@ -118,8 +118,7 @@ impl MangoAccount {
bump: 0,
padding: Default::default(),
net_deposits: 0,
perp_spot_transfers: 0,
health_region_pre_init_health: 0,
health_region_begin_init_health: 0,
reserved: [0; 240],
header_version: DEFAULT_MANGO_ACCOUNT_VERSION,
padding3: Default::default(),
@ -131,6 +130,7 @@ impl MangoAccount {
perps: vec![PerpPosition::default(); 4],
padding7: Default::default(),
perp_open_orders: vec![PerpOpenOrder::default(); 6],
perp_spot_transfers: 0,
}
}
@ -1095,8 +1095,7 @@ mod tests {
account.in_health_region = 3;
account.bump = 4;
account.net_deposits = 5;
account.perp_spot_transfers = 6;
account.health_region_pre_init_health = 7;
account.health_region_begin_init_health = 7;
account.tokens.resize(8, TokenPosition::default());
account.tokens[0].token_index = 8;
account.serum3.resize(8, Serum3Orders::default());
@ -1125,7 +1124,7 @@ mod tests {
account2.fixed.perp_spot_transfers
);
assert_eq!(
account.health_region_pre_init_health,
account.health_region_begin_init_health,
account2.fixed.health_region_begin_init_health
);
assert_eq!(

View File

@ -95,17 +95,7 @@ export class Group {
public vaultAmountsMap: Map<string, BN>,
) {}
public async reloadAll(client: MangoClient): Promise<void> {
let ids: Id | undefined = undefined;
if (client.idsSource === 'api') {
ids = await Id.fromApi(this.publicKey);
} else if (client.idsSource === 'static') {
ids = Id.fromIdsByPk(this.publicKey);
} else {
ids = undefined;
}
public async reloadAll(client: MangoClient, ids?: Id): Promise<void> {
// console.time('group.reload');
await Promise.all([
this.reloadAlts(client),

View File

@ -85,10 +85,17 @@ describe('Health Cache', () => {
I80F48.fromNumber(-310),
new BN(7),
new BN(11),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
new BN(1),
new BN(2),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
new BN(0),
new BN(0),
0,
0,
new BN(0),
new BN(0),
new BN(0),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
@ -186,10 +193,17 @@ describe('Health Cache', () => {
I80F48.fromNumber(fixture.perp1[1]),
new BN(fixture.perp1[2]),
new BN(fixture.perp1[3]),
new BN(0),
new BN(0),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
0,
0,
new BN(0),
new BN(0),
new BN(0),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);

View File

@ -1,7 +1,7 @@
import { BN } from '@project-serum/anchor';
import { AnchorProvider, BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market';
import { AccountInfo, PublicKey } from '@solana/web3.js';
import { AccountInfo, PublicKey, TransactionSignature } from '@solana/web3.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
@ -12,12 +12,11 @@ import { HealthCache } from './healthCache';
import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp';
import { MarketIndex, Serum3Side } from './serum3';
export class MangoAccount {
public name: string;
public tokens: TokenPosition[];
public serum3: Serum3Orders[];
public perps: PerpPosition[];
public perpOpenOrders: PerpOo[];
public name: string;
public netDeposits: BN;
static from(
publicKey: PublicKey,
@ -26,29 +25,31 @@ export class MangoAccount {
owner: PublicKey;
name: number[];
delegate: PublicKey;
beingLiquidated: number;
accountNum: number;
bump: number;
beingLiquidated: number;
inHealthRegion: number;
netDeposits: BN;
netSettled: BN;
perpSpotTransfers: BN;
healthRegionBeginInitHealth: BN;
headerVersion: number;
tokens: unknown;
serum3: unknown;
perps: unknown;
perpOpenOrders: unknown;
},
) {
): MangoAccount {
return new MangoAccount(
publicKey,
obj.group,
obj.owner,
obj.name,
obj.delegate,
obj.beingLiquidated,
obj.accountNum,
obj.bump,
obj.beingLiquidated == 1,
obj.inHealthRegion == 1,
obj.netDeposits,
obj.netSettled,
obj.perpSpotTransfers,
obj.healthRegionBeginInitHealth,
obj.headerVersion,
obj.tokens as TokenPositionDto[],
obj.serum3 as Serum3PositionDto[],
@ -64,12 +65,13 @@ export class MangoAccount {
public owner: PublicKey,
name: number[],
public delegate: PublicKey,
beingLiquidated: number,
public accountNum: number,
bump: number,
netDeposits: BN,
netSettled: BN,
headerVersion: number,
public beingLiquidated: boolean,
public inHealthRegion: boolean,
public netDeposits: BN,
public perpSpotTransfers: BN,
public healthRegionBeginInitHealth: BN,
public headerVersion: number,
tokens: TokenPositionDto[],
serum3: Serum3PositionDto[],
perps: PerpPositionDto[],
@ -81,17 +83,16 @@ export class MangoAccount {
this.serum3 = serum3.map((dto) => Serum3Orders.from(dto));
this.perps = perps.map((dto) => PerpPosition.from(dto));
this.perpOpenOrders = perpOpenOrders.map((dto) => PerpOo.from(dto));
this.netDeposits = netDeposits;
}
async reload(client: MangoClient): Promise<MangoAccount> {
public async reload(client: MangoClient): Promise<MangoAccount> {
const mangoAccount = await client.getMangoAccount(this);
await mangoAccount.reloadAccountData(client);
Object.assign(this, mangoAccount);
return mangoAccount;
}
async reloadWithSlot(
public async reloadWithSlot(
client: MangoClient,
): Promise<{ value: MangoAccount; slot: number }> {
const resp = await client.getMangoAccountWithSlot(this.publicKey);
@ -127,46 +128,59 @@ export class MangoAccount {
return this;
}
tokensActive(): TokenPosition[] {
public isDelegate(client: MangoClient): boolean {
return this.delegate.equals(
(client.program.provider as AnchorProvider).wallet.publicKey,
);
}
public tokensActive(): TokenPosition[] {
return this.tokens.filter((token) => token.isActive());
}
serum3Active(): Serum3Orders[] {
public serum3Active(): Serum3Orders[] {
return this.serum3.filter((serum3) => serum3.isActive());
}
perpActive(): PerpPosition[] {
public perpActive(): PerpPosition[] {
return this.perps.filter((perp) => perp.isActive());
}
perpOrdersActive(): PerpOo[] {
public perpOrdersActive(): PerpOo[] {
return this.perpOpenOrders.filter(
(oo) => oo.orderMarket !== PerpOo.OrderMarketUnset,
);
}
getToken(tokenIndex: TokenIndex): TokenPosition | undefined {
public getToken(tokenIndex: TokenIndex): TokenPosition | undefined {
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
}
getSerum3Account(marketIndex: MarketIndex): Serum3Orders | undefined {
public getSerum3Account(marketIndex: MarketIndex): Serum3Orders | undefined {
return this.serum3.find((sa) => sa.marketIndex == marketIndex);
}
getPerpPosition(perpMarketIndex: PerpMarketIndex): PerpPosition | undefined {
public getPerpPosition(
perpMarketIndex: PerpMarketIndex,
useEventQueue?: boolean,
): PerpPosition | undefined {
return this.perps.find((pp) => pp.marketIndex == perpMarketIndex);
}
getPerpPositionUi(group: Group, perpMarketIndex: PerpMarketIndex): number {
public getPerpPositionUi(
group: Group,
perpMarketIndex: PerpMarketIndex,
useEventQueue?: boolean,
): number {
const pp = this.perps.find((pp) => pp.marketIndex == perpMarketIndex);
if (!pp) {
throw new Error(`No position found for PerpMarket ${perpMarketIndex}!`);
}
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
return pp.getBasePositionUi(perpMarket);
return pp.getBasePositionUi(perpMarket, useEventQueue);
}
getSerum3OoAccount(marketIndex: MarketIndex): OpenOrders {
public getSerum3OoAccount(marketIndex: MarketIndex): OpenOrders {
const oo: OpenOrders | undefined =
this.serum3OosMapByMarketIndex.get(marketIndex);
@ -189,7 +203,7 @@ export class MangoAccount {
* @param bank
* @returns native balance for a token, is signed
*/
getTokenBalance(bank: Bank): I80F48 {
public getTokenBalance(bank: Bank): I80F48 {
const tp = this.getToken(bank.tokenIndex);
return tp ? tp.balance(bank) : ZERO_I80F48();
}
@ -199,7 +213,7 @@ export class MangoAccount {
* @param bank
* @returns native deposits for a token, 0 if position has borrows
*/
getTokenDeposits(bank: Bank): I80F48 {
public getTokenDeposits(bank: Bank): I80F48 {
const tp = this.getToken(bank.tokenIndex);
return tp ? tp.deposits(bank) : ZERO_I80F48();
}
@ -209,7 +223,7 @@ export class MangoAccount {
* @param bank
* @returns native borrows for a token, 0 if position has deposits
*/
getTokenBorrows(bank: Bank): I80F48 {
public getTokenBorrows(bank: Bank): I80F48 {
const tp = this.getToken(bank.tokenIndex);
return tp ? tp.borrows(bank) : ZERO_I80F48();
}
@ -219,7 +233,7 @@ export class MangoAccount {
* @param bank
* @returns UI balance for a token, is signed
*/
getTokenBalanceUi(bank: Bank): number {
public getTokenBalanceUi(bank: Bank): number {
const tp = this.getToken(bank.tokenIndex);
return tp ? tp.balanceUi(bank) : 0;
}
@ -229,7 +243,7 @@ export class MangoAccount {
* @param bank
* @returns UI deposits for a token, 0 or more
*/
getTokenDepositsUi(bank: Bank): number {
public getTokenDepositsUi(bank: Bank): number {
const ta = this.getToken(bank.tokenIndex);
return ta ? ta.depositsUi(bank) : 0;
}
@ -239,7 +253,7 @@ export class MangoAccount {
* @param bank
* @returns UI borrows for a token, 0 or less
*/
getTokenBorrowsUi(bank: Bank): number {
public getTokenBorrowsUi(bank: Bank): number {
const ta = this.getToken(bank.tokenIndex);
return ta ? ta.borrowsUi(bank) : 0;
}
@ -249,7 +263,7 @@ export class MangoAccount {
* @param healthType
* @returns raw health number, in native quote
*/
getHealth(group: Group, healthType: HealthType): I80F48 {
public getHealth(group: Group, healthType: HealthType): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.health(healthType);
}
@ -260,7 +274,7 @@ export class MangoAccount {
* @param healthType
* @returns health ratio, in percentage form
*/
getHealthRatio(group: Group, healthType: HealthType): I80F48 {
public getHealthRatio(group: Group, healthType: HealthType): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.healthRatio(healthType);
}
@ -270,7 +284,7 @@ export class MangoAccount {
* @param healthType
* @returns health ratio, in percentage form, capped to 100
*/
getHealthRatioUi(group: Group, healthType: HealthType): number {
public getHealthRatioUi(group: Group, healthType: HealthType): number {
const ratio = this.getHealthRatio(group, healthType).toNumber();
return ratio > 100 ? 100 : Math.trunc(ratio);
}
@ -279,7 +293,7 @@ export class MangoAccount {
* Sum of all the assets i.e. token deposits, borrows, total assets in spot open orders, and perps positions.
* @returns equity, in native quote
*/
getEquity(group: Group): I80F48 {
public getEquity(group: Group): I80F48 {
const tokensMap = new Map<number, I80F48>();
for (const tp of this.tokensActive()) {
const bank = group.getFirstBankByTokenIndex(tp.tokenIndex);
@ -322,7 +336,7 @@ export class MangoAccount {
* The amount of native quote you could withdraw against your existing assets.
* @returns collateral value, in native quote
*/
getCollateralValue(group: Group): I80F48 {
public getCollateralValue(group: Group): I80F48 {
return this.getHealth(group, HealthType.init);
}
@ -330,7 +344,7 @@ export class MangoAccount {
* Sum of all positive assets.
* @returns assets, in native quote
*/
getAssetsValue(group: Group, healthType: HealthType): I80F48 {
public getAssetsValue(group: Group, healthType: HealthType): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.assets(healthType);
}
@ -339,7 +353,7 @@ export class MangoAccount {
* Sum of all negative assets.
* @returns liabs, in native quote
*/
getLiabsValue(group: Group, healthType: HealthType): I80F48 {
public getLiabsValue(group: Group, healthType: HealthType): I80F48 {
const hc = HealthCache.fromMangoAccount(group, this);
return hc.liabs(healthType);
}
@ -349,7 +363,7 @@ export class MangoAccount {
* PNL is defined here as spot value + serum3 open orders value + perp value - net deposits value (evaluated at native quote price at the time of the deposit/withdraw)
* spot value + serum3 open orders value + perp value is returned by getEquity (open orders values are added to spot token values implicitly)
*/
getPnl(group: Group): I80F48 {
public getPnl(group: Group): I80F48 {
return this.getEquity(group)?.add(
I80F48.fromI64(this.netDeposits).mul(I80F48.fromNumber(-1)),
);
@ -359,7 +373,10 @@ export class MangoAccount {
* The amount of given native token you can withdraw including borrows, considering all existing assets as collateral.
* @returns amount of given native token you can borrow, considering all existing assets as collateral, in native token
*/
getMaxWithdrawWithBorrowForToken(group: Group, mintPk: PublicKey): I80F48 {
public getMaxWithdrawWithBorrowForToken(
group: Group,
mintPk: PublicKey,
): I80F48 {
const tokenBank: Bank = group.getFirstBankByMint(mintPk);
const initHealth = this.getHealth(group, HealthType.init);
@ -403,7 +420,10 @@ export class MangoAccount {
return maxBorrowNativeWithoutFees.add(existingTokenDeposits);
}
getMaxWithdrawWithBorrowForTokenUi(group: Group, mintPk: PublicKey): number {
public getMaxWithdrawWithBorrowForTokenUi(
group: Group,
mintPk: PublicKey,
): number {
const maxWithdrawWithBorrow = this.getMaxWithdrawWithBorrowForToken(
group,
mintPk,
@ -453,7 +473,7 @@ export class MangoAccount {
* Note: health ratio is technically if liabs are 0
* @returns health ratio, in percentage form
*/
simHealthRatioWithTokenPositionUiChanges(
public simHealthRatioWithTokenPositionUiChanges(
group: Group,
uiTokenChanges: {
uiTokenAmount: number;
@ -485,7 +505,7 @@ export class MangoAccount {
): Promise<OpenOrders[]> {
const response =
await client.program.provider.connection.getMultipleAccountsInfo(
this.serum3.map((s) => s.openOrders),
this.serum3Active().map((s) => s.openOrders),
);
const accounts = response.filter((a): a is AccountInfo<Buffer> =>
Boolean(a),
@ -689,6 +709,42 @@ export class MangoAccount {
.toNumber();
}
// TODO: don't send a settle instruction if there's nothing to settle
public async serum3SettleFundsForAllMarkets(
client: MangoClient,
group: Group,
): Promise<TransactionSignature[]> {
// Future: collect ixs, batch them, and send them in fewer txs
return await Promise.all(
this.serum3Active().map((s) => {
const serum3Market = group.getSerum3MarketByMarketIndex(s.marketIndex);
return client.serum3SettleFunds(
group,
this,
serum3Market.serumMarketExternal,
);
}),
);
}
// TODO: cancel until all are cancelled
public async serum3CancelAllOrdersForAllMarkets(
client: MangoClient,
group: Group,
): Promise<TransactionSignature[]> {
// Future: collect ixs, batch them, and send them in in fewer txs
return await Promise.all(
this.serum3Active().map((s) => {
const serum3Market = group.getSerum3MarketByMarketIndex(s.marketIndex);
return client.serum3CancelAllOrders(
group,
this,
serum3Market.serumMarketExternal,
);
}),
);
}
/**
*
* @param group
@ -1000,10 +1056,17 @@ export class PerpPosition {
I80F48.from(dto.quotePositionNative),
dto.bidsBaseLots,
dto.asksBaseLots,
dto.takerBaseLots,
dto.takerQuoteLots,
I80F48.from(dto.longSettledFunding),
I80F48.from(dto.shortSettledFunding),
dto.takerBaseLots,
dto.takerQuoteLots,
dto.takerBaseLots,
dto.takerQuoteLots,
dto.cumulativeLongFunding,
dto.cumulativeShortFunding,
dto.makerVolume,
dto.takerVolume,
dto.perpSpotTransfers,
);
}
@ -1016,10 +1079,17 @@ export class PerpPosition {
ZERO_I80F48(),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
ZERO_I80F48(),
ZERO_I80F48(),
new BN(0),
new BN(0),
new BN(0),
new BN(0),
0,
0,
new BN(0),
new BN(0),
new BN(0),
);
}
@ -1027,20 +1097,34 @@ export class PerpPosition {
public marketIndex: PerpMarketIndex,
public basePositionLots: BN,
public quotePositionNative: I80F48,
public quoteEntryNative: BN,
public quoteRunningNative: BN,
public longSettledFunding: I80F48,
public shortSettledFunding: I80F48,
public bidsBaseLots: BN,
public asksBaseLots: BN,
public takerBaseLots: BN,
public takerQuoteLots: BN,
public longSettledFunding: I80F48,
public shortSettledFunding: I80F48,
public cumulativeLongFunding: number,
public cumulativeShortFunding: number,
public makerVolume: BN,
public takerVolume: BN,
public perpSpotTransfers: BN,
) {}
isActive(): boolean {
return this.marketIndex != PerpPosition.PerpMarketIndexUnset;
}
public getBasePositionUi(perpMarket: PerpMarket): number {
return perpMarket.baseLotsToUi(this.basePositionLots);
public getBasePositionUi(
perpMarket: PerpMarket,
useEventQueue?: boolean,
): number {
return perpMarket.baseLotsToUi(
useEventQueue
? this.basePositionLots.add(this.takerBaseLots)
: this.basePositionLots,
);
}
public getUnsettledFunding(perpMarket: PerpMarket): I80F48 {
@ -1090,15 +1174,21 @@ export class PerpPosition {
export class PerpPositionDto {
constructor(
public marketIndex: number,
public reserved: [],
public basePositionLots: BN,
public quotePositionNative: { val: BN },
public quoteEntryNative: BN,
public quoteRunningNative: BN,
public longSettledFunding: I80F48Dto,
public shortSettledFunding: I80F48Dto,
public bidsBaseLots: BN,
public asksBaseLots: BN,
public takerBaseLots: BN,
public takerQuoteLots: BN,
public longSettledFunding: I80F48Dto,
public shortSettledFunding: I80F48Dto,
public cumulativeLongFunding: number,
public cumulativeShortFunding: number,
public makerVolume: BN,
public takerVolume: BN,
public perpSpotTransfers: BN,
) {}
}

View File

@ -5,7 +5,7 @@ import Big from 'big.js';
import { MangoClient } from '../client';
import { I80F48, I80F48Dto } from '../numbers/I80F48';
import { As, toNative, U64_MAX_BN } from '../utils';
import { OracleConfig, QUOTE_DECIMALS } from './bank';
import { OracleConfig, QUOTE_DECIMALS, TokenIndex } from './bank';
export type PerpMarketIndex = number & As<'perp-market-index'>;
@ -22,22 +22,23 @@ export class PerpMarket {
public maxFunding: I80F48;
public longFunding: I80F48;
public shortFunding: I80F48;
public openInterest: BN;
public seqNum: BN;
public feesAccrued: I80F48;
public feesSettled: I80F48;
priceLotsToUiConverter: number;
baseLotsToUiConverter: number;
quoteLotsToUiConverter: number;
public _price: I80F48;
public _uiPrice: number;
private priceLotsToUiConverter: number;
private baseLotsToUiConverter: number;
private quoteLotsToUiConverter: number;
static from(
publicKey: PublicKey,
obj: {
group: PublicKey;
settleTokenIndex: number;
perpMarketIndex: number;
trustedMarket: number;
groupInsuranceFund: number;
name: number[];
oracle: PublicKey;
oracleConfig: OracleConfig;
@ -62,17 +63,22 @@ export class PerpMarket {
openInterest: BN;
seqNum: BN;
feesAccrued: I80F48Dto;
feesSettled: I80F48Dto;
bump: number;
baseDecimals: number;
registrationTime: BN;
feesSettled: I80F48Dto;
feePenalty: number;
settleFeeFlat: number;
settleFeeAmountThreshold: number;
settleFeeFractionLowHealth: number;
},
): PerpMarket {
return new PerpMarket(
publicKey,
obj.group,
obj.settleTokenIndex as TokenIndex,
obj.perpMarketIndex as PerpMarketIndex,
obj.trustedMarket == 1,
obj.groupInsuranceFund == 1,
obj.name,
obj.oracle,
obj.oracleConfig,
@ -97,18 +103,23 @@ export class PerpMarket {
obj.openInterest,
obj.seqNum,
obj.feesAccrued,
obj.feesSettled,
obj.bump,
obj.baseDecimals,
obj.registrationTime,
obj.feesSettled,
obj.feePenalty,
obj.settleFeeFlat,
obj.settleFeeAmountThreshold,
obj.settleFeeFractionLowHealth,
);
}
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public settleTokenIndex: TokenIndex,
public perpMarketIndex: PerpMarketIndex, // TODO rename to marketIndex?
public trustedMarket: boolean,
public groupInsuranceFund: boolean,
name: number[],
public oracle: PublicKey,
oracleConfig: OracleConfig,
@ -129,14 +140,17 @@ export class PerpMarket {
public impactQuantity: BN,
longFunding: I80F48Dto,
shortFunding: I80F48Dto,
fundingLastUpdated: BN,
openInterest: BN,
seqNum: BN,
public fundingLastUpdated: BN,
public openInterest: BN,
public seqNum: BN,
feesAccrued: I80F48Dto,
feesSettled: I80F48Dto,
bump: number,
public baseDecimals: number,
public registrationTime: BN,
feesSettled: I80F48Dto,
public feePenalty: number,
public settleFeeFlat: number,
public settleFeeAmountThreshold: number,
public settleFeeFractionLowHealth: number,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
this.maintAssetWeight = I80F48.from(maintAssetWeight);
@ -150,8 +164,6 @@ export class PerpMarket {
this.maxFunding = I80F48.from(maxFunding);
this.longFunding = I80F48.from(longFunding);
this.shortFunding = I80F48.from(shortFunding);
this.openInterest = openInterest;
this.seqNum = seqNum;
this.feesAccrued = I80F48.from(feesAccrued);
this.feesSettled = I80F48.from(feesSettled);
@ -187,6 +199,15 @@ export class PerpMarket {
}
return this._uiPrice;
}
get minOrderSize(): number {
return this.baseLotsToUiConverter;
}
get tickSize(): number {
return this.priceLotsToUiConverter;
}
public async loadAsks(client: MangoClient): Promise<BookSide> {
const asks = await client.program.account.bookSide.fetch(this.asks);
return BookSide.from(client, this, BookSideType.asks, asks);
@ -551,6 +572,7 @@ export class PerpOrder {
side,
leafNode.timestamp,
expiryTimestamp,
perpMarket.perpMarketIndex,
);
}
@ -567,7 +589,16 @@ export class PerpOrder {
public side: PerpOrderSide,
public timestamp: BN,
public expiryTimestamp: BN,
public perpMarketIndex: number,
) {}
get price(): number {
return this.uiPrice;
}
get size(): number {
return this.uiSize;
}
}
export class PerpEventQueue {

View File

@ -4,10 +4,10 @@ import { Cluster, PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { As } from '../utils';
import { TokenIndex } from './bank';
import { Group } from './group';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
export type MarketIndex = number & As<'market-index'>;
@ -23,7 +23,6 @@ export class Serum3Market {
serumProgram: PublicKey;
serumMarketExternal: PublicKey;
marketIndex: number;
bump: number;
registrationTime: BN;
},
): Serum3Market {

View File

@ -68,8 +68,15 @@ enum AccountRetriever {
export type IdsSource = 'api' | 'static' | 'get-program-accounts';
export type MangoClientOptions = {
idsSource?: IdsSource;
postSendTxCallback?: ({ txid }: { txid: string }) => void;
prioritizationFee?: number;
};
// TODO: replace ui values with native as input wherever possible
export class MangoClient {
private idsSource: IdsSource;
private postSendTxCallback?: ({ txid }) => void;
private prioritizationFee: number;
@ -77,12 +84,9 @@ export class MangoClient {
public program: Program<MangoV4>,
public programId: PublicKey,
public cluster: Cluster,
public opts: {
postSendTxCallback?: ({ txid }: { txid: string }) => void;
prioritizationFee?: number;
} = {},
public idsSource: IdsSource = 'api',
public opts: MangoClientOptions = {},
) {
this.idsSource = opts?.idsSource || 'api';
this.prioritizationFee = opts?.prioritizationFee || 0;
this.postSendTxCallback = opts?.postSendTxCallback;
// TODO: evil side effect, but limited backtraces are a nightmare
@ -148,7 +152,8 @@ export class MangoClient {
public async getGroup(groupPk: PublicKey): Promise<Group> {
const groupAccount = await this.program.account.group.fetch(groupPk);
const group = Group.from(groupPk, groupAccount);
await group.reloadAll(this);
const ids: Id | undefined = await this.getIds(groupPk);
await group.reloadAll(this, ids);
return group;
}
@ -196,6 +201,17 @@ export class MangoClient {
return groups[0];
}
public async getIds(groupPk: PublicKey): Promise<Id | undefined> {
switch (this.idsSource) {
case 'api':
return await Id.fromApi(groupPk);
case 'get-program-accounts':
return undefined;
case 'static':
return Id.fromIdsByPk(groupPk);
}
}
// Tokens/Banks
public async tokenRegister(
@ -1211,7 +1227,7 @@ export class MangoClient {
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
limit: number,
limit?: number,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
@ -1222,7 +1238,7 @@ export class MangoClient {
)!;
const ix = await this.program.methods
.serum3CancelAllOrders(limit)
.serum3CancelAllOrders(limit ? limit : 10)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
@ -1683,6 +1699,49 @@ export class MangoClient {
.instruction();
}
public async perpCancelOrderIx(
group: Group,
mangoAccount: MangoAccount,
perpMarketIndex: PerpMarketIndex,
orderId: BN,
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
return await this.program.methods
.perpCancelOrder(new BN(orderId))
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
perpMarket: perpMarket.publicKey,
asks: perpMarket.asks,
bids: perpMarket.bids,
})
.instruction();
}
public async perpCancelOrder(
group: Group,
mangoAccount: MangoAccount,
perpMarketIndex: PerpMarketIndex,
orderId: BN,
): Promise<TransactionSignature> {
return await sendTransaction(
this.program.provider as AnchorProvider,
[
await this.perpCancelOrderIx(
group,
mangoAccount,
perpMarketIndex,
orderId,
),
],
group.addressLookupTablesList,
{
postSendTxCallback: this.postSendTxCallback,
},
);
}
public async perpCancelAllOrders(
group: Group,
mangoAccount: MangoAccount,
@ -2169,14 +2228,76 @@ export class MangoClient {
.rpc();
}
public async healthRegionBeginIx(
group: Group,
account: MangoAccount,
banks: Bank[] = [],
perpMarkets: PerpMarket[] = [],
): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[account],
[...banks],
[...perpMarkets],
);
const parsedHealthAccounts = healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable: false,
isSigner: false,
} as AccountMeta),
);
return await this.program.methods
.healthRegionBegin()
.accounts({
account: account.publicKey,
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.remainingAccounts(parsedHealthAccounts)
.instruction();
}
public async healthRegionEndIx(
group: Group,
account: MangoAccount,
banks: Bank[] = [],
perpMarkets: PerpMarket[] = [],
): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[account],
[...banks],
[...perpMarkets],
);
const parsedHealthAccounts = healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable: false,
isSigner: false,
} as AccountMeta),
);
return await this.program.methods
.healthRegionEnd()
.accounts({ account: account.publicKey })
.remainingAccounts(parsedHealthAccounts)
.instruction();
}
/// static
static connect(
provider: Provider,
cluster: Cluster,
programId: PublicKey,
opts: any = {},
getIdsFromApi: IdsSource = 'api',
opts?: MangoClientOptions,
): MangoClient {
const idl = IDL;
@ -2185,7 +2306,6 @@ export class MangoClient {
programId,
cluster,
opts,
getIdsFromApi,
);
}

View File

@ -2,9 +2,9 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { coder } from '@project-serum/anchor/dist/cjs/spl/token';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { I80F48, ZERO_I80F48 } from '../accounts/I80F48';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { toUiDecimals } from '../utils';
const CLUSTER_URL =
@ -15,7 +15,7 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 2);
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
async function main() {
async function main(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
@ -29,8 +29,7 @@ async function main() {
adminProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
{ idsSource: 'get-program-accounts' },
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);

View File

@ -24,7 +24,7 @@ async function debugUser(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
) {
): Promise<void> {
console.log(mangoAccount.toString(group));
await mangoAccount.reload(client);
@ -80,7 +80,9 @@ async function debugUser(
),
);
async function getMaxWithdrawWithBorrowForTokenUiWrapper(token) {
async function getMaxWithdrawWithBorrowForTokenUiWrapper(
token,
): Promise<void> {
console.log(
`mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, ${token}) ` +
mangoAccount.getMaxWithdrawWithBorrowForTokenUi(
@ -93,7 +95,7 @@ async function debugUser(
await getMaxWithdrawWithBorrowForTokenUiWrapper(srcToken);
}
function simHealthRatioWithTokenPositionChangesWrapper(debug, change) {
function simHealthRatioWithTokenPositionChangesWrapper(debug, change): void {
console.log(
`mangoAccount.simHealthRatioWithTokenPositionChanges ${debug}` +
mangoAccount.simHealthRatioWithTokenPositionUiChanges(group, [change]),
@ -110,7 +112,7 @@ async function debugUser(
});
}
function getMaxSourceForTokenSwapWrapper(src, tgt) {
function getMaxSourceForTokenSwapWrapper(src, tgt): void {
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
mangoAccount.getMaxSourceUiForTokenSwap(
@ -129,22 +131,14 @@ async function debugUser(
}
}
function getMaxForPerpWrapper(perpMarket: PerpMarket) {
function getMaxForPerpWrapper(perpMarket: PerpMarket): void {
console.log(
`getMaxQuoteForPerpBidUi ${perpMarket.perpMarketIndex} ` +
mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
perpMarket.price.toNumber(),
),
mangoAccount.getMaxQuoteForPerpBidUi(group, perpMarket.perpMarketIndex),
);
console.log(
`getMaxBaseForPerpAskUi ${perpMarket.perpMarketIndex} ` +
mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
perpMarket.price.toNumber(),
),
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex),
);
}
for (const perpMarket of Array.from(
@ -153,7 +147,7 @@ async function debugUser(
getMaxForPerpWrapper(perpMarket);
}
function getMaxForSerum3Wrapper(serum3Market: Serum3Market) {
function getMaxForSerum3Wrapper(serum3Market: Serum3Market): void {
// if (serum3Market.name !== 'SOL/USDC') return;
console.log(
`getMaxQuoteForSerum3BidUi ${serum3Market.name} ` +
@ -177,7 +171,7 @@ async function debugUser(
}
}
async function main() {
async function main(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
@ -192,8 +186,9 @@ async function main() {
adminProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);

View File

@ -50,8 +50,9 @@ async function createGroup() {
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
console.log(`Creating Group...`);
@ -83,8 +84,9 @@ async function registerTokens() {
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts' /* idsjson service doesn't know about this group yet */,
{
idsSource: 'get-program-accounts',
} /* idsjson service doesn't know about this group yet */,
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);

View File

@ -3626,11 +3626,11 @@ export type MangoV4 = {
"type": "i64"
},
{
"name": "netSettled",
"name": "perpSpotTransfers",
"type": "i64"
},
{
"name": "healthRegionPreInitHealth",
"name": "healthRegionBeginInitHealth",
"docs": [
"Init health as calculated during HealthReginBegin, rounded up."
],
@ -4618,7 +4618,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
16
8
]
}
},
@ -4630,11 +4630,11 @@ export type MangoV4 = {
},
{
"name": "cumulativeDepositInterest",
"type": "f32"
"type": "f64"
},
{
"name": "cumulativeBorrowInterest",
"type": "f32"
"type": "f64"
}
]
}
@ -4792,9 +4792,29 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
64
24
]
}
},
{
"name": "cumulativeLongFunding",
"type": "f64"
},
{
"name": "cumulativeShortFunding",
"type": "f64"
},
{
"name": "makerVolume",
"type": "u64"
},
{
"name": "takerVolume",
"type": "u64"
},
{
"name": "perpSpotTransfers",
"type": "i64"
}
]
}
@ -5561,7 +5581,7 @@ export type MangoV4 = {
},
{
"name": "marketIndex",
"type": "u64",
"type": "u16",
"index": false
},
{
@ -5833,7 +5853,7 @@ export type MangoV4 = {
]
},
{
"name": "UpdateFundingLog",
"name": "PerpUpdateFundingLog",
"fields": [
{
"name": "mangoGroup",
@ -5859,6 +5879,16 @@ export type MangoV4 = {
"name": "price",
"type": "i128",
"index": false
},
{
"name": "feesAccrued",
"type": "i128",
"index": false
},
{
"name": "openInterest",
"type": "i64",
"index": false
}
]
},
@ -5904,6 +5934,16 @@ export type MangoV4 = {
"name": "loanFeeRate",
"type": "i128",
"index": false
},
{
"name": "totalBorrows",
"type": "i128",
"index": false
},
{
"name": "totalDeposits",
"type": "i128",
"index": false
}
]
},
@ -5938,7 +5978,7 @@ export type MangoV4 = {
]
},
{
"name": "LiquidateTokenAndTokenLog",
"name": "TokenLiqWithTokenLog",
"fields": [
{
"name": "mangoGroup",
@ -6075,7 +6115,7 @@ export type MangoV4 = {
]
},
{
"name": "LiquidateTokenBankruptcyLog",
"name": "TokenLiqBankruptcyLog",
"fields": [
{
"name": "mangoGroup",
@ -6144,12 +6184,312 @@ export type MangoV4 = {
},
{
"name": "cumulativeDepositInterest",
"type": "f32",
"type": "f64",
"index": false
},
{
"name": "cumulativeBorrowInterest",
"type": "f32",
"type": "f64",
"index": false
}
]
},
{
"name": "DeactivatePerpPositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "marketIndex",
"type": "u16",
"index": false
},
{
"name": "cumulativeLongFunding",
"type": "f64",
"index": false
},
{
"name": "cumulativeShortFunding",
"type": "f64",
"index": false
},
{
"name": "makerVolume",
"type": "u64",
"index": false
},
{
"name": "takerVolume",
"type": "u64",
"index": false
},
{
"name": "perpSpotTransfers",
"type": "i64",
"index": false
}
]
},
{
"name": "TokenMetaDataLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mint",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "mintDecimals",
"type": "u8",
"index": false
},
{
"name": "oracle",
"type": "publicKey",
"index": false
},
{
"name": "mintInfo",
"type": "publicKey",
"index": false
}
]
},
{
"name": "PerpMarketMetaDataLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarket",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "baseDecimals",
"type": "u8",
"index": false
},
{
"name": "baseLotSize",
"type": "i64",
"index": false
},
{
"name": "quoteLotSize",
"type": "i64",
"index": false
},
{
"name": "oracle",
"type": "publicKey",
"index": false
}
]
},
{
"name": "Serum3RegisterMarketLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "serumMarket",
"type": "publicKey",
"index": false
},
{
"name": "marketIndex",
"type": "u16",
"index": false
},
{
"name": "baseTokenIndex",
"type": "u16",
"index": false
},
{
"name": "quoteTokenIndex",
"type": "u16",
"index": false
},
{
"name": "serumProgram",
"type": "publicKey",
"index": false
},
{
"name": "serumProgramExternal",
"type": "publicKey",
"index": false
}
]
},
{
"name": "PerpLiqBasePositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "baseTransfer",
"type": "i64",
"index": false
},
{
"name": "quoteTransfer",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "PerpLiqBankruptcyLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "insuranceTransfer",
"type": "i128",
"index": false
},
{
"name": "socializedLoss",
"type": "i128",
"index": false
}
]
},
{
"name": "PerpSettlePnlLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccountA",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccountB",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "settlement",
"type": "i128",
"index": false
},
{
"name": "settler",
"type": "publicKey",
"index": false
},
{
"name": "fee",
"type": "i128",
"index": false
}
]
},
{
"name": "PerpSettleFeesLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "settlement",
"type": "i128",
"index": false
}
]
@ -9902,11 +10242,11 @@ export const IDL: MangoV4 = {
"type": "i64"
},
{
"name": "netSettled",
"name": "perpSpotTransfers",
"type": "i64"
},
{
"name": "healthRegionPreInitHealth",
"name": "healthRegionBeginInitHealth",
"docs": [
"Init health as calculated during HealthReginBegin, rounded up."
],
@ -10894,7 +11234,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
16
8
]
}
},
@ -10906,11 +11246,11 @@ export const IDL: MangoV4 = {
},
{
"name": "cumulativeDepositInterest",
"type": "f32"
"type": "f64"
},
{
"name": "cumulativeBorrowInterest",
"type": "f32"
"type": "f64"
}
]
}
@ -11068,9 +11408,29 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
64
24
]
}
},
{
"name": "cumulativeLongFunding",
"type": "f64"
},
{
"name": "cumulativeShortFunding",
"type": "f64"
},
{
"name": "makerVolume",
"type": "u64"
},
{
"name": "takerVolume",
"type": "u64"
},
{
"name": "perpSpotTransfers",
"type": "i64"
}
]
}
@ -11837,7 +12197,7 @@ export const IDL: MangoV4 = {
},
{
"name": "marketIndex",
"type": "u64",
"type": "u16",
"index": false
},
{
@ -12109,7 +12469,7 @@ export const IDL: MangoV4 = {
]
},
{
"name": "UpdateFundingLog",
"name": "PerpUpdateFundingLog",
"fields": [
{
"name": "mangoGroup",
@ -12135,6 +12495,16 @@ export const IDL: MangoV4 = {
"name": "price",
"type": "i128",
"index": false
},
{
"name": "feesAccrued",
"type": "i128",
"index": false
},
{
"name": "openInterest",
"type": "i64",
"index": false
}
]
},
@ -12180,6 +12550,16 @@ export const IDL: MangoV4 = {
"name": "loanFeeRate",
"type": "i128",
"index": false
},
{
"name": "totalBorrows",
"type": "i128",
"index": false
},
{
"name": "totalDeposits",
"type": "i128",
"index": false
}
]
},
@ -12214,7 +12594,7 @@ export const IDL: MangoV4 = {
]
},
{
"name": "LiquidateTokenAndTokenLog",
"name": "TokenLiqWithTokenLog",
"fields": [
{
"name": "mangoGroup",
@ -12351,7 +12731,7 @@ export const IDL: MangoV4 = {
]
},
{
"name": "LiquidateTokenBankruptcyLog",
"name": "TokenLiqBankruptcyLog",
"fields": [
{
"name": "mangoGroup",
@ -12420,12 +12800,312 @@ export const IDL: MangoV4 = {
},
{
"name": "cumulativeDepositInterest",
"type": "f32",
"type": "f64",
"index": false
},
{
"name": "cumulativeBorrowInterest",
"type": "f32",
"type": "f64",
"index": false
}
]
},
{
"name": "DeactivatePerpPositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "marketIndex",
"type": "u16",
"index": false
},
{
"name": "cumulativeLongFunding",
"type": "f64",
"index": false
},
{
"name": "cumulativeShortFunding",
"type": "f64",
"index": false
},
{
"name": "makerVolume",
"type": "u64",
"index": false
},
{
"name": "takerVolume",
"type": "u64",
"index": false
},
{
"name": "perpSpotTransfers",
"type": "i64",
"index": false
}
]
},
{
"name": "TokenMetaDataLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mint",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "mintDecimals",
"type": "u8",
"index": false
},
{
"name": "oracle",
"type": "publicKey",
"index": false
},
{
"name": "mintInfo",
"type": "publicKey",
"index": false
}
]
},
{
"name": "PerpMarketMetaDataLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarket",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "baseDecimals",
"type": "u8",
"index": false
},
{
"name": "baseLotSize",
"type": "i64",
"index": false
},
{
"name": "quoteLotSize",
"type": "i64",
"index": false
},
{
"name": "oracle",
"type": "publicKey",
"index": false
}
]
},
{
"name": "Serum3RegisterMarketLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "serumMarket",
"type": "publicKey",
"index": false
},
{
"name": "marketIndex",
"type": "u16",
"index": false
},
{
"name": "baseTokenIndex",
"type": "u16",
"index": false
},
{
"name": "quoteTokenIndex",
"type": "u16",
"index": false
},
{
"name": "serumProgram",
"type": "publicKey",
"index": false
},
{
"name": "serumProgramExternal",
"type": "publicKey",
"index": false
}
]
},
{
"name": "PerpLiqBasePositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "baseTransfer",
"type": "i64",
"index": false
},
{
"name": "quoteTransfer",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "PerpLiqBankruptcyLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "insuranceTransfer",
"type": "i128",
"index": false
},
{
"name": "socializedLoss",
"type": "i128",
"index": false
}
]
},
{
"name": "PerpSettlePnlLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccountA",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccountB",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "settlement",
"type": "i128",
"index": false
},
{
"name": "settler",
"type": "publicKey",
"index": false
},
{
"name": "fee",
"type": "i128",
"index": false
}
]
},
{
"name": "PerpSettleFeesLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "settlement",
"type": "i128",
"index": false
}
]

View File

@ -1,108 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import idsJson from '../../../ids.json';
import { MangoClient } from '../../client';
import { MANGO_V4_ID, SERUM3_PROGRAM_ID } from '../../constants';
import { Id } from '../../ids';
//
// script to add a group to ids json
//
function replacer(key, value) {
if (value instanceof Map) {
return Object.fromEntries(value);
} else {
return value;
}
}
async function main() {
const groupName = 'devnet.microwavedcola';
const cluster = 'devnet';
// build client and fetch group for admin
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
cluster,
MANGO_V4_ID[cluster],
);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, 0);
// collect mappings &
// collect pubkeys
const banks = await client.getBanksForGroup(group);
const banksMapByMint = new Map(
banks.map((tuple) => [tuple.mint.toBase58(), tuple]),
);
const stubOracles = await client.getStubOracle(group);
const mintInfos = await client.getMintInfosForGroup(group);
const serum3Markets = await client.serum3GetMarkets(group);
const perpMarkets = await client.perpGetMarkets(group);
// build ids
const toDump = new Id(
cluster,
groupName,
group.publicKey.toBase58(),
SERUM3_PROGRAM_ID[cluster].toBase58(),
MANGO_V4_ID[cluster].toBase58(),
banks.map((tuple) => ({
name: tuple.name,
publicKey: tuple.publicKey.toBase58(),
})),
stubOracles.map((tuple) => ({
name: banksMapByMint.get(tuple.mint.toBase58())!.name,
publicKey: tuple.publicKey.toBase58(),
})),
mintInfos.map((tuple) => ({
name: banksMapByMint.get(tuple.mint.toBase58())!.name,
publicKey: tuple.publicKey.toBase58(),
})),
serum3Markets.map((tuple) => ({
name: tuple.name,
publicKey: tuple.publicKey.toBase58(),
marketExternal: tuple.serumMarketExternal.toBase58(),
})),
perpMarkets.map((tuple) => ({
name: tuple.name,
publicKey: tuple.publicKey.toBase58(),
})),
);
// adds ids for group in existing ids.json
const existingGroup = idsJson.groups.find((group) => group.name == groupName);
if (existingGroup) {
console.log('Updating existing group with latest state...');
} else {
console.log('Group does not exist yet...');
}
idsJson.groups = idsJson.groups.filter((group) => group.name !== groupName);
idsJson.groups.push(toDump);
// dump
const file = `${process.cwd()}/ts/client/ids.json`;
await fs.writeFileSync(file, JSON.stringify(idsJson, replacer, 2));
process.exit();
}
main();

View File

@ -1,122 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
]);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
// user1
const user1 = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.PAYER_KEYPAIR!, 'utf-8')),
),
);
const user1Wallet = new Wallet(user1);
const user1Provider = new AnchorProvider(connection, user1Wallet, options);
const user1Client = await MangoClient.connect(
user1Provider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`user1 ${user1Wallet.publicKey.toBase58()}`);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await user1Client.getGroupForAdmin(admin.publicKey, GROUP_NUM);
console.log(`Found group ${group.publicKey.toBase58()}`);
const user1MangoAccount = await user1Client.getOrCreateMangoAccount(group);
console.log(`...mangoAccount1 ${user1MangoAccount.publicKey}`);
/// user1 deposits some btc, so user2 can borrow it
let amount = 0.001;
let token = 'BTC';
console.log(`Depositing...${amount} 'BTC'`);
await user1Client.tokenDeposit(group, user1MangoAccount, token, amount);
await user1MangoAccount.reload(user1Client, group);
console.log(`${user1MangoAccount.toString(group)}`);
// user 2
const user2 = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const user2Wallet = new Wallet(user2);
const user2Provider = new AnchorProvider(connection, user2Wallet, options);
const user2Client = await MangoClient.connect(
user2Provider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`user2 ${user2Wallet.publicKey.toBase58()}`);
const user2MangoAccount = await user2Client.getOrCreateMangoAccount(group);
console.log(`...mangoAccount2 ${user2MangoAccount.publicKey}`);
/// Increase usdc price temporarily to allow lots of borrows
console.log(
`Setting USDC price to 1.5, to allow the user to borrow lots of btc`,
);
const adminWallet = new Wallet(admin);
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
await client.stubOracleSet(group, group.banksMap.get('USDC')?.oracle!, 1.5);
/// user2 deposits some collateral and borrows BTC
amount = 1;
console.log(`Depositing...${amount} 'USDC'`);
await user2Client.tokenDeposit(group, user2MangoAccount, 'USDC', amount);
await user2MangoAccount.reload(user2Client, group);
console.log(`${user2MangoAccount.toString(group)}`);
const maxNative = await (
await user2MangoAccount.getMaxWithdrawWithBorrowForToken(group, token)
).toNumber();
amount = 0.9 * maxNative;
console.log(`Withdrawing...${amount} native BTC'`);
await user2Client.tokenWithdrawNative(
group,
user2MangoAccount,
token,
amount,
true,
);
await user2MangoAccount.reload(user2Client, group);
console.log(`${user2MangoAccount.toString(group)}`);
/// Reduce usdc price to normal again
console.log(
`Setting USDC price back to 1.0, decreasing the user's collateral size`,
);
await client.stubOracleSet(group, group.banksMap.get('USDC')?.oracle!, 1.0);
process.exit();
}
main();

View File

@ -1,84 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
//
// (unfinished?) script which shows how to use the flash loan 1 ix
//
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
]);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, 0);
console.log(`Found group ${group.publicKey.toBase58()}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
if (false) {
// deposit and withdraw
console.log(`Depositing...50 USDC`);
await client.tokenDeposit(group, mangoAccount, 'USDC', 50);
await mangoAccount.reload(client, group);
console.log(`Depositing...0.0005 BTC`);
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005);
await mangoAccount.reload(client, group);
}
try {
const sig = await client.marginTrade({
group: group,
mangoAccount: mangoAccount,
inputMintPk: new PublicKey(DEVNET_MINTS['USDC']),
amountIn: 0.001,
outputMintPk: new PublicKey(DEVNET_MINTS['SOL']),
slippage: 1,
});
console.log(
`sig https://explorer.solana.com/address/${sig}?cluster=devnet`,
);
} catch (error) {
console.log(error);
}
process.exit();
}
main();

View File

@ -1,39 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../../client';
//
// script which shows example usage of ids json (saves having to do gpa)
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connectForGroupName(
userProvider,
'devnet.microwavedcola' /* Use ids json instead of getProgramAccounts */,
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, 0);
console.log(`Found group ${group.publicKey.toBase58()}`);
process.exit();
}
main();

View File

@ -1,69 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MANGO_V4_ID } from '../../constants';
import { MangoClient } from '../../index';
//
// An example for users based on high level api i.e. the client
// Create
// process.env.USER_KEYPAIR - mango account owner keypair path
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, 0);
console.log(`Found group ${group.publicKey.toBase58()}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
// logging serum3 open orders for user
while (true) {
console.log(`Current own orders on OB...`);
const orders = await client.getSerum3Orders(
group,
'BTC/USDC',
);
for (const order of orders) {
console.log(
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${
order.size
} ${order.openOrdersAddress.toBase58()}`,
);
}
await new Promise((r) => setTimeout(r, 500));
}
process.exit();
}
main();

View File

@ -1,58 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { TokenPosition } from '../../accounts/mangoAccount';
import { MANGO_V4_ID } from '../../constants';
import { MangoClient } from '../../index';
//
// An example for users based on high level api i.e. the client
// Create
// process.env.USER_KEYPAIR - mango account owner keypair path
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
//
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, 0);
console.log(`Found group ${group.publicKey.toBase58()}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
// log users tokens
for (const token of mangoAccount.tokens) {
if (token.tokenIndex == TokenPosition.TokenIndexUnset) continue;
console.log(token.toString());
}
process.exit();
}
main();

View File

@ -1,192 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { OrderType, Side } from '../../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../../accounts/serum3';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
//
// An example for users based on high level api i.e. the client
// Create
// process.env.USER_KEYPAIR - mango account owner keypair path
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
//
// This script deposits some tokens, places some serum orders, cancels them, places some perp orders
//
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
]);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
// mango account owner
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// delegate
const delegate = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER3_KEYPAIR!, 'utf-8')),
),
);
const delegateWallet = new Wallet(delegate);
const delegateProvider = new AnchorProvider(
connection,
delegateWallet,
options,
);
// Note: simply create a client with delegate and use this client to execute ixs
const delegateClient = await MangoClient.connect(
delegateProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`Delegate ${delegateWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await delegateClient.getGroupForAdmin(admin.publicKey, 0);
console.log(group.toString());
// fetch mango account using owners pubkey
console.log(`Fetching mangoaccount...`);
const mangoAccount = (
await delegateClient.getMangoAccountForOwner(group, user.publicKey)
)[0];
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
if (true) {
// set delegate, and change name
console.log(`...changing mango account name, and setting a delegate`);
await client.editMangoAccount(
group,
mangoAccount,
'my_changed_name',
delegate.publicKey,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());
}
if (true) {
// deposit
console.log(`...depositing 50 USDC`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS['USDC']),
50,
);
await mangoAccount.reload(client, group);
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS['BTC']),
0.0005,
);
await mangoAccount.reload(client, group);
// serum3
console.log(`...placing serum3 bid`);
await delegateClient.serum3PlaceOrder(
group,
mangoAccount,
'BTC/USDC',
Serum3Side.bid,
20,
0.0001,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
10,
);
await mangoAccount.reload(delegateClient, group);
console.log(`...current own orders on OB`);
let orders = await delegateClient.getSerum3Orders(
group,
'BTC/USDC',
);
for (const order of orders) {
console.log(
` - order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
);
console.log(` - cancelling order with ${order.orderId}`);
await delegateClient.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
}
console.log(`...settling funds`);
await delegateClient.serum3SettleFunds(
group,
mangoAccount,
'BTC/USDC',
);
}
if (true) {
// perps
console.log(`...placing perp bid`);
try {
await delegateClient.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.bid,
30000,
0.000001,
30000 * 0.000001,
Math.floor(Math.random() * 99999),
OrderType.limit,
0,
1,
);
} catch (error) {
console.log(error);
}
}
process.exit();
}
main();

View File

@ -1,150 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { HealthType } from '../../accounts/mangoAccount';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
import { toUiDecimalsForQuote } from '../../utils';
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
]);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
if (true) {
await group.reloadAll(client);
console.log(group.banksMapByName.get('USDC')![0].toString());
console.log(group.banksMapByName.get('BTC')![0].toString());
}
if (false) {
// deposit and withdraw
try {
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS['BTC']),
0.0005,
);
await mangoAccount.reload(client, group);
console.log(`...withdrawing 5 USDC`);
await client.tokenWithdraw(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS['USDC']),
50,
true,
);
await mangoAccount.reload(client, group);
} catch (error) {
console.log(error);
}
}
if (true) {
await mangoAccount.reload(client, group);
console.log(
'...mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity().toNumber()),
);
console.log(
'...mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue().toNumber()),
);
console.log(
'...mangoAccount.accountData["healthCache"].health(HealthType.init) ' +
toUiDecimalsForQuote(
mangoAccount.accountData['healthCache']
.health(HealthType.init)
.toNumber(),
),
);
console.log(
'...mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote(mangoAccount.getAssetsVal().toNumber()),
);
console.log(
'...mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote(mangoAccount.getLiabsVal().toNumber()),
);
console.log(
'...mangoAccount.getMaxWithdrawWithBorrowForToken(group, "SOL") ' +
toUiDecimalsForQuote(
(
await mangoAccount.getMaxWithdrawWithBorrowForToken(
group,
new PublicKey(DEVNET_MINTS['SOL']),
)
).toNumber(),
),
);
console.log(
"...mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
toUiDecimalsForQuote(
mangoAccount
.getSerum3MarketMarginAvailable(group, 'BTC/USDC')
.toNumber(),
),
);
console.log(
"...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
toUiDecimalsForQuote(
mangoAccount
.getPerpMarketMarginAvailable(group, 'BTC-PERP')
.toNumber(),
),
);
}
if (true) {
await group.reloadAll(client);
console.log(group.banksMapByName.get('USDC')![0].toString());
console.log(group.banksMapByName.get('BTC')![0].toString());
}
process.exit();
}
main();

View File

@ -1,109 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import idsJson from '../../../ids.json';
import { MangoClient } from '../../client';
import { MANGO_V4_ID, SERUM3_PROGRAM_ID } from '../../constants';
import { Id } from '../../ids';
//
// script to add a group to ids json
//
function replacer(key, value) {
if (value instanceof Map) {
return Object.fromEntries(value);
} else {
return value;
}
}
async function main() {
const groupName = 'mainnet-beta.microwavedcola';
const cluster = 'mainnet-beta';
// build client and fetch group for admin
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.MB_CLUSTER_URL, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
cluster,
MANGO_V4_ID[cluster],
);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
),
);
console.log(`Admin ${admin.publicKey.toBase58()}`);
const group = await client.getGroupForCreator(admin.publicKey, 0);
// collect mappings &
// collect pubkeys
const banks = await client.getBanksForGroup(group);
const banksMapByMint = new Map(
banks.map((tuple) => [tuple.mint.toBase58(), tuple]),
);
const stubOracles = await client.getStubOracle(group);
const mintInfos = await client.getMintInfosForGroup(group);
const serum3Markets = await client.serum3GetMarkets(group);
const perpMarkets = await client.perpGetMarkets(group);
// build ids
const toDump = new Id(
cluster,
groupName,
group.publicKey.toBase58(),
SERUM3_PROGRAM_ID[cluster].toBase58(),
MANGO_V4_ID[cluster].toBase58(),
banks.map((tuple) => ({
name: tuple.name,
publicKey: tuple.publicKey.toBase58(),
})),
stubOracles.map((tuple) => ({
name: banksMapByMint.get(tuple.mint.toBase58())!.name,
publicKey: tuple.publicKey.toBase58(),
})),
mintInfos.map((tuple) => ({
name: banksMapByMint.get(tuple.mint.toBase58())!.name,
publicKey: tuple.publicKey.toBase58(),
})),
serum3Markets.map((tuple) => ({
name: tuple.name,
publicKey: tuple.publicKey.toBase58(),
marketExternal: tuple.serumMarketExternal.toBase58(),
})),
perpMarkets.map((tuple) => ({
name: tuple.name,
publicKey: tuple.publicKey.toBase58(),
})),
);
console.log(toDump);
// adds ids for group in existing ids.json
const existingGroup = idsJson.groups.find((group) => group.name == groupName);
if (existingGroup) {
console.log('Updating existing group with latest state...');
} else {
console.log('Group does not exist yet...');
}
idsJson.groups = idsJson.groups.filter((group) => group.name !== groupName);
idsJson.groups.push(toDump);
// dump
const file = `${process.cwd()}/ts/client/ids.json`;
await fs.writeFileSync(file, JSON.stringify(idsJson, replacer, 2));
process.exit();
}
main();

View File

@ -1,280 +0,0 @@
import { Jupiter } from '@jup-ag/core';
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import {
AccountMeta,
Connection,
Keypair,
SYSVAR_INSTRUCTIONS_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import BN from 'bn.js';
import fs from 'fs';
import { QUOTE_DECIMALS } from '../../accounts/bank';
import { MangoClient } from '../../index';
import { getAssociatedTokenAddress } from '../../utils';
const MB_CLUSTER_URL =
process.env.MB_CLUSTER_URL ||
'https://mango.rpcpool.com/946ef7337da3f5b8d3e4a34e7f88';
const MB_PAYER_KEYPAIR =
process.env.MB_PAYER_KEYPAIR ||
'/Users/tylershipe/.config/solana/deploy.json';
//
// example script which shows usage of flash loan ix using a jupiter swap
//
// NOTE: we assume that ATA for source and target already exist for wallet
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(MB_CLUSTER_URL, options);
// load user key
const user = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER_KEYPAIR!, 'utf-8'))),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connectForGroupName(
userProvider,
'mainnet-beta.microwavedcola',
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// load admin key
const admin = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER_KEYPAIR!, 'utf-8'))),
);
console.log(`Admin ${admin.publicKey.toBase58()}`);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, 0);
console.log(`Found group ${group.publicKey.toBase58()}`);
console.log(`start btc bank ${group.banksMap.get('BTC').toString()}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(`start balance \n${mangoAccount.toString(group)}`);
//
// flash loan 3
//
if (true) {
// source of swap
const sourceBank = group.banksMap.get('USDC');
// target of swap
const targetBank = group.banksMap.get('BTC');
// 0.2$, at 1BTC=20,000$, 0.2$=0.00001BTC
const sourceAmount = 2 * Math.pow(10, QUOTE_DECIMALS - 1);
console.log(`Flash loaning ${sourceBank.name} to ${targetBank.name}`);
// jupiter route
const jupiter = await Jupiter.load({
connection: client.program.provider.connection,
cluster: 'mainnet-beta',
user: mangoAccount.owner, // or public key
// platformFeeAndAccounts: NO_PLATFORM_FEE,
routeCacheDuration: 10_000, // Will not refetch data on computeRoutes for up to 10 seconds
});
const routes = await jupiter.computeRoutes({
inputMint: sourceBank.mint, // Mint address of the input token
outputMint: targetBank.mint, // Mint address of the output token
inputAmount: sourceAmount, // raw input amount of tokens
slippage: 5, // The slippage in % terms
forceFetch: false, // false is the default value => will use cache if not older than routeCacheDuration
});
const routesInfosWithoutRaydium = routes.routesInfos.filter((r) => {
if (r.marketInfos.length > 1) {
for (const mkt of r.marketInfos) {
if (mkt.amm.label === 'Raydium' || mkt.amm.label === 'Serum')
return false;
}
}
return true;
});
// loop until we manage first successful swap
let res;
let i = 0;
while (true) {
const instructions: TransactionInstruction[] = [];
// select a route and fetch+build its tx
const selectedRoute = routesInfosWithoutRaydium[i];
const { transactions } = await jupiter.exchange({
routeInfo: selectedRoute,
});
const { setupTransaction, swapTransaction } = transactions;
for (const ix of swapTransaction.instructions) {
if (
ix.programId.toBase58() ===
'JUP2jxvXaqu7NQY1GmNF4m1vodw12LVXYxbFL2uJvfo'
) {
instructions.push(ix);
}
}
// run jup setup in a separate tx, ideally this should be packed before flashLoanBegin in same tx,
// but it increases chance of flash loan tx to exceed tx size limit
if (setupTransaction) {
await this.program.provider.sendAndConfirm(setupTransaction);
}
// flash loan start ix - takes a loan for source token,
// flash loan end ix - returns increase in all token account's amounts to respective vaults,
const healthRemainingAccounts =
client.buildFixedAccountRetrieverHealthAccounts(
group,
mangoAccount,
[sourceBank, targetBank], // we would be taking a sol loan potentially
);
// 1. build flash loan end ix
const flashLoadnEndIx = await client.program.methods
.flashLoanEnd(true)
.accounts({
account: mangoAccount.publicKey,
owner: (client.program.provider as AnchorProvider).wallet.publicKey,
})
.remainingAccounts([
...healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable: false,
isSigner: false,
} as AccountMeta),
),
{
pubkey: sourceBank.vault,
isWritable: true,
isSigner: false,
} as AccountMeta,
{
pubkey: targetBank.vault,
isWritable: true,
isSigner: false,
} as AccountMeta,
{
pubkey: await getAssociatedTokenAddress(
sourceBank.mint,
mangoAccount.owner,
),
isWritable: true, // increase in this address amount is transferred back to the sourceBank.vault above in this case whatever is residual of source bank loan
isSigner: false,
} as AccountMeta,
{
pubkey: await getAssociatedTokenAddress(
targetBank.mint,
mangoAccount.owner,
),
isWritable: true, // increase in this address amount is transferred back to the targetBank.vault above in this case whatever is result of swap
isSigner: false,
} as AccountMeta,
{
pubkey: group.publicKey,
isWritable: false,
isSigner: false,
} as AccountMeta,
])
.instruction();
instructions.push(flashLoadnEndIx);
// 2. build flash loan start ix, add end ix as a post ix
try {
res = await client.program.methods
.flashLoanBegin([
new BN(sourceAmount),
new BN(
0,
) /* we don't care about borrowing the target amount, this is just a dummy */,
])
.accounts({
// for observing ixs in the entire tx,
// e.g. apart from flash loan start and end no other ix should target mango v4 program
// e.g. forbid FlashLoanBegin been called from CPI
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.remainingAccounts([
{
pubkey: sourceBank.publicKey,
isWritable: true, // metadata for flash loan is updated
isSigner: false,
} as AccountMeta,
{
pubkey: targetBank.publicKey,
isWritable: true, // this is a dummy, its just done so that we match flash loan start and end ix
isSigner: false,
} as AccountMeta,
{
pubkey: sourceBank.vault,
isWritable: true,
isSigner: false,
} as AccountMeta,
{
pubkey: targetBank.vault,
isWritable: true, // this is a dummy, its just done so that we match flash loan start and end ix
isSigner: false,
} as AccountMeta,
{
pubkey: await getAssociatedTokenAddress(
sourceBank.mint,
mangoAccount.owner,
),
isWritable: true, // token transfer i.e. loan to a desired token account e.g. user's ATA when using a route made for a specific user
isSigner: false,
} as AccountMeta,
{
pubkey: await getAssociatedTokenAddress(
targetBank.mint,
mangoAccount.owner,
),
isWritable: false, // this is a dummy, its just done so that we match flash loan start and end ix
isSigner: false,
} as AccountMeta,
{
pubkey: group.publicKey,
isWritable: false,
isSigner: false,
} as AccountMeta,
])
.postInstructions(instructions)
.rpc();
// break when success
break;
} catch (error) {
console.log(error);
if (
(error.toString() as string).includes('Transaction too large:') ||
(error.toString() as string).includes(
'encoding overruns Uint8Array',
) ||
(error.toString() as string).includes(
'The value of "offset" is out of range. It must be >= 0 and <= 1231. Received 1232',
) ||
(error.toString() as string).includes(
'The value of "value" is out of range. It must be >= 0 and <= 255. Received',
) ||
i > 10
) {
console.log(`route ${i} was bad, trying next one...`);
i++;
} else {
throw error; // let others bubble up
}
}
}
console.log(`success tx - https://explorer.solana.com/tx/${res}`);
group.reloadBanks(client);
console.log(`end btc bank ${group.banksMap.get('BTC').toString()}`);
await mangoAccount.reload(client, group);
console.log(`end balance \n${mangoAccount.toString(group)}`);
}
}
main();

View File

@ -1,42 +0,0 @@
import { AnchorProvider } from '@project-serum/anchor';
import { Connection, PublicKey } from '@solana/web3.js';
import { parseSwitchboardOracle } from '../../accounts/oracle';
async function main() {
const options = AnchorProvider.defaultOptions();
async function foo(obj) {
let connection = new Connection(obj.net, options);
let ai = await connection.getAccountInfo(new PublicKey(obj.pk));
console.log(`${obj.name} price ${await parseSwitchboardOracle(ai!)}`);
}
for (const oracle of [
{
name: 'devnet mngo v1',
pk: '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o',
net: 'https://mango.devnet.rpcpool.com',
},
{
name: 'devnet sol v2',
pk: 'GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR',
net: 'https://mango.devnet.rpcpool.com',
},
{
name: 'mainnet btc v2',
pk: '3HtmwdXJPAdMZ73fTGeCFgbDQZGLZWpmsm3JAB5quGJN',
net: 'http://api.mainnet-beta.solana.com/',
},
{
name: 'mainnet sol v2',
pk: 'GvDMxPzN1sCj7L26YDK2HnMRXEQmQ2aemov8YBtPS7vR',
net: 'http://api.mainnet-beta.solana.com/',
},
]) {
await foo(oracle);
}
process.exit();
}
main();

View File

@ -1,51 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Market } from '@project-serum/serum';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import * as os from 'os';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
const main = async () => {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(os.homedir() + '/.config/solana/admin.json', 'utf-8'),
),
),
);
const adminWallet = new Wallet(admin);
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(
adminProvider,
'devnet',
MANGO_V4_ID['devnet'],
);
const btcMint = new PublicKey('3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU');
const usdcMint = new PublicKey(
'EmXq3Ni9gfudTiyNKzzYvpnQqnJEMRw2ttnVXoJXjLo1',
);
const serumProgramId = new PublicKey(
'DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY',
);
const market = await Market.findAccountsByMints(
connection,
btcMint,
usdcMint,
serumProgramId,
);
console.log('market', market);
};
main();

View File

@ -33,8 +33,9 @@ async function main() {
adminProvider,
'devnet',
MANGO_V4_ID['devnet'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
@ -57,16 +58,19 @@ async function main() {
group.consoleLogBanks();
// deregister all serum markets
for (const market of group.serum3MarketsMap.values()) {
sig = await client.serum3deregisterMarket(group, market.name);
for (const market of group.serum3MarketsMapByExternal.values()) {
sig = await client.serum3deregisterMarket(
group,
market.serumMarketExternal,
);
console.log(
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig}?cluster=devnet`,
);
}
// close all perp markets
for (const market of group.perpMarketsMap.values()) {
sig = await client.perpCloseMarket(group, market.name);
for (const market of group.perpMarketsMapByMarketIndex.values()) {
sig = await client.perpCloseMarket(group, market.perpMarketIndex);
console.log(
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}?cluster=devnet`,
);

View File

@ -65,8 +65,9 @@ async function main() {
adminProvider,
'devnet',
MANGO_V4_ID['devnet'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
// group

View File

@ -1,136 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { Serum3Side } from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// script which shows how to close a mango account cleanly i.e. close all active positions, withdraw all tokens, etc.
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
// note: either use finalized or expect closing certain things to fail and having to runs scrript multiple times
async function main() {
const options = AnchorProvider.defaultOptions();
// note: see note above
// options.commitment = 'finalized';
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
// user
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
{},
'get-program-accounts',
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
try {
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`Found group ${group.publicKey.toBase58()}`);
// fetch account
const mangoAccount = (
await client.getMangoAccountsForOwner(group, user.publicKey)
)[0];
console.log(`...found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
// close mango account serum3 positions, closing might require cancelling orders and settling
for (const serum3Account of mangoAccount.serum3Active()) {
let orders = await client.getSerum3Orders(
group,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
);
for (const order of orders) {
console.log(
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
);
console.log(` - Cancelling order with ${order.orderId}`);
await client.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
}
await client.serum3SettleFunds(
group,
mangoAccount,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
);
await client.serum3CloseOpenOrders(
group,
mangoAccount,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
);
}
// we closed a serum account, this changes the health accounts we are passing in for future ixs
await mangoAccount.reload(client, group);
// withdraw all tokens
for (const token of mangoAccount.tokensActive()) {
let native = token.balance(
group.getFirstBankByTokenIndex(token.tokenIndex),
);
// to avoid rounding issues
if (native.toNumber() < 1) {
continue;
}
let nativeFlooredNumber = Math.floor(native.toNumber());
console.log(
`withdrawing token ${
group.getFirstBankByTokenIndex(token.tokenIndex).name
} native amount ${nativeFlooredNumber} `,
);
await client.tokenWithdrawNative(
group,
mangoAccount,
group.getFirstBankByTokenIndex(token.tokenIndex).mint,
nativeFlooredNumber - 1 /* see comment in token_withdraw in program */,
false,
);
}
// reload and print current positions
await mangoAccount.reload(client, group);
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
// close account
console.log(`Close mango account...`);
const res = await client.closeMangoAccount(group, mangoAccount);
} catch (error) {
console.log(error);
}
process.exit();
}
main();

View File

@ -55,8 +55,9 @@ async function main() {
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
@ -86,7 +87,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('USDC')!),
1000,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
await client.tokenDeposit(
group,
@ -94,7 +95,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('MNGO')!),
100,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
await client.tokenDeposit(
group,
@ -102,7 +103,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('ETH')!),
500,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
await client.tokenDeposit(
group,
@ -110,7 +111,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('SRM')!),
500,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
await client.tokenDeposit(
group,
@ -118,7 +119,7 @@ async function main() {
new PublicKey(DEVNET_MINTS.get('BTC')!),
1,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(mangoAccount.toString(group));
} catch (error) {

View File

@ -1,4 +1,4 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { Serum3Side } from '../accounts/serum3';
@ -35,8 +35,9 @@ async function main() {
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
@ -59,9 +60,11 @@ async function main() {
// close mango account serum3 positions, closing might require cancelling orders and settling
for (const serum3Account of mangoAccount.serum3Active()) {
let orders = await client.getSerum3Orders(
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
group.serum3MarketsMapByMarketIndex.get(serum3Account.marketIndex)
?.serumMarketExternal!,
);
for (const order of orders) {
console.log(
@ -71,8 +74,8 @@ async function main() {
await client.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
group.serum3MarketsMapByMarketIndex.get(serum3Account.marketIndex)
?.serumMarketExternal!,
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
@ -80,17 +83,19 @@ async function main() {
await client.serum3SettleFunds(
group,
mangoAccount,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
group.serum3MarketsMapByMarketIndex.get(serum3Account.marketIndex)
?.serumMarketExternal!,
);
await client.serum3CloseOpenOrders(
group,
mangoAccount,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
group.serum3MarketsMapByMarketIndex.get(serum3Account.marketIndex)
?.serumMarketExternal!,
);
}
// we closed a serum account, this changes the health accounts we are passing in for future ixs
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
// withdraw all tokens
for (const token of mangoAccount.tokensActive()) {
@ -113,13 +118,15 @@ async function main() {
group,
mangoAccount,
group.getFirstBankByTokenIndex(token.tokenIndex).mint,
nativeFlooredNumber - 1 /* see comment in token_withdraw in program */,
new BN(
nativeFlooredNumber - 1,
) /* see comment in token_withdraw in program */,
false,
);
}
// reload and print current positions
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());

View File

@ -54,8 +54,9 @@ async function main() {
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);

View File

@ -1,91 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { Serum3Side } from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
const MAINNET_ORACLES = new Map([
['USDT', '3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL'],
['BTC', 'GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU'],
['ETH', 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB'],
['soETH', 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB'],
['SOL', 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG'],
['MSOL', 'E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9'],
['MNGO', '79wm3jjcPr6RaNQ4DGvP5KxG1mNd3gEBsg6FsNVFezK4'],
]);
const PAYER_KEYPAIR = process.env.MB_PAYER_KEYPAIR || '';
//
// (untested?) script which closes a mango account cleanly, first closes all positions, withdraws all tokens and then closes it
//
async function viewUnownedAccount(userKeypairFile: string) {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.rpcpool.com/0f9acc0d45173b51bf7d7e09c1e5',
options,
);
// user
// const userWallet = new Wallet(Keypair.generate());
// const userProvider = new AnchorProvider(connection, userWallet, options);
// console.log(`User ${userWallet.publicKey.toBase58()}`);
// admin
const admin = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR, 'utf-8'))),
);
const adminWallet = new Wallet(admin);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
console.log(`Admin ${admin.publicKey.toBase58()}`);
const client = await MangoClient.connect(
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, 2);
console.log(`Found group ${group.publicKey.toBase58()}`);
const btcMainnetOracle = new PublicKey(MAINNET_ORACLES.get('BTC')!);
console.log(`Registering perp market...`);
try {
await client.perpCreateMarket(
group,
btcMainnetOracle,
0,
'BTC-PERP',
0.1,
6,
0,
1,
10,
0.975,
0.95,
1.025,
1.05,
0.012,
0.0002,
0.0,
0.05,
0.05,
100,
false,
true,
);
console.log('done');
} catch (error) {
console.log(error);
}
process.exit();
}
async function main() {
await viewUnownedAccount(process.env.MB_USER2_KEYPAIR || '');
}
main();

View File

@ -1,5 +1,5 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
@ -27,8 +27,9 @@ async function main() {
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
const groups = await (async () => {
@ -65,8 +66,8 @@ async function main() {
}
// close all perp markets
for (const market of group.perpMarketsMap.values()) {
sig = await client.perpCloseMarket(group, market.name);
for (const market of group.perpMarketsMapByMarketIndex.values()) {
sig = await client.perpCloseMarket(group, market.perpMarketIndex);
console.log(
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
);

View File

@ -1,6 +1,12 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import {
AddressLookupTableProgram,
Connection,
Keypair,
PublicKey,
} from '@solana/web3.js';
import fs from 'fs';
import { TokenIndex } from '../accounts/bank';
import { Group } from '../accounts/group';
import {
Serum3OrderType,
@ -9,6 +15,7 @@ import {
} from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
import { buildVersionedTx } from '../utils';
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
@ -19,6 +26,8 @@ const MAINNET_MINTS = new Map([
['SOL', 'So11111111111111111111111111111111111111112'], // Wrapped SOL
['MSOL', 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So'],
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
['RAY', '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R'],
['DUST', 'DUSTawucrTsGU8hcqRdHDCbuYhCPADMLM2VcCb8VnFnQ'],
]);
const MAINNET_ORACLES = new Map([
['USDT', '3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL'],
@ -28,6 +37,8 @@ const MAINNET_ORACLES = new Map([
['SOL', 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG'],
['MSOL', 'E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9'],
['MNGO', '79wm3jjcPr6RaNQ4DGvP5KxG1mNd3gEBsg6FsNVFezK4'],
['RAY', 'AnLf8tVYCM816gmBjiy8n53eXKKEDydT5piYjjQDPgTB'],
['DUST', 'C5tuUPi7xJHBHZGZX6wWYf1Svm6jtTVwYrYrBCiEVejK'],
]);
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
@ -35,17 +46,20 @@ const MAINNET_ORACLES = new Map([
const MAINNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'],
['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],
['RAY/SOL', 'C6tp2RVZnxBPFbnAsfTjis8BN9tycESAT4SgDQgbbrsA'],
['DUST/SOL', '8WCzJpSNcLUYXPYeUDAXpH4hgqxFJpkYkVT6GJDSpcGx'],
]);
const { MB_CLUSTER_URL, MB_PAYER_KEYPAIR, MB_USER_KEYPAIR, MB_USER2_KEYPAIR } =
process.env;
async function buildAdminClient(): Promise<[MangoClient, Keypair]> {
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
),
Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER_KEYPAIR!, 'utf-8'))),
);
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
const connection = new Connection(MB_CLUSTER_URL!, options);
const adminWallet = new Wallet(admin);
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
@ -55,6 +69,9 @@ async function buildAdminClient(): Promise<[MangoClient, Keypair]> {
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{
idsSource: 'get-program-accounts',
},
),
admin,
];
@ -64,7 +81,7 @@ async function buildUserClient(
userKeypair: string,
): Promise<[MangoClient, Group, Keypair]> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
const connection = new Connection(MB_CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(userKeypair, 'utf-8'))),
@ -79,9 +96,7 @@ async function buildUserClient(
);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
),
Buffer.from(JSON.parse(fs.readFileSync(MB_PAYER_KEYPAIR!, 'utf-8'))),
);
console.log(`Admin ${admin.publicKey.toBase58()}`);
const group = await client.getGroupForCreator(admin.publicKey, 2);
@ -287,6 +302,55 @@ async function registerTokens() {
1.2,
0.05,
);
console.log(`Registering RAY...`);
const rayMainnetMint = new PublicKey(MAINNET_MINTS.get('RAY')!);
const rayMainnetOracle = new PublicKey(MAINNET_ORACLES.get('RAY')!);
await client.tokenRegister(
group,
rayMainnetMint,
rayMainnetOracle,
0.1,
7,
'RAY',
0.004,
0.7,
0.2,
0.85,
0.4,
4.0,
0.005,
0.0005,
7 / 8,
3 / 4,
8 / 7,
4 / 3,
1 / 16,
);
console.log(`Registering DUST...`);
const dustMainnetMint = new PublicKey(MAINNET_MINTS.get('DUST')!);
const dustMainnetOracle = new PublicKey(MAINNET_ORACLES.get('DUST')!);
await client.tokenRegister(
group,
dustMainnetMint,
dustMainnetOracle,
0.1,
8,
'DUST',
0.004,
0.7,
0.3,
0.85,
0.6,
6.0,
0.005,
0.0005,
0, // no asset weight for isolation
0,
81 / 80,
41 / 40, // 40x leverage so we can test something
1 / 160, // no liquidation fee
);
// log tokens/banks
await group.reloadAll(client);
@ -295,6 +359,20 @@ async function registerTokens() {
}
}
async function unregisterTokens() {
const result = await buildAdminClient();
const client = result[0];
const admin = result[1];
const group = await client.getGroupForCreator(admin.publicKey, 2);
let bank = group.getFirstBankByTokenIndex(8 as TokenIndex);
let sig = await client.tokenDeregister(group, bank.mint);
console.log(
`Removed token ${bank.name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
async function registerSerum3Markets() {
const result = await buildAdminClient();
const client = result[0];
@ -328,6 +406,50 @@ async function registerSerum3Markets() {
1,
'SOL/USDC',
);
// Register RAY and DUST markets
await client.serum3RegisterMarket(
group,
new PublicKey(MAINNET_SERUM3_MARKETS.get('RAY/SOL')!),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('RAY')!)),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
2,
'RAY/SOL',
);
await client.serum3RegisterMarket(
group,
new PublicKey(MAINNET_SERUM3_MARKETS.get('DUST/SOL')!),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('DUST')!)),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
3,
'DUST/SOL',
);
}
async function unregisterSerum3Markets() {
const result = await buildAdminClient();
const client = result[0];
const admin = result[1];
const group = await client.getGroupForCreator(admin.publicKey, 2);
let serum3Market = group.getSerum3MarketByName('RAY/SOL');
let sig = await client.serum3deregisterMarket(
group,
serum3Market.serumMarketExternal,
);
console.log(
`Deregistered serum market ${serum3Market.name}, sig https://explorer.solana.com/tx/${sig}`,
);
serum3Market = group.getSerum3MarketByName('DUST/SOL');
sig = await client.serum3deregisterMarket(
group,
serum3Market.serumMarketExternal,
);
console.log(
`Deregistered serum market ${serum3Market.name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
async function createUser(userKeypair: string) {
@ -350,7 +472,7 @@ async function createUser(userKeypair: string) {
new PublicKey(MAINNET_MINTS.get('USDC')!),
10,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`...deposited 10 USDC`);
await client.tokenDeposit(
@ -359,7 +481,7 @@ async function createUser(userKeypair: string) {
new PublicKey(MAINNET_MINTS.get('SOL')!),
1,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`...deposited 1 SOL`);
}
@ -422,7 +544,7 @@ async function placeSerum3TradeAndCancelIt(userKeypair: string) {
console.log(order);
}
console.log(`...cancelling serum3 orders`);
await client.serum3CancelAllorders(
await client.serum3CancelAllOrders(
group,
mangoAccount,
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!),
@ -440,6 +562,126 @@ async function placeSerum3TradeAndCancelIt(userKeypair: string) {
}
}
async function createAndPopulateAlt() {
const result = await buildAdminClient();
const client = result[0];
const admin = result[1];
const group = await client.getGroupForCreator(admin.publicKey, 2);
const connection = client.program.provider.connection;
// Create ALT, and set to group at index 0
if (group.addressLookupTables[0].equals(PublicKey.default)) {
try {
console.log(`ALT: Creating`);
const createIx = AddressLookupTableProgram.createLookupTable({
authority: admin.publicKey,
payer: admin.publicKey,
recentSlot: await connection.getSlot('finalized'),
});
const createTx = await buildVersionedTx(
client.program.provider as AnchorProvider,
[createIx[0]],
);
let sig = await connection.sendTransaction(createTx);
console.log(
`...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig}`,
);
console.log(`ALT: set at index 0 for group...`);
sig = await client.altSet(group, createIx[1], 0);
console.log(`...https://explorer.solana.com/tx/${sig}`);
} catch (error) {
console.log(error);
}
}
// Extend using mango v4 relevant pub keys
try {
let bankAddresses = Array.from(group.banksMapByMint.values())
.flat()
.map((bank) => [bank.publicKey, bank.oracle, bank.vault])
.flat()
.concat(
Array.from(group.banksMapByMint.values())
.flat()
.map((mintInfo) => mintInfo.publicKey),
);
let serum3MarketAddresses = Array.from(
group.serum3MarketsMapByExternal.values(),
)
.flat()
.map((serum3Market) => serum3Market.publicKey);
let serum3ExternalMarketAddresses = Array.from(
group.serum3ExternalMarketsMap.values(),
)
.flat()
.map((serum3ExternalMarket) => [
serum3ExternalMarket.publicKey,
serum3ExternalMarket.bidsAddress,
serum3ExternalMarket.asksAddress,
])
.flat();
let perpMarketAddresses = Array.from(
group.perpMarketsMapByMarketIndex.values(),
)
.flat()
.map((perpMarket) => [
perpMarket.publicKey,
perpMarket.oracle,
perpMarket.bids,
perpMarket.asks,
perpMarket.eventQueue,
])
.flat();
async function extendTable(addresses: PublicKey[]) {
await group.reloadAll(client);
const alt =
await client.program.provider.connection.getAddressLookupTable(
group.addressLookupTables[0],
);
addresses = addresses.filter(
(newAddress) =>
alt.value?.state.addresses &&
alt.value?.state.addresses.findIndex((addressInALt) =>
addressInALt.equals(newAddress),
) === -1,
);
if (addresses.length === 0) {
return;
}
const extendIx = AddressLookupTableProgram.extendLookupTable({
lookupTable: group.addressLookupTables[0],
payer: admin.publicKey,
authority: admin.publicKey,
addresses,
});
const extendTx = await buildVersionedTx(
client.program.provider as AnchorProvider,
[extendIx],
);
let sig = await client.program.provider.connection.sendTransaction(
extendTx,
);
console.log(`https://explorer.solana.com/tx/${sig}`);
}
console.log(`ALT: extending using mango v4 relevant public keys`);
await extendTable(bankAddresses);
await extendTable(serum3MarketAddresses);
await extendTable(serum3ExternalMarketAddresses);
await extendTable(perpMarketAddresses);
} catch (error) {
console.log(error);
}
}
async function main() {
try {
// await createGroup();
@ -448,22 +690,28 @@ async function main() {
}
try {
// await registerTokens();
// await unregisterTokens();
} catch (error) {
console.log(error);
}
try {
// await registerSerum3Markets();
// await unregisterSerum3Markets();
} catch (error) {
console.log(error);
}
try {
// await createUser(process.env.MB_USER_KEYPAIR!);
// await createUser(process.env.MB_USER2_KEYPAIR!);
// await expandMangoAccount(process.env.MB_USER_KEYPAIR!);
await placeSerum3TradeAndCancelIt(process.env.MB_USER_KEYPAIR!);
// await createUser(MB_USER_KEYPAIR!);
// await createUser(MB_USER2_KEYPAIR!);
// await expandMangoAccount(MB_USER_KEYPAIR!);
// await placeSerum3TradeAndCancelIt(MB_USER_KEYPAIR!);
} catch (error) {
console.log(error);
}
try {
// await createAndPopulateAlt();
} catch (error) {}
}
try {

View File

@ -1,4 +1,4 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { Serum3Side } from '../accounts/serum3';
@ -47,9 +47,11 @@ async function closeUserAccount(userKeypairFile: string) {
try {
// cancel serum3 accounts, closing might require cancelling orders and settling
for (const serum3Account of mangoAccount.serum3Active()) {
let orders = await client.getSerum3Orders(
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
group.serum3MarketsMapByMarketIndex.get(serum3Account.marketIndex)
?.serumMarketExternal!,
);
for (const order of orders) {
console.log(
@ -59,8 +61,8 @@ async function closeUserAccount(userKeypairFile: string) {
await client.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
group.serum3MarketsMapByMarketIndex.get(serum3Account.marketIndex)
?.serumMarketExternal!,
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
@ -69,23 +71,29 @@ async function closeUserAccount(userKeypairFile: string) {
await client.serum3SettleFunds(
group,
mangoAccount,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
group.serum3MarketsMapByMarketIndex.get(serum3Account.marketIndex)
?.serumMarketExternal!,
);
await client.serum3CloseOpenOrders(
group,
mangoAccount,
group.getSerum3MarketByIndex(serum3Account.marketIndex)!.name,
group.serum3MarketsMapByMarketIndex.get(serum3Account.marketIndex)
?.serumMarketExternal!,
);
}
// we closed a serum account, this changes the health accounts we are passing in for future ixs
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
// withdraw all tokens
for (const token of mangoAccount.tokensActive()) {
const native = token.native(group.findBank(token.tokenIndex)!);
const native = token.balance(
group.getFirstBankByTokenIndex(token.tokenIndex)!,
);
console.log(
`token native ${native} ${group.findBank(token.tokenIndex)!.name}`,
`token native ${native} ${
group.getFirstBankByTokenIndex(token.tokenIndex)!.name
}`,
);
if (native.toNumber() < 1) {
continue;
@ -94,8 +102,12 @@ async function closeUserAccount(userKeypairFile: string) {
await client.tokenWithdrawNative(
group,
mangoAccount,
group.findBank(token.tokenIndex)!.name,
token.native(group.findBank(token.tokenIndex)!).toNumber(),
group.getFirstBankByTokenIndex(token.tokenIndex)!.mint,
new BN(
token
.balance(group.getFirstBankByTokenIndex(token.tokenIndex)!)
.toNumber(),
),
false,
);
}
@ -103,7 +115,7 @@ async function closeUserAccount(userKeypairFile: string) {
console.log(error);
}
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());

View File

@ -115,16 +115,18 @@ async function main() {
console.log(
'mangoAccount.simHealthWithTokenPositionChanges ' +
toUiDecimalsForQuote(
await mangoAccount.simHealthRatioWithTokenPositionUiChanges(group, [
mangoAccount.simHealthRatioWithTokenPositionUiChanges(group, [
{
mintPk: group.banksMapByName.get('USDC')![0].mint,
uiTokenAmount:
-20_000 * Math.pow(10, group.banksMap.get('BTC')?.mintDecimals!),
-20000 *
Math.pow(10, group.banksMapByName.get('BTC')![0].mintDecimals!),
},
{
mintPk: group.banksMapByName.get('BTC')![0].mint,
uiTokenAmount:
1 * Math.pow(10, group.banksMap.get('BTC')?.mintDecimals!),
1 *
Math.pow(10, group.banksMapByName.get('BTC')![0].mintDecimals!),
},
]),
),

View File

@ -51,8 +51,9 @@ async function main() {
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
// group

View File

@ -1,5 +1,5 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
@ -28,8 +28,9 @@ async function main() {
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
@ -59,15 +60,15 @@ async function main() {
try {
console.log(`...depositing 5 USDC`);
await client.tokenDeposit(group, mangoAccount, usdcMint, 5);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`...depositing 0.0002 BTC`);
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`...depositing 0.15 SOL`);
await client.tokenDeposit(group, mangoAccount, solMint, 0.15);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
} catch (error) {
console.log(error);
}

View File

@ -1,13 +1,13 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoAccount } from '../accounts/mangoAccount';
import { PerpOrderSide, PerpOrderType } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../accounts/serum3';
import { Side, PerpOrderType } from '../accounts/perp';
import { MangoAccount } from '../accounts/mangoAccount';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
@ -56,8 +56,9 @@ async function main() {
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
@ -105,9 +106,9 @@ async function main() {
group,
mangoAccount,
assetMint,
assetAmount,
new BN(assetAmount),
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
if (liabAmount > 0) {
// temporarily drop the borrowed token value, so the borrow goes through
@ -119,7 +120,7 @@ async function main() {
group,
mangoAccount,
liabMint,
liabAmount,
new BN(liabAmount),
true,
);
} finally {
@ -139,12 +140,17 @@ async function main() {
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const market = group.getSerum3MarketByIndexByName('SOL/USDC')!;
const market = group.getSerum3MarketByName('SOL/USDC')!;
const sellMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const buyMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
await client.tokenDepositNative(group, mangoAccount, sellMint, 100000);
await mangoAccount.reload(client, group);
await client.tokenDepositNative(
group,
mangoAccount,
sellMint,
new BN(100000),
);
await mangoAccount.reload(client);
// temporarily up the init asset weight of the bought token
await client.tokenEdit(
@ -215,9 +221,9 @@ async function main() {
group,
mangoAccount,
collateralMint,
100000,
new BN(100000),
); // valued as $0.004 maint collateral
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4);
@ -225,8 +231,8 @@ async function main() {
await client.perpPlaceOrder(
group,
mangoAccount,
'MNGO-PERP',
Side.bid,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.bid,
1, // ui price that won't get hit
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
@ -258,9 +264,9 @@ async function main() {
group,
mangoAccount,
collateralMint,
100000,
new BN(100000),
); // valued as $0.004 maint collateral
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5);
@ -268,8 +274,8 @@ async function main() {
await client.perpPlaceOrder(
group,
fundingAccount,
'MNGO-PERP',
Side.ask,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.ask,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
@ -282,8 +288,8 @@ async function main() {
await client.perpPlaceOrder(
group,
mangoAccount,
'MNGO-PERP',
Side.bid,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.bid,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
@ -293,7 +299,10 @@ async function main() {
5,
);
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
await client.perpConsumeAllEvents(
group,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
);
} finally {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
}
@ -320,9 +329,9 @@ async function main() {
group,
mangoAccount,
collateralMint,
100000,
new BN(100000),
); // valued as $0.004 maint collateral
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
try {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 10);
@ -332,18 +341,18 @@ async function main() {
group,
mangoAccount,
liabMint,
-5000,
new BN(-5000),
true,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
// Execute two trades that leave the account with +$0.022 positive pnl
await client.stubOracleSet(group, baseOracle, PRICES['MNGO'] / 2);
await client.perpPlaceOrder(
group,
fundingAccount,
'MNGO-PERP',
Side.ask,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.ask,
20,
0.0011, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
@ -355,8 +364,8 @@ async function main() {
await client.perpPlaceOrder(
group,
mangoAccount,
'MNGO-PERP',
Side.bid,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.bid,
20,
0.0011, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
@ -365,15 +374,18 @@ async function main() {
0,
5,
);
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
await client.perpConsumeAllEvents(
group,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
);
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
await client.perpPlaceOrder(
group,
fundingAccount,
'MNGO-PERP',
Side.bid,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.bid,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
@ -385,8 +397,8 @@ async function main() {
await client.perpPlaceOrder(
group,
mangoAccount,
'MNGO-PERP',
Side.ask,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.ask,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
@ -395,7 +407,10 @@ async function main() {
0,
5,
);
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
await client.perpConsumeAllEvents(
group,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
);
} finally {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);

View File

@ -1,8 +1,7 @@
import { BN, AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { Side, PerpOrderType } from '../accounts/perp';
import { MANGO_V4_ID } from '../constants';
//
@ -32,8 +31,9 @@ async function main() {
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
@ -49,14 +49,14 @@ async function main() {
let accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
for (let account of accounts) {
for (let serumOrders of account.serum3Active()) {
const serumMarket = group.getSerum3MarketByIndex(
const serumMarket = group.getSerum3MarketByMarketIndex(
serumOrders.marketIndex,
)!;
const serumExternal = serumMarket.serumMarketExternal;
console.log(
`closing serum orders on: ${account} for market ${serumMarket.name}`,
);
await client.serum3CancelAllorders(group, account, serumExternal, 10);
await client.serum3CancelAllOrders(group, account, serumExternal, 10);
await client.serum3SettleFunds(group, account, serumExternal);
await client.serum3CloseOpenOrders(group, account, serumExternal);
}
@ -66,7 +66,12 @@ async function main() {
console.log(
`closing perp orders on: ${account} for market ${perpMarket.name}`,
);
await client.perpCancelAllOrders(group, account, perpMarket.name, 10);
await client.perpCancelAllOrders(
group,
account,
perpMarket.perpMarketIndex,
10,
);
}
}

View File

@ -1,52 +0,0 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { Serum3Side } from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// (untested?) script which closes a mango account cleanly, first closes all positions, withdraws all tokens and then closes it
//
async function viewUnownedAccount(userKeypairFile: string) {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.rpcpool.com/0f9acc0d45173b51bf7d7e09c1e5',
options,
);
// user
const userWallet = new Wallet(Keypair.generate());
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// admin
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR || '', 'utf-8')),
),
);
console.log(`Admin ${admin.publicKey.toBase58()}`);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, 2);
console.log(`Found group ${group.publicKey.toBase58()}`);
const x = await client.getMangoAccount(
new PublicKey('6cTqJrSzQZWGEeHHePqFuJV4Kf54YDVfSamdCrT3agw6'),
);
const y = await x.reloadAccountData(client, group);
process.exit();
}
async function main() {
await viewUnownedAccount(process.env.MB_USER2_KEYPAIR || '');
}
main();

View File

@ -22,8 +22,9 @@ async function main() {
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
// Load mango account

View File

@ -7,6 +7,7 @@ import {
TransactionInstruction,
} from '@solana/web3.js';
import fs from 'fs';
import { RestClient } from 'ftx-api';
import path from 'path';
import { Group } from '../../accounts/group';
import { MangoAccount } from '../../accounts/mangoAccount';
@ -27,7 +28,12 @@ import {
seqEnforcerProgramIds,
} from './sequence-enforcer-util';
// TODO switch to more efficient async logging if available in nodejs
// Future
// * use async nodejs logging
// * merge gMa calls
// * take out spammers
// * batch ixs across various markets
// * only refresh part of the group which market maker is interested in
// Env vars
const CLUSTER: Cluster =
@ -57,14 +63,14 @@ type State = {
};
type MarketContext = {
params: any;
market: PerpMarket;
perpMarket: PerpMarket;
bids: BookSide;
asks: BookSide;
lastBookUpdate: number;
aggBid: number | undefined;
aggAsk: number | undefined;
ftxMid: number | undefined;
ftxBid: number | undefined;
ftxAsk: number | undefined;
ftxLast: number | undefined;
sequenceAccount: PublicKey;
sequenceAccountBump: number;
@ -74,7 +80,18 @@ type MarketContext = {
lastOrderUpdate: number;
};
// Refresh mango account and perp market order books
const ftxClient = new RestClient();
function getPerpMarketAssetsToTradeOn(group: Group) {
const allMangoGroupPerpMarketAssets = Array.from(
group.perpMarketsMapByName.keys(),
).map((marketName) => marketName.replace('-PERP', ''));
return Object.keys(params.assets).filter((asset) =>
allMangoGroupPerpMarketAssets.includes(asset),
);
}
// Refresh group, mango account and perp markets
async function refreshState(
client: MangoClient,
group: Group,
@ -83,18 +100,27 @@ async function refreshState(
): Promise<State> {
const ts = Date.now() / 1000;
// TODO do all updates in one RPC call
await Promise.all([group.reloadAll(client), mangoAccount.reload(client)]);
const result = await Promise.all([
group.reloadAll(client),
mangoAccount.reload(client),
...Array.from(marketContexts.values()).map((mc) =>
ftxClient.getMarket(mc.perpMarket.name),
),
]);
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
const mc = marketContexts.get(perpMarket.perpMarketIndex)!;
mc.market = perpMarket;
Array.from(marketContexts.values()).map(async (mc, i) => {
const perpMarket = mc.perpMarket;
mc.perpMarket = group.getPerpMarketByMarketIndex(
perpMarket.perpMarketIndex,
);
mc.bids = await perpMarket.loadBids(client);
mc.asks = await perpMarket.loadAsks(client);
mc.lastBookUpdate = ts;
}
mc.ftxAsk = (result[i + 2] as any).result.ask;
mc.ftxBid = (result[i + 2] as any).result.bid;
mc.ftxLast = (result[i + 2] as any).result.last;
});
return {
mangoAccount,
@ -113,7 +139,7 @@ async function initSequenceEnforcerAccounts(
mc.sequenceAccount,
(client.program.provider as AnchorProvider).wallet.publicKey,
mc.sequenceAccountBump,
mc.market.name,
mc.perpMarket.name,
CLUSTER,
),
);
@ -138,6 +164,40 @@ async function initSequenceEnforcerAccounts(
}
}
async function cancelAllOrdersForAMarket(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
perpMarket: PerpMarket,
) {
for (const i of Array(100).keys()) {
await sendTransaction(
client.program.provider as AnchorProvider,
[
await client.perpCancelAllOrdersIx(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
),
],
[],
);
await mangoAccount.reload(client);
if (
(
await mangoAccount.loadPerpOpenOrdersForMarket(
client,
group,
perpMarket.perpMarketIndex,
)
).length === 0
) {
break;
}
}
}
// Cancel all orders on exit
async function onExit(
client: MangoClient,
@ -145,17 +205,9 @@ async function onExit(
mangoAccount: MangoAccount,
marketContexts: MarketContext[],
) {
const ixs: TransactionInstruction[] = [];
for (const mc of marketContexts) {
const cancelAllIx = await client.perpCancelAllOrdersIx(
group,
mangoAccount,
mc.market.perpMarketIndex,
10,
);
ixs.push(cancelAllIx);
cancelAllOrdersForAMarket(client, group, mangoAccount, mc.perpMarket);
}
await sendTransaction(client.program.provider as AnchorProvider, ixs, []);
}
// Main driver for the market maker
@ -166,15 +218,15 @@ async function fullMarketMaker() {
const user = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))),
);
// TODO: make work for delegate
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
// Load mango account
@ -182,7 +234,9 @@ async function fullMarketMaker() {
new PublicKey(MANGO_ACCOUNT_PK),
);
console.log(
`MangoAccount ${mangoAccount.publicKey} for user ${user.publicKey}`,
`MangoAccount ${mangoAccount.publicKey} for user ${user.publicKey} ${
mangoAccount.isDelegate(client) ? 'via delegate ' + user.publicKey : ''
}`,
);
await mangoAccount.reload(client);
@ -204,9 +258,8 @@ async function fullMarketMaker() {
// Build and maintain an aggregate context object per market
const marketContexts: Map<PerpMarketIndex, MarketContext> = new Map();
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
for (const perpMarketAsset of getPerpMarketAssetsToTradeOn(group)) {
const perpMarket = group.getPerpMarketByName(perpMarketAsset + '-PERP');
const [sequenceAccount, sequenceAccountBump] =
await PublicKey.findProgramAddress(
[
@ -218,8 +271,8 @@ async function fullMarketMaker() {
seqEnforcerProgramIds[CLUSTER],
);
marketContexts.set(perpMarket.perpMarketIndex, {
params: params.assets[perpMarket.name.replace('-PERP', '')].perp,
market: perpMarket,
params: params.assets[perpMarketAsset].perp,
perpMarket: perpMarket,
bids: await perpMarket.loadBids(client),
asks: await perpMarket.loadAsks(client),
lastBookUpdate: 0,
@ -231,10 +284,9 @@ async function fullMarketMaker() {
sentAskPrice: 0,
lastOrderUpdate: 0,
// TODO
aggBid: undefined,
aggAsk: undefined,
ftxMid: undefined,
ftxBid: undefined,
ftxAsk: undefined,
ftxLast: undefined,
});
}
@ -257,8 +309,9 @@ async function fullMarketMaker() {
// Loop indefinitely
while (control.isRunning) {
try {
// TODO update this in a non blocking manner
state = await refreshState(client, group, mangoAccount, marketContexts);
refreshState(client, group, mangoAccount, marketContexts).then(
(result) => (state = result),
);
mangoAccount = state.mangoAccount;
@ -267,16 +320,15 @@ async function fullMarketMaker() {
for (const mc of Array.from(marketContexts.values())) {
const pos = mangoAccount.getPerpPositionUi(
group,
mc.market.perpMarketIndex,
mc.perpMarket.perpMarketIndex,
);
// TODO use ftx to get mid then also combine with books from other exchanges
const midWorkaround = mc.market.uiPrice;
if (midWorkaround) {
pfQuoteValue += pos * midWorkaround;
const mid = (mc.ftxBid! + mc.ftxAsk!) / 2;
if (mid) {
pfQuoteValue += pos * mid;
} else {
pfQuoteValue = undefined;
console.log(
`Breaking pfQuoteValue computation, since mid is undefined for ${mc.market.name}!`,
`Breaking pfQuoteValue computation, since mid is undefined for ${mc.perpMarket.name}!`,
);
break;
}
@ -303,7 +355,6 @@ async function fullMarketMaker() {
continue;
}
// TODO: batch ixs
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
ixs,
@ -333,13 +384,13 @@ async function makeMarketUpdateInstructions(
mc: MarketContext,
pfQuoteValue: number,
): Promise<TransactionInstruction[]> {
const perpMarketIndex = mc.market.perpMarketIndex;
const perpMarket = mc.market;
const perpMarketIndex = mc.perpMarket.perpMarketIndex;
const perpMarket = mc.perpMarket;
const aggBid = perpMarket.uiPrice; // TODO mc.aggBid;
const aggAsk = perpMarket.uiPrice; // TODO mc.aggAsk;
const aggBid = mc.ftxBid;
const aggAsk = mc.ftxAsk;
if (aggBid === undefined || aggAsk === undefined) {
console.log(`No Aggregate Book for ${mc.market.name}!`);
console.log(`No Aggregate Book for ${mc.perpMarket.name}!`);
return [];
}
@ -353,8 +404,7 @@ async function makeMarketUpdateInstructions(
const sizePerc = mc.params.sizePerc;
const quoteSize = equity * sizePerc;
const size = quoteSize / fairValue;
// TODO look at event queue as well for unprocessed fills
const basePos = mangoAccount.getPerpPositionUi(group, perpMarketIndex);
const basePos = mangoAccount.getPerpPositionUi(group, perpMarketIndex, true);
const lean = (-leanCoeff * basePos) / size;
const pfQuoteLeanCoeff = params.pfQuoteLeanCoeff || 0.001; // How much to move if pf pos is equal to equity
const pfQuoteLean = (pfQuoteValue / equity) * -pfQuoteLeanCoeff;
@ -362,9 +412,6 @@ async function makeMarketUpdateInstructions(
const bias = mc.params.bias;
const bidPrice = fairValue * (1 - charge + lean + bias + pfQuoteLean);
const askPrice = fairValue * (1 + charge + lean + bias + pfQuoteLean);
// TODO volatility adjustment
const modelBidPrice = perpMarket.uiPriceToLots(bidPrice);
const nativeBidSize = perpMarket.uiBaseToLots(size);
const modelAskPrice = perpMarket.uiPriceToLots(askPrice);
@ -383,15 +430,8 @@ async function makeMarketUpdateInstructions(
? BN.max(bestBid.priceLots.add(new BN(1)), modelAskPrice)
: modelAskPrice;
// TODO use order book to requote if size has changed
// TODO
// const takeSpammers = mc.params.takeSpammers;
// const spammerCharge = mc.params.spammerCharge;
let moveOrders = false;
if (mc.lastBookUpdate >= mc.lastOrderUpdate + 2) {
// console.log(` - moveOrders - 303`);
// If mango book was updated recently, then MangoAccount was also updated
const openOrders = await mangoAccount.loadPerpOpenOrdersForMarket(
client,
@ -407,9 +447,6 @@ async function makeMarketUpdateInstructions(
requoteThresh;
}
} else {
// console.log(
// ` - moveOrders - 319, mc.lastBookUpdate ${mc.lastBookUpdate}, mc.lastOrderUpdate ${mc.lastOrderUpdate}`,
// );
// If order was updated before MangoAccount, then assume that sent order already executed
moveOrders =
moveOrders ||
@ -427,7 +464,9 @@ async function makeMarketUpdateInstructions(
),
];
// TODO Clear 1 lot size orders at the top of book that bad people use to manipulate the price
instructions.push(
await client.healthRegionBeginIx(group, mangoAccount, [], [perpMarket]),
);
if (moveOrders) {
// Cancel all, requote
@ -446,7 +485,6 @@ async function makeMarketUpdateInstructions(
mangoAccount,
perpMarketIndex,
PerpOrderSide.bid,
// TODO fix this, native to ui to native
perpMarket.priceLotsToUi(bookAdjBid),
perpMarket.baseLotsToUi(nativeBidSize),
undefined,
@ -479,7 +517,7 @@ async function makeMarketUpdateInstructions(
instructions.push(placeAskIx);
}
console.log(
`Requoting for market ${mc.market.name} sentBid: ${
`Requoting for market ${mc.perpMarket.name} sentBid: ${
mc.sentBidPrice
} newBid: ${bookAdjBid} sentAsk: ${
mc.sentAskPrice
@ -492,12 +530,16 @@ async function makeMarketUpdateInstructions(
mc.lastOrderUpdate = Date.now() / 1000;
} else {
console.log(
`Not requoting for market ${mc.market.name}. No need to move orders`,
`Not requoting for market ${mc.perpMarket.name}. No need to move orders`,
);
}
// If instruction is only the sequence enforcement, then just send empty
if (instructions.length === 1) {
instructions.push(
await client.healthRegionEndIx(group, mangoAccount, [], [perpMarket]),
);
// If instruction is only the sequence enforcement and health region ixs, then just send empty
if (instructions.length === 3) {
return [];
} else {
return instructions;

View File

@ -86,8 +86,9 @@ async function main() {
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
{
idsSource: 'get-program-accounts',
},
);
// Load mango account

View File

@ -34,9 +34,11 @@ export function toUiDecimals(
nativeAmount: BN | I80F48 | number,
decimals: number,
): number {
// TODO: remove BN and upgrade to bigint https://github.com/solana-labs/solana/issues/27440
if (nativeAmount instanceof BN) {
return nativeAmount.div(new BN(Math.pow(10, decimals))).toNumber();
} else if (nativeAmount instanceof I80F48) {
nativeAmount = I80F48.fromU64(nativeAmount);
}
if (nativeAmount instanceof I80F48) {
return nativeAmount
.div(I80F48.fromNumber(Math.pow(10, decimals)))
.toNumber();

View File

@ -45,8 +45,6 @@ export async function sendTransaction(
latestBlockhash.blockhash != null &&
latestBlockhash.lastValidBlockHeight != null
) {
// TODO: tyler, can we remove these?
console.log('confirming via blockhash');
status = (
await connection.confirmTransaction(
{
@ -58,8 +56,6 @@ export async function sendTransaction(
)
).value;
} else {
// TODO: tyler, can we remove these?
console.log('confirming via timeout');
status = (await connection.confirmTransaction(signature, 'processed'))
.value;
}

View File

@ -2,7 +2,9 @@
"compilerOptions": {
"esModuleInterop": true,
"moduleResolution": "node",
"lib": ["es2019"],
"lib": [
"es2019"
],
"outDir": "./dist",
"resolveJsonModule": true,
"noImplicitAny": false,
@ -11,12 +13,14 @@
"target": "es2019",
"strictNullChecks": true
},
"include": ["ts/client/src", "ts/client/scripts", "ts/client/scripts"],
"include": [
"ts/client/src",
"ts/client/src/scripts",
"ts/client/src/debug-scripts",
"ts/client/src/deployment-scripts"
],
"exclude": [
"./ts/**/*.test.js",
"node_modules",
"**/node_modules",
"./ts/client/src/scripts",
"./ts/client/src/debug-scripts"
]
}
}

View File

@ -1027,7 +1027,7 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
axios@^0.21.1:
axios@^0.21.1, axios@^0.21.4:
version "0.21.4"
resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz"
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
@ -1904,6 +1904,15 @@ fsevents@~2.3.2:
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
ftx-api@^1.1.13:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ftx-api/-/ftx-api-1.1.13.tgz#89ced81182020c80bcf5a97082fd3b2b62d1fea3"
integrity sha512-YZdiU4QN2H8wId4Bglb/Yj6hJrLImKv/yEIj3jw1pHMb1Bv/kdM25mlr+t+TqGPWy2prX/ywsh1+OVVI6G5jhA==
dependencies:
axios "^0.21.4"
isomorphic-ws "^4.0.1"
ws "^7.4.0"
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@ -3465,7 +3474,7 @@ wrappy@1:
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^7.4.5:
ws@^7.4.0, ws@^7.4.5:
version "7.5.9"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==