Merge tag 'program-v0.14.0' into deploy

This commit is contained in:
Christian Kamm 2023-05-01 11:30:05 +02:00
commit 514f861e2f
66 changed files with 2987 additions and 241 deletions

View File

@ -8,8 +8,7 @@ on:
required: true
type: string
imageName:
description: 'Image Name'
description: 'liquidator, keeper, mm, settler'
description: 'Docker Image Name'
required: true
type: string
imageTag:

View File

@ -12,17 +12,11 @@ on:
'bin/liquidator/**',
'bin/settler/**',
]
workflow_call:
secrets:
GCR_PROJECT:
required: true
GCR_SA_KEY:
required: true
workflow_dispatch:
env:
PROJECT_ID: ${{ secrets.GCR_PROJECT }}
IMAGE: mango-v4
REGISTRY: ghcr.io
jobs:
build:
@ -41,21 +35,13 @@ jobs:
install: true
buildkitd-flags: --debug
# Login to Google Cloud
- name: 'Login to Google Cloud'
uses: 'google-github-actions/auth@v0'
id: auth
with:
token_format: 'access_token'
credentials_json: '${{ secrets.GCR_SA_KEY }}'
# Login to GCR
- name: Login to GCR
# Login to Registry
- name: Login to Registry
uses: docker/login-action@v2
with:
registry: us-docker.pkg.dev
username: oauth2accesstoken
password: ${{ steps.auth.outputs.access_token }}
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Build and push the base image, leveraging layer caching
- name: Build and Push Base Image
@ -64,37 +50,7 @@ jobs:
context: .
push: true
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
${{ env.REGISTRY }}/blockworks-foundation/${{ env.IMAGE }}:${{ github.sha }}
${{ env.REGISTRY }}/blockworks-foundation/${{ env.IMAGE }}:latest
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
with:
file: bin/liquidator/Dockerfile.liquidator
context: .
push: true
tags: |
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-liquidator:${{ github.sha }}
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-liquidator:latest
# Build and push the settler runtime image
- name: Build and Push Settle Bot
uses: docker/build-push-action@v2
with:
file: bin/settler/Dockerfile.settler
context: .
push: true
tags: |
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-settler:${{ github.sha }}
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-settler:latest
# Build and push the keeper runtime image
- name: Build and Push Keeper
uses: docker/build-push-action@v2
with:
file: bin/keeper/Dockerfile.keeper
context: .
push: true
tags: |
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-keeper:${{ github.sha }}
us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-keeper:latest

Binary file not shown.

View File

@ -4,10 +4,40 @@ Update this for each program release and mainnet deployment.
## not on mainnet
### v0.13.0, 2023-4-
### v0.14.0, 2023-4-
Deployment:
- Force-closing of perp positions (#525)
When a perp markets is set to "force-close" by the DAO, anyone can close open
perp orders and positions on the market. This allows the DAO to wind down perp
markets if needed.
- Force-closing of OpenBook market use via Mango (#551)
When an OpenBook market's Mango integration is set to "force-close" by the DAO,
anyone can close open orders on that market that were placed via Mango.
This allows the DAO to wind down interactions with an OpenBook market.
- Fix exception for the Jupiter program in flash loan (#552)
Account delegates cannot execute generic flash loans, but were supposed to be
able to use whitelisted Jupiter programs during a flash loan. The bug that
prevented the exception from working was fixed.
- Allow the DAO to withdraw from the insurance fund token account (#561)
- Fix a bug with settle limit accounting when liqors take over positive pnl (#562)
- Improve logging on force-close instructions (#555)
- Fix perp order seqnum logging (#556)
- Fix build when using mango-v4 code with the "no-entrypoint" feature (#558)
## mainnet
### v0.13.0, 2023-4-18
Deployment: Apr 18, 2023 at 17:33:15 Central European Summer Time, https://explorer.solana.com/tx/4WWVHCAheTRBhzyXUjsV1Kqfn8LdnkupiVbK4qaPNqby8P5vv7hY6HS3rHHL9bMu1RGdCZvqsd2MHjdawLYQ6Pxi
- Add explicit token account checks to FlashLoan (#542)
It looks like the reported security issue was not exploitable, but the guards
@ -50,9 +80,9 @@ Deployment:
The security admin was not supposed to be able to enable instructions, but a bug
allowed it. With this fix, only the group admin (DAO) can enable instructions.
### v0.12.0, 2023-4-
### v0.12.0, 2023-4-17
Deployment:
Deployment: Apr 17, 2023 at 15:49:33 Central European Summer Time, https://explorer.solana.com/tx/2PbaCRMGgpGiysxk5y8x3TdFRZbGEAKZdyAzEQhAMXfCxS4bPN96YZ4Pp6hHfp17fd7RYUd13t4vtjpaFb4ccYRm
- Emit perp fees settled on update_funding (#530)
@ -62,8 +92,6 @@ Deployment:
That way it's easier to be specific about where the limit should be checked.
## mainnet
### v0.11.0, 2023-4-4
Deployment: Apr 4, 2023 at 21:43:18 Central European Summer Time, https://explorer.solana.com/tx/5Z36iV6VhAfmxwZubQduV1hNyUyyB9AyjovAwNrWLb5cdAqGm4F3NGmz6V8VpHT6yUwCEDxm2hWMrdJXNkZ8RSPR

2
Cargo.lock generated
View File

@ -3005,7 +3005,7 @@ dependencies = [
[[package]]
name = "mango-v4"
version = "0.13.0"
version = "0.14.0"
dependencies = [
"anchor-lang",
"anchor-spl",

View File

@ -1,5 +1,5 @@
# syntax = docker/dockerfile:1.2
# Base image containing all binaries, deployed to gcr.io/mango-markets/mango-v4:latest
# Base image containing all binaries, deployed to ghcr.io/blockworks-foundation/mango-v4:latest
FROM rust:1.65 as base
# RUN cargo install cargo-chef --locked
RUN rustup component add rustfmt
@ -12,15 +12,13 @@ COPY . .
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
# RUN cargo chef prepare --recipe-path recipe.json
FROM base as build
COPY --from=plan /app/recipe-*.json .
# COPY --from=plan /app/recipe.json .
COPY . .
# RUN cargo chef cook --release --recipe-path recipe-keeper.json --bin keeper
# RUN cargo chef cook --release --recipe-path recipe-liquidator.json --bin liquidator
RUN cargo build --release --bin keeper --bin liquidator --bin settler
# RUN cargo chef cook --release --recipe-path recipe.json
RUN cargo build --release --bins
FROM debian:bullseye-slim as run
RUN apt-get update && apt-get -y install ca-certificates libc6

View File

@ -1 +1,3 @@
mm: node dist/cjs/scripts/mm/market-maker.js
mm: node dist/cjs/scripts/mm/market-maker.js
rebalancer: node dist/cjs/scripts/rebalancer.js
keeper: node dist/cjs/scripts/keeper/keeper.js

View File

@ -2,7 +2,7 @@
# heroku container:push keeper -R -a HEROKU_APP_NAME
# heroku container:release -a HEROKU_APP_NAME
ARG BASE_TAG=latest
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG
FROM ghcr.io/blockworks-foundation/mango-v4:$BASE_TAG
ENTRYPOINT ["keeper"]
USER mangouser
CMD ["crank"]

View File

@ -2,6 +2,6 @@
# heroku container:push keeper -R -a HEROKU_APP_NAME
# heroku container:release -a HEROKU_APP_NAME
ARG BASE_TAG=latest
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG
FROM ghcr.io/blockworks-foundation/mango-v4:$BASE_TAG
USER mangouser
CMD ["liquidator"]

View File

@ -77,9 +77,8 @@ impl Rebalancer {
pub async fn zero_all_non_quote(&self) -> anyhow::Result<()> {
log::trace!("checking for rebalance: {}", self.mango_account_address);
// FIXME: re-enable when flash loans are back
// self.rebalance_tokens().await?;
self.rebalance_perps().await?;
self.rebalance_tokens().await?;
Ok(())
}

View File

@ -2,6 +2,6 @@
# heroku container:push keeper -R -a HEROKU_APP_NAME
# heroku container:release -a HEROKU_APP_NAME
ARG BASE_TAG=latest
FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG
FROM ghcr.io/blockworks-foundation/mango-v4:$BASE_TAG
USER mangouser
CMD ["settler"]

View File

@ -18,7 +18,7 @@ use itertools::Itertools;
use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::state::{
Bank, Group, MangoAccountValue, PerpMarketIndex, PlaceOrderType, Serum3MarketIndex, Side,
TokenIndex,
TokenIndex, INSURANCE_TOKEN_INDEX,
};
use solana_address_lookup_table_program::state::AddressLookupTable;
@ -305,6 +305,7 @@ impl MangoClient {
pub async fn derive_liquidation_health_check_remaining_account_metas(
&self,
liqee: &MangoAccountValue,
affected_tokens: Vec<u16>,
writable_banks: &[TokenIndex],
) -> anyhow::Result<Vec<AccountMeta>> {
let account = self.mango_account().await?;
@ -312,6 +313,7 @@ impl MangoClient {
.derive_health_check_remaining_account_metas_two_accounts(
&account,
liqee,
&affected_tokens,
writable_banks,
)
}
@ -880,7 +882,12 @@ impl MangoClient {
let health_remaining_ams = self
.context
.derive_health_check_remaining_account_metas_two_accounts(account_a.1, account_b.1, &[])
.derive_health_check_remaining_account_metas_two_accounts(
account_a.1,
account_b.1,
&[],
&[],
)
.unwrap();
Ok(Instruction {
@ -963,7 +970,7 @@ impl MangoClient {
let settle_token_info = self.context.token(perp.market.settle_token_index);
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(liqee.1, &[])
.derive_liquidation_health_check_remaining_account_metas(liqee.1, vec![], &[])
.await
.unwrap();
@ -1013,7 +1020,11 @@ impl MangoClient {
let settle_token_info = self.context.token(perp.market.settle_token_index);
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(liqee.1, &[])
.derive_liquidation_health_check_remaining_account_metas(
liqee.1,
vec![INSURANCE_TOKEN_INDEX],
&[],
)
.await
.unwrap();
@ -1060,6 +1071,7 @@ impl MangoClient {
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(
liqee.1,
vec![],
&[asset_token_index, liab_token_index],
)
.await
@ -1110,6 +1122,7 @@ impl MangoClient {
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(
liqee.1,
vec![INSURANCE_TOKEN_INDEX],
&[quote_token_index, liab_token_index],
)
.await

View File

@ -280,6 +280,7 @@ impl MangoGroupContext {
&self,
account1: &MangoAccountValue,
account2: &MangoAccountValue,
affected_tokens: &[TokenIndex],
writable_banks: &[TokenIndex],
) -> anyhow::Result<Vec<AccountMeta>> {
// figure out all the banks/oracles that need to be passed for the health check
@ -290,6 +291,7 @@ impl MangoGroupContext {
.active_token_positions()
.chain(account1.active_token_positions())
.map(|ta| ta.token_index)
.chain(affected_tokens.iter().copied())
.unique();
for token_index in token_indexes {

View File

@ -1,5 +1,5 @@
{
"version": "0.13.0",
"version": "0.14.0",
"name": "mango_v4",
"instructions": [
{
@ -177,6 +177,42 @@
}
]
},
{
"name": "groupWithdrawInsuranceFund",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "admin",
"isMut": false,
"isSigner": true
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false
},
{
"name": "destination",
"isMut": true,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "amount",
"type": "u64"
}
]
},
{
"name": "ixGateSet",
"accounts": [
@ -1743,6 +1779,12 @@
"type": {
"option": "bool"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -3092,6 +3134,12 @@
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -3635,6 +3683,37 @@
],
"args": []
},
{
"name": "perpForceClosePosition",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "accountA",
"isMut": true,
"isSigner": false
},
{
"name": "accountB",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpSettleFees",
"accounts": [
@ -3924,7 +4003,13 @@
"benchmark",
""
],
"accounts": [],
"accounts": [
{
"name": "dummy",
"isMut": false,
"isSigner": false
}
],
"args": []
}
],
@ -5048,12 +5133,16 @@
],
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding4",
"type": {
"array": [
"u8",
7
6
]
}
},
@ -5111,12 +5200,16 @@
"name": "reduceOnly",
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding1",
"type": {
"array": [
"u8",
3
2
]
}
},
@ -7065,6 +7158,29 @@
},
{
"name": "TokenForceCloseBorrowsWithToken"
},
{
"name": "PerpForceClosePosition"
},
{
"name": "GroupWithdrawInsuranceFund"
}
]
}
},
{
"name": "CheckLiquidatable",
"type": {
"kind": "enum",
"variants": [
{
"name": "NotLiquidatable"
},
{
"name": "Liquidatable"
},
{
"name": "BecameNotLiquidatable"
}
]
}
@ -8561,6 +8677,101 @@
"index": false
}
]
},
{
"name": "PerpForceClosePositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "accountA",
"type": "publicKey",
"index": false
},
{
"name": "accountB",
"type": "publicKey",
"index": false
},
{
"name": "baseTransfer",
"type": "i64",
"index": false
},
{
"name": "quoteTransfer",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "TokenForceCloseBorrowsWithTokenLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "assetTokenIndex",
"type": "u16",
"index": false
},
{
"name": "liabTokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetTransfer",
"type": "i128",
"index": false
},
{
"name": "liabTransfer",
"type": "i128",
"index": false
},
{
"name": "assetPrice",
"type": "i128",
"index": false
},
{
"name": "liabPrice",
"type": "i128",
"index": false
},
{
"name": "feeFactor",
"type": "i128",
"index": false
}
]
}
],
"errors": [

View File

@ -1,6 +1,6 @@
[package]
name = "mango-v4"
version = "0.13.0"
version = "0.14.0"
description = "Created with Anchor"
edition = "2021"

View File

@ -1,4 +1,7 @@
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct Benchmark {}
pub struct Benchmark<'info> {
/// CHECK: unused, exists only because anchor is unhappy in no-entrypoint mode otherwise
pub dummy: UncheckedAccount<'info>,
}

View File

@ -0,0 +1,34 @@
use crate::{error::MangoError, state::*};
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};
#[derive(Accounts)]
pub struct GroupWithdrawInsuranceFund<'info> {
#[account(
has_one = insurance_vault,
has_one = admin,
constraint = group.load()?.is_ix_enabled(IxGate::GroupWithdrawInsuranceFund) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
pub admin: Signer<'info>,
#[account(mut)]
pub insurance_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub destination: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
impl<'info> GroupWithdrawInsuranceFund<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.insurance_vault.to_account_info(),
to: self.destination.to_account_info(),
authority: self.group.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

View File

@ -12,6 +12,7 @@ pub use flash_loan::*;
pub use group_close::*;
pub use group_create::*;
pub use group_edit::*;
pub use group_withdraw_insurance_fund::*;
pub use health_region::*;
pub use ix_gate_set::*;
pub use perp_cancel_all_orders::*;
@ -23,6 +24,7 @@ pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_deactivate_position::*;
pub use perp_edit_market::*;
pub use perp_force_close_position::*;
pub use perp_liq_base_or_positive_pnl::*;
pub use perp_liq_force_cancel_orders::*;
pub use perp_liq_negative_pnl_or_bankruptcy::*;
@ -69,6 +71,7 @@ mod flash_loan;
mod group_close;
mod group_create;
mod group_edit;
mod group_withdraw_insurance_fund;
mod health_region;
mod ix_gate_set;
mod perp_cancel_all_orders;
@ -80,6 +83,7 @@ mod perp_consume_events;
mod perp_create_market;
mod perp_deactivate_position;
mod perp_edit_market;
mod perp_force_close_position;
mod perp_liq_base_or_positive_pnl;
mod perp_liq_force_cancel_orders;
mod perp_liq_negative_pnl_or_bankruptcy;

View File

@ -0,0 +1,37 @@
use crate::error::*;
use crate::state::*;
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct PerpForceClosePosition<'info> {
#[account(
constraint = group.load()?.is_ix_enabled(IxGate::PerpForceClosePosition) @ MangoError::IxIsDisabled,
)]
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
has_one = oracle,
constraint = perp_market.load()?.is_force_close()
)]
pub perp_market: AccountLoader<'info, PerpMarket>,
#[account(
mut,
has_one = group,
constraint = account_a.load()?.is_operational() @ MangoError::AccountIsFrozen,
constraint = account_a.key() != account_b.key()
)]
pub account_a: AccountLoader<'info, MangoAccountFixed>,
#[account(
mut,
has_one = group,
constraint = account_b.load()?.is_operational() @ MangoError::AccountIsFrozen
)]
pub account_b: AccountLoader<'info, MangoAccountFixed>,
/// CHECK: Oracle can have different account types, constrained by address in perp_market
pub oracle: UncheckedAccount<'info>,
}

View File

@ -137,7 +137,8 @@ pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
require_msg!(
ix.program_id == AssociatedToken::id()
|| ix.program_id == jupiter_mainnet_3::ID
|| ix.program_id == jupiter_mainnet_4::ID,
|| ix.program_id == jupiter_mainnet_4::ID
|| ix.program_id == crate::id(),
"delegate is only allowed to pass in ixs to ATA or Jupiter v3 or v4 programs"
);
}

View File

@ -0,0 +1,19 @@
use anchor_lang::prelude::*;
use anchor_spl::token;
use crate::{accounts_ix::GroupWithdrawInsuranceFund, group_seeds};
pub fn group_withdraw_insurance_fund(
ctx: Context<GroupWithdrawInsuranceFund>,
amount: u64,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
token::transfer(
ctx.accounts.transfer_ctx().with_signer(&[group_seeds]),
amount.min(ctx.accounts.insurance_vault.amount),
)?;
Ok(())
}

View File

@ -66,6 +66,8 @@ pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
log_if_changed(&group, ix_gate, IxGate::TokenWithdraw);
log_if_changed(&group, ix_gate, IxGate::AccountBuybackFeesWithMngo);
log_if_changed(&group, ix_gate, IxGate::TokenForceCloseBorrowsWithToken);
log_if_changed(&group, ix_gate, IxGate::PerpForceClosePosition);
log_if_changed(&group, ix_gate, IxGate::GroupWithdrawInsuranceFund);
group.ix_gate = ix_gate;

View File

@ -12,6 +12,7 @@ pub use flash_loan::*;
pub use group_close::*;
pub use group_create::*;
pub use group_edit::*;
pub use group_withdraw_insurance_fund::*;
pub use health_region::*;
pub use ix_gate_set::*;
pub use perp_cancel_all_orders::*;
@ -23,6 +24,7 @@ pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_deactivate_position::*;
pub use perp_edit_market::*;
pub use perp_force_close_position::*;
pub use perp_liq_base_or_positive_pnl::*;
pub use perp_liq_force_cancel_orders::*;
pub use perp_liq_negative_pnl_or_bankruptcy::*;
@ -69,6 +71,7 @@ mod flash_loan;
mod group_close;
mod group_create;
mod group_edit;
mod group_withdraw_insurance_fund;
mod health_region;
mod ix_gate_set;
mod perp_cancel_all_orders;
@ -80,6 +83,7 @@ mod perp_consume_events;
mod perp_create_market;
mod perp_deactivate_position;
mod perp_edit_market;
mod perp_force_close_position;
mod perp_liq_base_or_positive_pnl;
mod perp_liq_force_cancel_orders;
mod perp_liq_negative_pnl_or_bankruptcy;

View File

@ -97,6 +97,7 @@ pub fn perp_create_market(
padding3: Default::default(),
settle_pnl_limit_window_size_ts,
reduce_only: 0,
force_close: 0,
padding4: Default::default(),
maint_overall_asset_weight: I80F48::from_num(maint_overall_asset_weight),
init_overall_asset_weight: I80F48::from_num(init_overall_asset_weight),

View File

@ -38,6 +38,7 @@ pub fn perp_edit_market(
reset_stable_price: bool,
positive_pnl_liquidation_fee_opt: Option<f32>,
name_opt: Option<String>,
force_close_opt: Option<bool>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
@ -330,6 +331,19 @@ pub fn perp_edit_market(
require_group_admin = true;
};
if let Some(force_close) = force_close_opt {
if force_close {
require!(perp_market.reduce_only > 0, MangoError::SomeError);
}
msg!(
"Force close: old - {:?}, new - {:?}",
perp_market.force_close,
u8::from(force_close)
);
perp_market.force_close = u8::from(force_close);
require_group_admin = true;
};
// account constraint #1
if require_group_admin {
require!(

View File

@ -0,0 +1,70 @@
use anchor_lang::prelude::*;
use crate::accounts_ix::*;
use crate::accounts_zerocopy::AccountInfoRef;
use crate::error::MangoError;
use crate::logs::{emit_perp_balances, PerpForceClosePositionLog};
use crate::state::*;
use fixed::types::I80F48;
pub fn perp_force_close_position(ctx: Context<PerpForceClosePosition>) -> Result<()> {
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let perp_market_index = perp_market.perp_market_index;
let mut account_a = ctx.accounts.account_a.load_full_mut()?;
let mut account_b = ctx.accounts.account_b.load_full_mut()?;
let account_a_perp_position = account_a.perp_position_mut(perp_market_index)?;
let account_b_perp_position = account_b.perp_position_mut(perp_market_index)?;
require_gt!(
account_a_perp_position.base_position_lots(),
0,
MangoError::SomeError
);
require_gt!(
0,
account_b_perp_position.base_position_lots(),
MangoError::SomeError
);
let base_transfer = account_a_perp_position
.base_position_lots()
.min(account_b_perp_position.base_position_lots().abs())
.max(0);
let now_slot = Clock::get()?.slot;
let oracle_price = perp_market.oracle_price(
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
Some(now_slot),
)?;
let quote_transfer = I80F48::from(base_transfer * perp_market.base_lot_size) * oracle_price;
account_a_perp_position.record_trade(&mut perp_market, -base_transfer, quote_transfer);
account_b_perp_position.record_trade(&mut perp_market, base_transfer, -quote_transfer);
emit_perp_balances(
ctx.accounts.group.key(),
ctx.accounts.account_a.key(),
account_a_perp_position,
&perp_market,
);
emit_perp_balances(
ctx.accounts.group.key(),
ctx.accounts.account_b.key(),
&account_b_perp_position,
&perp_market,
);
emit!(PerpForceClosePositionLog {
mango_group: ctx.accounts.group.key(),
perp_market_index: perp_market.perp_market_index,
account_a: ctx.accounts.account_a.key(),
account_b: ctx.accounts.account_b.key(),
base_transfer: base_transfer,
quote_transfer: quote_transfer.to_bits(),
price: oracle_price.to_bits(),
});
Ok(())
}

View File

@ -58,7 +58,7 @@ pub fn perp_liq_base_or_positive_pnl(
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liqee_health_cache.require_after_phase1_liquidation()?;
if !liqee.check_liquidatable(&liqee_health_cache)? {
if liqee.check_liquidatable(&liqee_health_cache)? != CheckLiquidatable::Liquidatable {
return Ok(());
}
@ -739,11 +739,15 @@ mod tests {
pm.init_overall_asset_weight = I80F48::from_num(overall_weight);
}
{
perp_p(&mut setup.liqee).record_trade(
let p = perp_p(&mut setup.liqee);
p.record_trade(
setup.perp_market.data(),
init_liqee_base,
I80F48::from_num(init_liqee_quote),
);
p.realized_other_pnl_native = p
.unsettled_pnl(setup.perp_market.data(), I80F48::ONE)
.unwrap();
let settle_bank = setup.settle_bank.data();
settle_bank
@ -773,6 +777,15 @@ mod tests {
-(exp_liqee_quote - init_liqee_quote),
0.01
);
// The settle limit taken over matches the quote pos when removing the
// quote gains from giving away base lots
assert_eq_f!(
I80F48::from_num(liqor_perp.settle_pnl_limit_realized_trade),
liqor_perp.quote_position_native.to_num::<f64>()
+ liqor_perp.base_position_lots as f64,
1.1
);
let settle_bank = result.settle_bank.data();
assert_eq_f!(
token_p(&mut result.liqee).native(settle_bank),

View File

@ -11,38 +11,29 @@ pub fn perp_liq_force_cancel_orders(
) -> Result<()> {
let mut account = ctx.accounts.account.load_full_mut()?;
//
// Check liqee health if liquidation is allowed
//
let mut health_cache = {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health_cache =
new_health_cache(&account.borrow(), &retriever).context("create health cache")?;
{
let result = account.check_liquidatable(&health_cache);
if account.fixed.is_operational() {
if !result? {
return Ok(());
}
} else {
// Frozen accounts can always have their orders cancelled
if !result.is_anchor_error_with_code(MangoError::HealthMustBeNegative.into()) {
// Propagate unexpected errors
result?;
}
}
}
health_cache
new_health_cache(&account.borrow(), &retriever).context("create health cache")?
};
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
//
// Early return if if liquidation is not allowed or if market is not in force close
//
let liquidatable = account.check_liquidatable(&health_cache)?;
if account.fixed.is_operational()
&& liquidatable != CheckLiquidatable::Liquidatable
&& !perp_market.is_force_close()
{
return Ok(());
}
//
// Cancel orders
//
{
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let mut book = Orderbook {
bids: ctx.accounts.bids.load_mut()?,
asks: ctx.accounts.asks.load_mut()?,

View File

@ -50,7 +50,7 @@ pub fn perp_liq_negative_pnl_or_bankruptcy(
let liqee_settle_health = liqee_health_cache.perp_settle_health();
liqee_health_cache.require_after_phase2_liquidation()?;
if !liqee.check_liquidatable(&liqee_health_cache)? {
if liqee.check_liquidatable(&liqee_health_cache)? != CheckLiquidatable::Liquidatable {
return Ok(());
}

View File

@ -4,6 +4,7 @@ use anchor_lang::prelude::*;
pub fn serum3_edit_market(
ctx: Context<Serum3EditMarket>,
reduce_only_opt: Option<bool>,
force_close_opt: Option<bool>,
) -> Result<()> {
let mut serum3_market = ctx.accounts.market.load_mut()?;
@ -24,6 +25,19 @@ pub fn serum3_edit_market(
}
};
if let Some(force_close) = force_close_opt {
if force_close {
require!(serum3_market.is_reduce_only(), MangoError::SomeError);
}
msg!(
"Force close: old - {:?}, new - {:?}",
serum3_market.force_close,
u8::from(force_close)
);
serum3_market.force_close = u8::from(force_close);
require_group_admin = true;
};
if require_group_admin {
require!(
group.admin == ctx.accounts.admin.key(),

View File

@ -51,7 +51,7 @@ pub fn serum3_liq_force_cancel_orders(
}
//
// Check liqee health if liquidation is allowed
// Early return if if liquidation is not allowed or if market is not in force close
//
let mut health_cache = {
let mut account = ctx.accounts.account.load_full_mut()?;
@ -61,17 +61,12 @@ pub fn serum3_liq_force_cancel_orders(
new_health_cache(&account.borrow(), &retriever).context("create health cache")?;
{
let result = account.check_liquidatable(&health_cache);
if account.fixed.is_operational() {
if !result? {
return Ok(());
}
} else {
// Frozen accounts can always have their orders cancelled
if !result.is_anchor_error_with_code(MangoError::HealthMustBeNegative.into()) {
// Propagate unexpected errors
result?;
}
let liquidatable = account.check_liquidatable(&health_cache)?;
if account.fixed.is_operational()
&& liquidatable != CheckLiquidatable::Liquidatable
&& !serum_market.is_force_close()
{
return Ok(());
}
}

View File

@ -36,6 +36,7 @@ pub fn serum3_register_market(
base_token_index: base_bank.token_index,
quote_token_index: quote_bank.token_index,
reduce_only: 0,
force_close: 0,
padding1: Default::default(),
name: fill_from_str(&name)?,
serum_program: ctx.accounts.serum_program.key(),

View File

@ -1,7 +1,7 @@
use crate::accounts_ix::*;
use crate::error::*;
use crate::health::*;
use crate::logs::TokenBalanceLog;
use crate::logs::{TokenBalanceLog, TokenForceCloseBorrowsWithTokenLog};
use crate::state::*;
use anchor_lang::prelude::*;
use fixed::types::I80F48;
@ -161,6 +161,19 @@ pub fn token_force_close_borrows_with_token(
borrow_index: liab_bank.borrow_index.to_bits(),
});
emit!(TokenForceCloseBorrowsWithTokenLog {
mango_group: liqee.fixed.group,
liqee: liqee_key,
liqor: liqor_key,
asset_token_index: asset_token_index,
liab_token_index: liab_token_index,
asset_transfer: asset_transfer.to_bits(),
liab_transfer: liab_transfer.to_bits(),
asset_price: asset_oracle_price.to_bits(),
liab_price: liab_oracle_price.to_bits(),
fee_factor: fee_factor.to_bits(),
});
let liqee_health_cache = new_health_cache(&liqee.borrow(), &mut account_retriever)
.context("create liqee health cache")?;
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);

View File

@ -46,7 +46,7 @@ pub fn token_liq_with_token(
let liqee_liq_end_health = liqee_health_cache.health(HealthType::LiquidationEnd);
liqee_health_cache.require_after_phase1_liquidation()?;
if !liqee.check_liquidatable(&liqee_health_cache)? {
if liqee.check_liquidatable(&liqee_health_cache)? != CheckLiquidatable::Liquidatable {
return Ok(());
}

View File

@ -85,6 +85,15 @@ pub mod mango_v4 {
Ok(())
}
pub fn group_withdraw_insurance_fund(
ctx: Context<GroupWithdrawInsuranceFund>,
amount: u64,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::group_withdraw_insurance_fund(ctx, amount)?;
Ok(())
}
pub fn ix_gate_set(ctx: Context<IxGateSet>, ix_gate: u128) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::ix_gate_set(ctx, ix_gate)?;
@ -403,9 +412,10 @@ pub mod mango_v4 {
pub fn serum3_edit_market(
ctx: Context<Serum3EditMarket>,
reduce_only_opt: Option<bool>,
force_close_opt: Option<bool>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::serum3_edit_market(ctx, reduce_only_opt)?;
instructions::serum3_edit_market(ctx, reduce_only_opt, force_close_opt)?;
Ok(())
}
@ -677,6 +687,7 @@ pub mod mango_v4 {
reset_stable_price: bool,
positive_pnl_liquidation_fee_opt: Option<f32>,
name_opt: Option<String>,
force_close_opt: Option<bool>,
) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_edit_market(
@ -710,6 +721,7 @@ pub mod mango_v4 {
reset_stable_price,
positive_pnl_liquidation_fee_opt,
name_opt,
force_close_opt,
)?;
Ok(())
}
@ -909,6 +921,12 @@ pub mod mango_v4 {
Ok(())
}
pub fn perp_force_close_position(ctx: Context<PerpForceClosePosition>) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_force_close_position(ctx)?;
Ok(())
}
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
#[cfg(feature = "enable-gpl")]
instructions::perp_settle_fees(ctx, max_settle_amount)?;

View File

@ -379,3 +379,28 @@ pub struct FilledPerpOrderLog {
pub perp_market_index: u16,
pub seq_num: u64,
}
#[event]
pub struct PerpForceClosePositionLog {
pub mango_group: Pubkey,
pub perp_market_index: u16,
pub account_a: Pubkey,
pub account_b: Pubkey,
pub base_transfer: i64,
pub quote_transfer: i128,
pub price: i128,
}
#[event]
pub struct TokenForceCloseBorrowsWithTokenLog {
pub mango_group: Pubkey,
pub liqor: Pubkey,
pub liqee: Pubkey,
pub asset_token_index: u16,
pub liab_token_index: u16,
pub asset_transfer: i128,
pub liab_transfer: i128,
pub asset_price: i128,
pub liab_price: i128,
pub fee_factor: i128,
}

View File

@ -188,6 +188,8 @@ pub enum IxGate {
TokenWithdraw = 47,
AccountBuybackFeesWithMngo = 48,
TokenForceCloseBorrowsWithToken = 49,
PerpForceClosePosition = 50,
GroupWithdrawInsuranceFund = 51,
// NOTE: Adding new variants requires matching changes in ts and the ix_gate_set instruction.
}

View File

@ -32,6 +32,15 @@ const BORSH_VEC_PADDING_BYTES: usize = 4;
const BORSH_VEC_SIZE_BYTES: usize = 4;
const DEFAULT_MANGO_ACCOUNT_VERSION: u8 = 1;
// Return variants for check_liquidatable method, should be wrapped in a Result
// for a future possiblity of returning any error
#[derive(PartialEq)]
pub enum CheckLiquidatable {
NotLiquidatable,
Liquidatable,
BecameNotLiquidatable,
}
// Mango Account
// This struct definition is only for clients e.g. typescript, so that they can easily use out of the box
// deserialization and not have to do custom deserialization
@ -1028,7 +1037,7 @@ impl<
Ok(())
}
pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result<bool> {
pub fn check_liquidatable(&mut self, health_cache: &HealthCache) -> Result<CheckLiquidatable> {
// Once maint_health falls below 0, we want to start liquidating,
// we want to allow liquidation to continue until init_health is positive,
// to prevent constant oscillation between the two states
@ -1039,17 +1048,17 @@ impl<
.maybe_recover_from_being_liquidated(liq_end_health)
{
msg!("Liqee init_health above zero");
return Ok(false);
return Ok(CheckLiquidatable::BecameNotLiquidatable);
}
} else {
let maint_health = health_cache.health(HealthType::Maint);
require!(
maint_health < I80F48::ZERO,
MangoError::HealthMustBeNegative
);
if maint_health >= I80F48::ZERO {
msg!("Liqee is not liquidatable");
return Ok(CheckLiquidatable::NotLiquidatable);
}
self.fixed_mut().set_being_liquidated(true);
}
Ok(true)
return Ok(CheckLiquidatable::Liquidatable);
}
// writes length of tokens vec at appropriate offset so that borsh can infer the vector length

View File

@ -467,6 +467,20 @@ impl PerpPosition {
}
}
// Bump the realized trade pnl settle limit for a fraction of the stable price value,
// allowing gradual settlement of very high-pnl trades.
let realized_stable_value = I80F48::from(reduced_lots.abs() * perp_market.base_lot_size)
* perp_market.stable_price();
let stable_value_fraction =
I80F48::from_num(perp_market.settle_pnl_limit_factor) * realized_stable_value;
self.increase_realized_trade_pnl_settle_limit(newly_realized_pnl, stable_value_fraction);
}
fn increase_realized_trade_pnl_settle_limit(
&mut self,
newly_realized_pnl: I80F48,
limit: I80F48,
) {
// When realized limit has a different sign from realized pnl, reset it completely
if (self.settle_pnl_limit_realized_trade > 0 && self.realized_trade_pnl_native <= 0)
|| (self.settle_pnl_limit_realized_trade < 0 && self.realized_trade_pnl_native >= 0)
@ -477,25 +491,13 @@ impl PerpPosition {
// Whenever realized pnl increases in magnitude, also increase realized pnl settle limit
// magnitude.
if newly_realized_pnl.signum() == self.realized_trade_pnl_native.signum() {
let realized_stable_value =
I80F48::from(reduced_lots.abs() * perp_market.base_lot_size)
* perp_market.stable_price();
let stable_value_fraction =
I80F48::from_num(perp_market.settle_pnl_limit_factor) * realized_stable_value;
// The realized pnl settle limit change is restricted to actually realized pnl:
// buying and then selling some base lots at the same price shouldn't affect
// the settle limit.
let limit_change = if newly_realized_pnl > 0 {
newly_realized_pnl
.min(stable_value_fraction)
.ceil()
.clamp_to_i64()
newly_realized_pnl.min(limit).ceil().clamp_to_i64()
} else {
newly_realized_pnl
.max(-stable_value_fraction)
.floor()
.clamp_to_i64()
newly_realized_pnl.max(-limit).floor().clamp_to_i64()
};
self.settle_pnl_limit_realized_trade += limit_change;
}
@ -774,7 +776,8 @@ impl PerpPosition {
/// Adds to the quote position and adds a recurring ("realized trade") settle limit
pub fn record_liquidation_pnl_takeover(&mut self, change: I80F48, recurring_limit: I80F48) {
self.change_quote_position(change);
self.realized_trade_pnl_native += recurring_limit;
self.realized_trade_pnl_native += change;
self.increase_realized_trade_pnl_settle_limit(change, recurring_limit);
}
}

View File

@ -138,12 +138,13 @@ impl<'a> Orderbook<'a> {
matched_order_changes.push((best_opposing.handle, new_best_opposing_quantity));
}
let seq_num = event_queue.header.seq_num;
let fill = FillEvent::new(
side,
maker_out,
best_opposing.node.owner_slot,
now_ts,
event_queue.header.seq_num,
seq_num,
best_opposing.node.owner,
best_opposing.node.client_order_id,
market.maker_fee,
@ -160,7 +161,7 @@ impl<'a> Orderbook<'a> {
emit!(FilledPerpOrderLog {
mango_group: market.group.key(),
perp_market_index: market.perp_market_index,
seq_num: event_queue.header.seq_num,
seq_num: seq_num,
});
}
let total_quote_lots_taken = order.max_quote_lots - remaining_quote_lots;

View File

@ -157,8 +157,9 @@ pub struct PerpMarket {
/// If true, users may no longer increase their market exposure. Only actions
/// that reduce their position are still allowed.
pub reduce_only: u8,
pub force_close: u8,
pub padding4: [u8; 7],
pub padding4: [u8; 6],
/// Weights for full perp market health, if positive
pub maint_overall_asset_weight: I80F48,
@ -218,6 +219,10 @@ impl PerpMarket {
self.reduce_only == 1
}
pub fn is_force_close(&self) -> bool {
self.force_close == 1
}
pub fn elligible_for_group_insurance_fund(&self) -> bool {
self.group_insurance_fund == 1
}
@ -478,6 +483,7 @@ impl PerpMarket {
padding3: Default::default(),
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
reduce_only: 0,
force_close: 0,
padding4: Default::default(),
maint_overall_asset_weight: I80F48::ONE,
init_overall_asset_weight: I80F48::ONE,

View File

@ -16,7 +16,8 @@ pub struct Serum3Market {
// ABI: Clients rely on this being at offset 42
pub quote_token_index: TokenIndex,
pub reduce_only: u8,
pub padding1: [u8; 3],
pub force_close: u8,
pub padding1: [u8; 2],
pub name: [u8; 16],
pub serum_program: Pubkey,
pub serum_market_external: Pubkey,
@ -48,6 +49,10 @@ impl Serum3Market {
pub fn is_reduce_only(&self) -> bool {
self.reduce_only == 1
}
pub fn is_force_close(&self) -> bool {
self.force_close == 1
}
}
#[account(zero_copy)]

View File

@ -1,7 +1,7 @@
use super::*;
#[tokio::test]
async fn test_force_close() -> Result<(), TransportError> {
async fn test_force_close_token() -> Result<(), TransportError> {
let test_builder = TestContextBuilder::new();
let context = test_builder.start_default().await;
let solana = &context.solana.clone();
@ -228,3 +228,215 @@ async fn test_force_close() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_force_close_perp() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 1000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
init_base_asset_weight: 0.95,
maint_base_liab_weight: 1.025,
init_base_liab_weight: 1.05,
base_liquidation_fee: 0.012,
maker_fee: -0.0001,
taker_fee: 0.0002,
settle_pnl_limit_factor: -1.0,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::ONE)
};
//
// Place a bid, corresponding ask, and consume event
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
reduce_only: false,
client_order_id: 5,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
reduce_only: false,
client_order_id: 6,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
-99.99,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
99.98,
0.001
));
// Market needs to be in force close
assert!(send_tx(
solana,
PerpForceClosePositionInstruction {
account_a: account_0,
account_b: account_1,
perp_market: perp_market,
},
)
.await
.is_err());
//
// Set force close and force close position and verify that base position is 0
//
send_tx(
solana,
PerpMakeReduceOnly {
admin,
group,
perp_market: perp_market,
reduce_only: true,
force_close: true,
},
)
.await
.unwrap();
// account_a needs to be long, and account_b needs to be short
assert!(send_tx(
solana,
PerpForceClosePositionInstruction {
account_a: account_1,
account_b: account_0,
perp_market: perp_market,
},
)
.await
.is_err());
send_tx(
solana,
PerpForceClosePositionInstruction {
account_a: account_0,
account_b: account_1,
perp_market: perp_market,
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
0.009,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
-0.0199,
0.001
));
Ok(())
}

View File

@ -364,6 +364,8 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> {
group,
admin,
perp_market,
reduce_only: true,
force_close: false,
},
)
.await

View File

@ -3006,6 +3006,7 @@ fn perp_edit_instruction_default() -> mango_v4::instruction::PerpEditMarket {
reset_stable_price: false,
positive_pnl_liquidation_fee_opt: None,
name_opt: None,
force_close_opt: None,
}
}
@ -3092,6 +3093,8 @@ pub struct PerpMakeReduceOnly {
pub group: Pubkey,
pub admin: TestKeypair,
pub perp_market: Pubkey,
pub reduce_only: bool,
pub force_close: bool,
}
#[async_trait::async_trait(?Send)]
@ -3107,7 +3110,8 @@ impl ClientInstruction for PerpMakeReduceOnly {
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let instruction = Self::Instruction {
reduce_only_opt: Some(true),
reduce_only_opt: Some(self.reduce_only),
force_close_opt: Some(self.force_close),
..perp_edit_instruction_default()
};
@ -3617,6 +3621,42 @@ impl ClientInstruction for PerpSettlePnlInstruction {
}
}
pub struct PerpForceClosePositionInstruction {
pub account_a: Pubkey,
pub account_b: Pubkey,
pub perp_market: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpForceClosePositionInstruction {
type Accounts = mango_v4::accounts::PerpForceClosePosition;
type Instruction = mango_v4::instruction::PerpForceClosePosition;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let accounts = Self::Accounts {
group: perp_market.group,
perp_market: self.perp_market,
account_a: self.account_a,
account_b: self.account_b,
oracle: perp_market.oracle,
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![]
}
}
pub struct PerpSettleFeesInstruction {
pub account: Pubkey,
pub perp_market: Pubkey,
@ -3882,7 +3922,9 @@ impl ClientInstruction for BenchmarkInstruction {
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {};
let accounts = Self::Accounts {};
let accounts = Self::Accounts {
dummy: Pubkey::new_unique(),
};
let instruction = make_instruction(program_id, &accounts, &instruction);
(accounts, instruction)

View File

@ -23,7 +23,3 @@ solana --url https://mango.devnet.rpcpool.com program deploy --program-id $PROGR
# publish idl
cargo run -p anchor-cli -- idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS \
--filepath target/idl/mango_v4_no_docs.json $PROGRAM_ID
# build npm package
(cd ./ts/client && tsc)

View File

@ -23,16 +23,14 @@ import { buildVersionedTx } from '../../src/utils';
// https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/serum.json#L70
const DEVNET_SERUM3_MARKETS = new Map([
['SOL/USDC', '82iPEvGiTceyxYpeLK3DhSwga3R5m4Yfyoydd13CukQ9'],
['SOL/USDC', '6xYbSQyhajUqyatJDdkonpj7v41bKeEBWpf7kwRh5X7A'],
]);
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
['SOL', 'So11111111111111111111111111111111111111112'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
]);
const DEVNET_ORACLES = new Map([
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
]);
@ -180,54 +178,32 @@ async function main() {
console.log(
`...edited group, https://explorer.solana.com/tx/${sig}?cluster=devnet`,
);
console.log(`Registering MNGO...`);
const mngoDevnetMint = new PublicKey(DEVNET_MINTS.get('MNGO')!);
const mngoDevnetOracle = new PublicKey(DEVNET_ORACLES.get('MNGO')!);
// register serum market
console.log(`Registering serum3 market...`);
const serumMarketExternalPk = new PublicKey(
DEVNET_SERUM3_MARKETS.get('SOL/USDC')!,
);
try {
sig = await client.tokenRegisterTrustless(
sig = await client.serum3RegisterMarket(
group,
mngoDevnetMint,
mngoDevnetOracle,
2,
'MNGO',
serumMarketExternalPk,
group.getFirstBankByMint(solDevnetMint),
group.getFirstBankByMint(usdcDevnetMint),
0,
'SOL/USDC',
);
await group.reloadAll(client);
const bank = group.getFirstBankByMint(mngoDevnetMint);
const serum3Market = group.getSerum3MarketByExternalMarket(
serumMarketExternalPk,
);
console.log(
`...registered token bank ${bank.publicKey}, https://explorer.solana.com/tx/${sig}?cluster=devnet`,
`...registered serum market ${serum3Market.publicKey}, https://explorer.solana.com/tx/${sig}?cluster=devnet`,
);
} catch (error) {
console.log(error);
}
// DEBUGGING
// log tokens/banks
// group.consoleLogBanks();
// // register serum market
// const serumMarketExternalPk = new PublicKey(
// DEVNET_SERUM3_MARKETS.get('SOL/USDC')!,
// );
// try {
// sig = await client.serum3RegisterMarket(
// group,
// serumMarketExternalPk,
// group.getFirstBankByMint(solDevnetMint),
// group.getFirstBankByMint(usdcDevnetMint),
// 0,
// 'SOL/USDC',
// );
// await group.reloadAll(client);
// const serum3Market = group.getSerum3MarketByExternalMarket(
// serumMarketExternalPk,
// );
// console.log(
// `...registered serum market ${serum3Market.publicKey}, https://explorer.solana.com/tx/${sig}?cluster=devnet`,
// );
// } catch (error) {
// console.log(error);
// }
// register perp market
console.log(`Registering perp market...`);
try {
@ -321,6 +297,25 @@ async function main() {
}
}
// await client.serum3EditMarket(group, 0 as MarketIndex, false, false);
// const perpMarket = group.getPerpMarketByMarketIndex(0 as PerpMarketIndex);
// const params = Builder(NullPerpEditParams)
// .reduceOnly(true)
// .forceClose(true)
// .build();
// await client.perpEditMarket(group, 0 as PerpMarketIndex, params);
// const params = Builder(NullTokenEditParams)
// .reduceOnly(2)
// .forceClose(true)
// .build();
// await client.tokenEdit(
// group,
// group.banksMapByName.get('SOL')![0].mint,
// params,
// );
process.exit();
}

View File

@ -4,7 +4,16 @@ import { expect } from 'chai';
import fs from 'fs';
import { Group } from '../../src/accounts/group';
import { HealthType } from '../../src/accounts/mangoAccount';
import { PerpOrderSide, PerpOrderType } from '../../src/accounts/perp';
import {
PerpMarketIndex,
PerpOrderSide,
PerpOrderType,
} from '../../src/accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../../src/accounts/serum3';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
import { toUiDecimalsForQuote } from '../../src/utils';
@ -22,17 +31,14 @@ const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
]);
export const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
['SOL/USDC', new PublicKey('6xYbSQyhajUqyatJDdkonpj7v41bKeEBWpf7kwRh5X7A')],
]);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
async function main() {
async function main(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',

View File

@ -1,9 +1,4 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
NATIVE_MINT,
TOKEN_PROGRAM_ID,
} from '../../src/utils/spl';
import {
AddressLookupTableProgram,
ComputeBudgetProgram,
@ -30,6 +25,11 @@ import {
} from '../../src/clientIxParamBuilder';
import { MANGO_V4_ID, OPENBOOK_PROGRAM_ID } from '../../src/constants';
import { buildVersionedTx, toNative } from '../../src/utils';
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
NATIVE_MINT,
TOKEN_PROGRAM_ID,
} from '../../src/utils/spl';
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);

View File

@ -322,6 +322,7 @@ async function perpEdit(): Promise<void> {
params.resetStablePrice ?? false,
params.positivePnlLiquidationFee,
params.name,
params.forceClose,
)
.accounts({
group: group.publicKey,

View File

@ -0,0 +1,105 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoAccount } from '../src/accounts/mangoAccount';
import { PerpMarketIndex } from '../src/accounts/perp';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const GROUP_PK =
process.env.GROUP_PK || '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
const PERP_MARKET_INDEX = Number(
process.env.PERP_MARKET_INDEX,
) as PerpMarketIndex;
async function forceClosePerpPositions(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
process.env.KEYPAIR || fs.readFileSync(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],
{
idsSource: 'get-program-accounts',
},
);
const group = await client.getGroup(new PublicKey(GROUP_PK));
const pm = group.getPerpMarketByMarketIndex(PERP_MARKET_INDEX);
if (!pm.reduceOnly) {
throw new Error(`Unexpected reduce only state ${pm.reduceOnly}`);
}
if (!pm.forceClose) {
throw new Error(`Unexpected force close state ${pm.forceClose}`);
}
// Get all mango accounts who have a position in the given market
const mangoAccounts = (await client.getAllMangoAccounts(group)).filter(
(a) =>
a.getPerpPosition(PERP_MARKET_INDEX) !== undefined &&
a.getPerpPositionUi(group, PERP_MARKET_INDEX) !== 0,
);
// Sort descending
mangoAccounts.sort(
(a, b) =>
b.getPerpPositionUi(group, PERP_MARKET_INDEX) -
a.getPerpPositionUi(group, PERP_MARKET_INDEX),
);
let a: MangoAccount;
let b: MangoAccount;
let i = 0,
j = mangoAccounts.length - 1;
// i iterates forward to 2nd last account, and b iterates backward till 2nd account
while (i < mangoAccounts.length - 1 && j > 0) {
if (i === j) {
break;
}
a = mangoAccounts[i];
b = mangoAccounts[j];
// PerpForceClosePosition ix expects a to be long, and b to short
const sig = await client.perpForceClosePosition(
group,
PERP_MARKET_INDEX,
a,
b,
);
console.log(
`PerpForceClosePosition ${a.publicKey} and ${
b.publicKey
} , sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
a = await a.reload(client);
b = await b.reload(client);
// Move to previous account once b's position is completely reduced
if (b.getPerpPositionUi(group, PERP_MARKET_INDEX) === 0) {
console.log(`Fully reduced position for ${b.publicKey}`);
j--;
}
// Move to next account once a's position is completely reduced
if (a.getPerpPositionUi(group, PERP_MARKET_INDEX) === 0) {
console.log(`Fully reduced position for ${a.publicKey}`);
i++;
}
}
}
forceClosePerpPositions();

View File

@ -0,0 +1,79 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import range from 'lodash/range';
import { MarketIndex } from '../src/accounts/serum3';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const GROUP_PK =
process.env.GROUP_PK || '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
const MARKET_INDEX = Number(process.env.MARKET_INDEX) as MarketIndex;
async function forceCloseSerum3Market(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
process.env.KEYPAIR || fs.readFileSync(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],
{
idsSource: 'get-program-accounts',
},
);
const group = await client.getGroup(new PublicKey(GROUP_PK));
const serum3Market = group.serum3MarketsMapByMarketIndex.get(MARKET_INDEX)!;
if (!serum3Market.reduceOnly) {
throw new Error(`Unexpected reduce only state ${serum3Market.reduceOnly}`);
}
if (!serum3Market.forceClose) {
throw new Error(`Unexpected force close state ${serum3Market.forceClose}`);
}
// Get all mango accounts who have a serum oo account for the given market
const mangoAccounts = (await client.getAllMangoAccounts(group, true)).filter(
(a) => a.serum3OosMapByMarketIndex.get(MARKET_INDEX) !== undefined,
);
for (let a of mangoAccounts) {
// Cancel all orders and confirm that all have been cancelled
for (const _ of range(0, 10)) {
console.log(a.getSerum3OoAccount(MARKET_INDEX).freeSlotBits.zeroBits());
const sig = await client.serum3LiqForceCancelOrders(
group,
a,
serum3Market.serumMarketExternal,
10,
);
console.log(
` serum3LiqForceCancelOrders for ${
a.publicKey
}, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
a = await a.reload(client);
if (a.getSerum3OoAccount(MARKET_INDEX).freeSlotBits.zeroBits() === 0) {
break;
}
}
}
}
forceCloseSerum3Market();

View File

@ -0,0 +1,151 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { TokenIndex } from '../src/accounts/bank';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
import {
fetchJupiterTransaction,
fetchRoutes,
prepareMangoRouterInstructions,
} from '../src/router';
import { toNative, toUiDecimals } from '../src/utils';
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK;
const TOKEN_INDEX = Number(process.env.TOKEN_INDEX) as TokenIndex;
const MAX_LIAB_TRANSFER = Number(process.env.MAX_LIAB_TRANSFER);
async function forceCloseTokenBorrows(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
process.env.KEYPAIR || fs.readFileSync(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],
{
idsSource: 'get-program-accounts',
},
);
let liqor = await client.getMangoAccount(new PublicKey(MANGO_ACCOUNT_PK!));
const group = await client.getGroup(liqor.group);
const forceCloseTokenBank = group.getFirstBankByTokenIndex(TOKEN_INDEX);
if (forceCloseTokenBank.reduceOnly != 2) {
throw new Error(
`Unexpected reduce only state ${forceCloseTokenBank.reduceOnly}`,
);
}
if (!forceCloseTokenBank.forceClose) {
throw new Error(
`Unexpected force close state ${forceCloseTokenBank.forceClose}`,
);
}
const usdcBank = group.getFirstBankByTokenIndex(0 as TokenIndex);
// Get all mango accounts with borrows for given token
const mangoAccountsWithBorrows = (
await client.getAllMangoAccounts(group)
).filter((a) => a.getTokenBalanceUi(forceCloseTokenBank) < 0);
console.log(`${liqor.toString(group, true)}`);
for (const liqee of mangoAccountsWithBorrows) {
liqor = await liqor.reload(client);
// Liqor can only liquidate borrow using deposits, since borrows are in reduce only
// Swap usdc worth token borrow (sub existing position), account for slippage using liquidation fee
// MAX_LIAB_TRANSFER guards against trying to swap to a very large amount
const amount =
Math.min(
liqee.getTokenBorrowsUi(forceCloseTokenBank) -
liqor.getTokenBalanceUi(forceCloseTokenBank),
MAX_LIAB_TRANSFER,
) *
forceCloseTokenBank.uiPrice *
(1 + forceCloseTokenBank.liquidationFee.toNumber());
console.log(
`liqor balance ${liqor.getTokenBalanceUi(
forceCloseTokenBank,
)}, liqee balance ${liqee.getTokenBalanceUi(
forceCloseTokenBank,
)}, liqor will swap further amount of $${toUiDecimals(
amount,
usdcBank.mintDecimals,
)} to ${forceCloseTokenBank.name}`,
);
const amountBn = toNative(
Math.min(amount, 99999999999), // Jupiter API can't handle amounts larger than 99999999999
usdcBank.mintDecimals,
);
const { bestRoute } = await fetchRoutes(
usdcBank.mint,
forceCloseTokenBank.mint,
amountBn.toString(),
forceCloseTokenBank.liquidationFee.toNumber() * 100,
'ExactIn',
'0',
liqor.owner,
);
if (!bestRoute) {
await new Promise((r) => setTimeout(r, 500));
continue;
}
const [ixs, alts] =
bestRoute.routerName === 'Mango'
? await prepareMangoRouterInstructions(
bestRoute,
usdcBank.mint,
forceCloseTokenBank.mint,
user.publicKey,
)
: await fetchJupiterTransaction(
client.connection,
bestRoute,
user.publicKey,
0,
usdcBank.mint,
forceCloseTokenBank.mint,
);
const sig = await client.marginTrade({
group: group,
mangoAccount: liqor,
inputMintPk: usdcBank.mint,
amountIn: amount,
outputMintPk: forceCloseTokenBank.mint,
userDefinedInstructions: ixs,
userDefinedAlts: alts,
flashLoanType: { swap: {} },
});
console.log(
` - marginTrade, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
await client.tokenForceCloseBorrowsWithToken(
group,
liqor,
liqee,
usdcBank.tokenIndex,
forceCloseTokenBank.tokenIndex,
);
}
}
forceCloseTokenBorrows();

View File

@ -0,0 +1,211 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import chunk from 'lodash/chunk';
import range from 'lodash/range';
import { Group } from '../../src/accounts/group';
import { FillEvent, OutEvent, PerpEventQueue } from '../../src/accounts/perp';
import { MangoClient } from '../../src/client';
import { MANGO_V4_ID } from '../../src/constants';
import { sendTransaction } from '../../src/utils/rpc';
// Env vars
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
const INTERVAL_UPDATE_BANKS = Number(process.env.INTERVAL_UPDATE_BANKS || 60);
const INTERVAL_CONSUME_EVENTS = Number(
process.env.INTERVAL_CONSUME_EVENTS || 5,
);
const INTERVAL_UPDATE_FUNDING = Number(
process.env.INTERVAL_UPDATE_FUNDING || 5,
);
const INTERVAL_CHECK_NEW_LISTINGS_AND_ABORT = Number(
process.env.INTERVAL_CHECK_NEW_LISTINGS_AND_ABORT || 120,
);
async function updateBanks(client: MangoClient, group: Group): Promise<void> {
console.log('Starting updateBanks loop');
// eslint-disable-next-line no-constant-condition
while (true) {
const tokenIndices = Array.from(group.banksMapByTokenIndex.keys());
const tokenIndicesByChunks = chunk(tokenIndices, 10);
tokenIndicesByChunks.map(async (tokenIndices) => {
const ixs = await Promise.all(
tokenIndices.map((ti) =>
client.tokenUpdateIndexAndRateIx(
group,
group.getFirstBankByTokenIndex(ti).mint,
),
),
);
try {
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
ixs,
group.addressLookupTablesList,
{ prioritizationFee: true },
);
console.log(
` - Token update index and rate success, tokenIndices - ${tokenIndices}, sig https://explorer.solana.com/tx/${sig}`,
);
} catch (e) {
console.log(
` - Token update index and rate error, tokenIndices - ${tokenIndices}, e - ${e}`,
);
}
});
await new Promise((r) => setTimeout(r, INTERVAL_UPDATE_BANKS * 1000));
}
}
async function consumeEvents(client: MangoClient, group: Group): Promise<void> {
console.log('Starting consumeEvents loop');
// eslint-disable-next-line no-constant-condition
while (true) {
const perpMarketIndices = Array.from(
group.perpMarketsMapByMarketIndex.keys(),
);
for (const perpMarketIndex of perpMarketIndices) {
for (const unused of range(0, 10)) {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const eq = await perpMarket.loadEventQueue(client);
const events = eq.getUnconsumedEvents().slice(0, 10);
const accounts: Set<PublicKey> = new Set();
for (const event of events) {
if (event.eventType === PerpEventQueue.FILL_EVENT_TYPE) {
accounts.add((event as FillEvent).maker);
accounts.add((event as FillEvent).taker);
} else if (event.eventType === PerpEventQueue.OUT_EVENT_TYPE) {
accounts.add((event as OutEvent).owner);
} else if (event.eventType === PerpEventQueue.LIQUIDATE_EVENT_TYPE) {
// pass
}
}
try {
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
[
await client.perpConsumeEventsIx(
group,
perpMarketIndex,
Array.from(accounts),
10,
),
],
group.addressLookupTablesList,
{ prioritizationFee: true },
);
console.log(
` - Consume events success, perpMarketIndex - ${perpMarketIndex}, sig https://explorer.solana.com/tx/${sig}`,
);
} catch (e) {
console.log(
` - Consume events error, perpMarketIndex - ${perpMarketIndex}, e - ${e}`,
);
}
}
}
await new Promise((r) => setTimeout(r, INTERVAL_CONSUME_EVENTS * 1000));
}
}
async function updateFunding(client: MangoClient, group: Group): Promise<void> {
console.log('Starting updateFunding loop');
// eslint-disable-next-line no-constant-condition
while (true) {
const perpMarketIndices = Array.from(
group.perpMarketsMapByMarketIndex.keys(),
);
for (const perpMarketIndex of perpMarketIndices) {
try {
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
[
await client.perpUpdateFundingIx(
group,
group.getPerpMarketByMarketIndex(perpMarketIndex),
),
],
group.addressLookupTablesList,
{ prioritizationFee: true },
);
console.log(
` - Update funding success, perpMarketIndex - ${perpMarketIndex}, sig https://explorer.solana.com/tx/${sig}`,
);
} catch (e) {
console.log(
` - Update funding error, perpMarketIndex - ${perpMarketIndex}, e - ${e}`,
);
}
}
await new Promise((r) => setTimeout(r, INTERVAL_UPDATE_FUNDING * 1000));
}
}
async function checkNewListingsAndAbort(
client: MangoClient,
group: Group,
): Promise<void> {
console.log('Starting checkNewListingsAndAbort loop');
// eslint-disable-next-line no-constant-condition
while (true) {
const freshlyFetchedGroup = await client.getGroup(group.publicKey);
if (
freshlyFetchedGroup.banksMapByTokenIndex.size !=
group.banksMapByTokenIndex.size ||
freshlyFetchedGroup.perpMarketsMapByMarketIndex.size !=
group.perpMarketsMapByMarketIndex.size
) {
process.exit();
}
await new Promise((r) =>
setTimeout(r, INTERVAL_CHECK_NEW_LISTINGS_AND_ABORT * 1000),
);
}
}
async function keeper(): Promise<void> {
// Load client
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
process.env.KEYPAIR || fs.readFileSync(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],
{
idsSource: 'get-program-accounts',
},
);
const mangoAccount = await client.getMangoAccount(
new PublicKey(MANGO_ACCOUNT_PK),
);
const group = await client.getGroup(mangoAccount.group);
await group.reloadAll(client);
updateBanks(client, group);
consumeEvents(client, group);
updateFunding(client, group);
checkNewListingsAndAbort(client, group);
}
keeper();

View File

@ -0,0 +1,74 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { HealthType, MangoAccount } from '../src/accounts/mangoAccount';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
import { toUiDecimalsForQuote } from '../src/utils';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const SOME_KEYPAIR =
process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const GROUP_PK = '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
async function main(): Promise<void> {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const someKeypair = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(SOME_KEYPAIR!, 'utf-8'))),
);
const someWallet = new Wallet(someKeypair);
const someProvider = new AnchorProvider(connection, someWallet, options);
const client = MangoClient.connect(
someProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{
idsSource: 'api',
},
);
const group = await client.getGroup(new PublicKey(GROUP_PK));
const mangoAccountsWithHealth = (
await client.getAllMangoAccounts(group, true)
)
.map((a: MangoAccount) => {
return {
account: a,
healthRatio: a.getHealthRatioUi(group, HealthType.maint),
equity: toUiDecimalsForQuote(a.getEquity(group)),
};
})
.filter((a) => a.equity > 1000)
.filter((a) => a.healthRatio < 50)
.sort((a, b) => a.healthRatio - b.healthRatio);
console.log(
`${'Owner'.padStart(45)}, ${'Account'.padStart(
45,
)}, ${'Health Ratio'.padStart(10)}, ${'Equity'.padStart(10)}`,
);
for (const obj of mangoAccountsWithHealth) {
console.log(
`${obj.account.owner.toBase58().padStart(45)} ${obj.account.publicKey
.toBase58()
.padStart(45)}: ${obj.healthRatio
.toFixed(2)
.padStart(8)} %, ${obj.equity.toLocaleString().padStart(10)} $`,
);
}
process.exit();
}
try {
main();
} catch (error) {
console.log(error);
}

View File

@ -0,0 +1,18 @@
This directory contains a sample market maker (`market-maker.ts`) in typescript, which can be run using ts-node.
The environment variables required are
- `MANGO_ACCOUNT_PK` - public key of the mango account
- `KEYPAIR` - private key of the owner of the mango account
- `MB_CLUSTER_URL` - RPC cluster url
Notes:
- Quoting is based off of kraken
- see default.json for quoting rules
Future:
- Hedging perp positions on mango-v4 spot
- Observing fills and reacting earlier
- Quoting off of binance

View File

@ -0,0 +1,95 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fetch from 'node-fetch';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
import { toNative, toUiDecimalsForQuote } from '../src/utils';
const { MB_CLUSTER_URL } = process.env;
const GROUP_PK = '78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX';
async function buildClient(): Promise<MangoClient> {
const clientKeypair = new Keypair();
const options = AnchorProvider.defaultOptions();
const connection = new Connection(MB_CLUSTER_URL!, options);
const clientWallet = new Wallet(clientKeypair);
const clientProvider = new AnchorProvider(connection, clientWallet, options);
return await MangoClient.connect(
clientProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{
idsSource: 'get-program-accounts',
},
);
}
async function computePriceImpact(
amount: string,
inputMint: string,
outputMint: string,
): Promise<{ outAmount: number; priceImpactPct: number }> {
const url = `https://quote-api.jup.ag/v4/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amount}&swapMode=ExactIn&slippageBps=10000&onlyDirectRoutes=false&asLegacyTransaction=false`;
const response = await fetch(url);
let res = await response.json();
res = res.data[0];
return {
outAmount: parseFloat(res.outAmount),
priceImpactPct: parseFloat(res.priceImpactPct),
};
}
async function main(): Promise<void> {
const client = await buildClient();
const group = await client.getGroup(new PublicKey(GROUP_PK));
await group.reloadAll(client);
console.log(
`${'COIN'.padStart(20)}, ${'Scale'.padStart(8)}, ${'Liq Fee'.padStart(
6,
)}, ${'$->coin'.padStart(6)}, ${'coin-$'.padStart(6)}`,
);
for (const bank of Array.from(group.banksMapByMint.values())) {
const usdcMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const pi1 = await computePriceImpact(
bank[0].depositWeightScaleStartQuote.toString(),
usdcMint,
bank[0].mint.toBase58(),
);
const inAmount = toNative(
Math.min(
Math.floor(
toUiDecimalsForQuote(bank[0].depositWeightScaleStartQuote) /
bank[0].uiPrice,
),
99999999999,
),
bank[0].mintDecimals,
);
const pi2 = await computePriceImpact(
inAmount.toString(),
bank[0].mint.toBase58(),
usdcMint,
);
console.log(
`${bank[0].name.padStart(20)}, ${(
'$' +
toUiDecimalsForQuote(bank[0].depositWeightScaleStartQuote).toString()
).padStart(8)}, ${(bank[0].liquidationFee.toNumber() * 100)
.toFixed(3)
.padStart(6)}%, ${(pi1.priceImpactPct * 100).toFixed(2)}%, ${(
pi2.priceImpactPct * 100
).toFixed(2)}%`,
);
}
}
main();

View File

@ -0,0 +1,197 @@
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
import {
Cluster,
Connection,
Keypair,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import { BN } from 'bn.js';
import fs from 'fs';
import {
MarketIndex,
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../src/accounts/serum3';
import { MangoClient } from '../src/client';
import { MANGO_V4_ID } from '../src/constants';
import { sendTransaction } from '../src/utils/rpc';
// Env vars
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
export interface OrderbookL2 {
bids: number[][];
asks: number[][];
}
async function rebalancer(): Promise<void> {
// Load client
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
process.env.KEYPAIR || fs.readFileSync(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],
{
idsSource: 'get-program-accounts',
},
);
// Load mango account
let mangoAccount = await client.getMangoAccount(
new PublicKey(MANGO_ACCOUNT_PK),
true,
);
console.log(
`MangoAccount ${mangoAccount.publicKey} for user ${user.publicKey} ${
mangoAccount.isDelegate(client) ? 'via delegate ' + user.publicKey : ''
}`,
);
await mangoAccount.reload(client);
// Load group
const group = await client.getGroup(mangoAccount.group);
await group.reloadAll(client);
const usdcBank = group.getFirstBankByMint(
new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
);
// Loop indefinitely
// eslint-disable-next-line no-constant-condition
while (true) {
await group.reloadAll(client);
mangoAccount = await mangoAccount.reload(client);
// console.log(mangoAccount.toString(group, true));
for (const tp of mangoAccount
.tokensActive()
.filter((tp) => tp.tokenIndex !== usdcBank.tokenIndex)) {
const baseBank = group.getFirstBankByTokenIndex(tp.tokenIndex);
const tokenBalance = tp.balanceUi(baseBank);
const serum3Markets = Array.from(
group.serum3MarketsMapByMarketIndex.values(),
)
// Find correct $TOKEN/$USDC market
.filter(
(serum3Market) =>
serum3Market.baseTokenIndex === tp.tokenIndex &&
serum3Market.quoteTokenIndex === usdcBank.tokenIndex,
);
if (!serum3Markets) {
continue;
}
const serum3Market = serum3Markets[0];
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
serum3Market.serumMarketExternal.toBase58(),
)!;
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(
Math.abs(tokenBalance),
);
// Skip if quantity is too small
if (maxBaseQuantity.eq(new BN(0))) {
// console.log(
// ` - Not rebalancing ${tokenBalance} $${baseBank.name}, quantity too small`,
// );
continue;
}
console.log(`- Rebalancing ${tokenBalance} $${baseBank.name}`);
// if balance is negative we want to bid at a higher price
// if balance is positive we want to ask at a lower price
const price =
baseBank.uiPrice *
(1 + (tokenBalance > 0 ? -1 : 1) * baseBank.liquidationFee.toNumber());
try {
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
[
...(await client.serum3PlaceOrderIx(
group,
mangoAccount,
serum3Market.serumMarketExternal,
tokenBalance > 0 ? Serum3Side.ask : Serum3Side.bid,
price,
Math.abs(tokenBalance),
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.immediateOrCancel,
new Date().valueOf(),
10,
)),
await client.serum3CancelAllOrdersIx(
group,
mangoAccount,
serum3Market.serumMarketExternal,
),
await client.serum3SettleFundsV2Ix(
group,
mangoAccount,
serum3Market.serumMarketExternal,
),
],
group.addressLookupTablesList,
{ prioritizationFee: true },
);
console.log(` -- sig https://explorer.solana.com/tx/${sig}`);
} catch (e) {
console.log(e);
}
}
mangoAccount = await mangoAccount.reload(client);
const ixs: TransactionInstruction[] = [];
for (const serum3OoMarketIndex of Array.from(
mangoAccount.serum3OosMapByMarketIndex.keys(),
)) {
const serum3ExternalPk = group.serum3MarketsMapByMarketIndex.get(
serum3OoMarketIndex as MarketIndex,
)!.serumMarketExternal;
// 12502 cu per market
ixs.push(
await client.serum3CloseOpenOrdersIx(
group,
mangoAccount,
serum3ExternalPk,
),
);
}
if (ixs.length) {
try {
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
ixs,
group.addressLookupTablesList,
{ prioritizationFee: true },
);
console.log(
` - closed all serum3 oo accounts, sig https://explorer.solana.com/tx/${sig}`,
);
} catch (e) {
console.log(e);
}
}
// console.log(`${new Date().toUTCString()} sleeping for 1s`);
await new Promise((r) => setTimeout(r, 1000));
}
}
rebalancer();

View File

@ -107,6 +107,7 @@ export class PerpMarket {
settlePnlLimitFactor: number;
settlePnlLimitWindowSizeTs: BN;
reduceOnly: number;
forceClose: number;
maintOverallAssetWeight: I80F48Dto;
initOverallAssetWeight: I80F48Dto;
positivePnlLiquidationFee: I80F48Dto;
@ -153,6 +154,7 @@ export class PerpMarket {
obj.settlePnlLimitFactor,
obj.settlePnlLimitWindowSizeTs,
obj.reduceOnly == 1,
obj.forceClose == 1,
obj.maintOverallAssetWeight,
obj.initOverallAssetWeight,
obj.positivePnlLiquidationFee,
@ -200,6 +202,7 @@ export class PerpMarket {
public settlePnlLimitFactor: number,
public settlePnlLimitWindowSizeTs: BN,
public reduceOnly: boolean,
public forceClose: boolean,
maintOverallAssetWeight: I80F48Dto,
initOverallAssetWeight: I80F48Dto,
positivePnlLiquidationFee: I80F48Dto,

View File

@ -25,6 +25,7 @@ export class Serum3Market {
marketIndex: number;
registrationTime: BN;
reduceOnly: number;
forceClose: number;
},
): Serum3Market {
return new Serum3Market(
@ -38,6 +39,7 @@ export class Serum3Market {
obj.marketIndex as MarketIndex,
obj.registrationTime,
obj.reduceOnly == 1,
obj.forceClose == 1,
);
}
@ -52,6 +54,7 @@ export class Serum3Market {
public marketIndex: MarketIndex,
public registrationTime: BN,
public reduceOnly: boolean,
public forceClose: boolean,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
}
@ -140,6 +143,40 @@ export class Serum3Market {
);
}
public async computePriceForMarketOrderOfSize(
client: MangoClient,
group: Group,
size: number,
side: 'buy' | 'sell',
): Promise<number> {
const ob =
side == 'buy'
? await this.loadBids(client, group)
: await this.loadAsks(client, group);
let acc = 0;
let selectedOrder;
const orderSize = size;
for (const order of ob.getL2(size * 2 /* TODO Fix random constant */)) {
acc += order[1];
if (acc >= orderSize) {
selectedOrder = order;
break;
}
}
if (!selectedOrder) {
throw new Error(
'Unable to place market order for this order size. Please retry.',
);
}
if (side === 'buy') {
return selectedOrder[0] * 1.05 /* TODO Fix random constant */;
} else {
return selectedOrder[0] * 0.95 /* TODO Fix random constant */;
}
}
public async logOb(client: MangoClient, group: Group): Promise<string> {
let res = ``;
res += ` ${this.name} OrderBook`;

View File

@ -420,6 +420,53 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async tokenForceCloseBorrowsWithToken(
group: Group,
liqor: MangoAccount,
liqee: MangoAccount,
assetTokenIndex: TokenIndex,
liabTokenIndex: TokenIndex,
maxLiabTransfer?: number,
): Promise<string> {
const assetBank = group.getFirstBankByTokenIndex(assetTokenIndex);
const liabBank = group.getFirstBankByTokenIndex(liabTokenIndex);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
group,
[liqor, liqee],
[assetBank, liabBank],
[],
);
const parsedHealthAccounts = healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable:
pk.equals(assetBank.publicKey) || pk.equals(liabBank.publicKey)
? true
: false,
isSigner: false,
} as AccountMeta),
);
const ix = await this.program.methods
.tokenForceCloseBorrowsWithToken(
assetTokenIndex,
liabTokenIndex,
maxLiabTransfer
? toNative(maxLiabTransfer, liabBank.mintDecimals)
: U64_MAX_BN,
)
.accounts({
group: group.publicKey,
liqor: liqor.publicKey,
liqorOwner: (this.program.provider as AnchorProvider).wallet.publicKey,
liqee: liqee.publicKey,
})
.remainingAccounts(parsedHealthAccounts)
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async tokenDeregister(
group: Group,
mintPk: PublicKey,
@ -1247,6 +1294,25 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async serum3EditMarket(
group: Group,
serum3MarketIndex: MarketIndex,
reduceOnly: boolean | null,
forceClose: boolean | null,
): Promise<TransactionSignature> {
const serum3Market =
group.serum3MarketsMapByMarketIndex.get(serum3MarketIndex);
const ix = await this.program.methods
.serum3EditMarket(reduceOnly, forceClose)
.accounts({
group: group.publicKey,
admin: (this.program.provider as AnchorProvider).wallet.publicKey,
market: serum3Market?.publicKey,
})
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async serum3deregisterMarket(
group: Group,
externalMarketPk: PublicKey,
@ -1377,10 +1443,6 @@ export class MangoClient {
externalMarketPk.toBase58(),
)!;
const openOrders = mangoAccount.serum3.find(
(account) => account.marketIndex === serum3Market.marketIndex,
)?.openOrders;
return await this.program.methods
.serum3CloseOpenOrders()
.accounts({
@ -1389,7 +1451,10 @@ export class MangoClient {
serumMarket: serum3Market.publicKey,
serumProgram: serum3Market.serumProgram,
serumMarketExternal: serum3Market.serumMarketExternal,
openOrders,
openOrders: await serum3Market.findOoPda(
this.programId,
mangoAccount.publicKey,
),
solDestination: (this.program.provider as AnchorProvider).wallet
.publicKey,
})
@ -1417,6 +1482,71 @@ export class MangoClient {
);
}
public async serum3LiqForceCancelOrders(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
limit?: number,
): Promise<string> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal = group.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
)!;
const openOrders = await serum3Market.findOoPda(
this.programId,
mangoAccount.publicKey,
);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
group,
[mangoAccount],
[],
[],
[[serum3Market, openOrders]],
);
const ix = await this.program.methods
.serum3LiqForceCancelOrders(limit ?? 10)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
openOrders,
serumMarket: serum3Market.publicKey,
serumProgram: OPENBOOK_PROGRAM_ID[this.cluster],
serumMarketExternal: serum3Market.serumMarketExternal,
marketBids: serum3MarketExternal.bidsAddress,
marketAsks: serum3MarketExternal.asksAddress,
marketEventQueue: serum3MarketExternal.decoded.eventQueue,
marketBaseVault: serum3MarketExternal.decoded.baseVault,
marketQuoteVault: serum3MarketExternal.decoded.quoteVault,
marketVaultSigner: await generateSerum3MarketExternalVaultSignerAddress(
this.cluster,
serum3Market,
serum3MarketExternal,
),
quoteBank: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.publicKey,
quoteVault: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.vault,
baseBank: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.publicKey,
baseVault: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.vault,
})
.remainingAccounts(
healthRemainingAccounts.map(
(pk) =>
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
),
)
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async serum3PlaceOrderIx(
group: Group,
mangoAccount: MangoAccount,
@ -1563,24 +1693,24 @@ export class MangoClient {
clientOrderId,
limit,
);
const settleIx = await this.serum3SettleFundsIx(
group,
mangoAccount,
externalMarketPk,
);
return await this.sendAndConfirmTransactionForGroup(group, [
...placeOrderIxes,
settleIx,
]);
const ixs = [...placeOrderIxes, settleIx];
return await this.sendAndConfirmTransactionForGroup(group, ixs);
}
public async serum3CancelAllOrders(
public async serum3CancelAllOrdersIx(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
limit?: number,
): Promise<TransactionSignature> {
): Promise<TransactionInstruction> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
@ -1589,14 +1719,16 @@ export class MangoClient {
externalMarketPk.toBase58(),
)!;
const ix = await this.program.methods
return await this.program.methods
.serum3CancelAllOrders(limit ? limit : 10)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
openOrders: mangoAccount.getSerum3Account(serum3Market.marketIndex)
?.openOrders,
openOrders: await serum3Market.findOoPda(
this.programId,
mangoAccount.publicKey,
),
serumMarket: serum3Market.publicKey,
serumProgram: OPENBOOK_PROGRAM_ID[this.cluster],
serumMarketExternal: serum3Market.serumMarketExternal,
@ -1605,8 +1737,22 @@ export class MangoClient {
marketEventQueue: serum3MarketExternal.decoded.eventQueue,
})
.instruction();
}
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
public async serum3CancelAllOrders(
group: Group,
mangoAccount: MangoAccount,
externalMarketPk: PublicKey,
limit?: number,
): Promise<TransactionSignature> {
return await this.sendAndConfirmTransactionForGroup(group, [
await this.serum3CancelAllOrdersIx(
group,
mangoAccount,
externalMarketPk,
limit,
),
]);
}
public async serum3SettleFundsIx(
@ -1961,6 +2107,7 @@ export class MangoClient {
params.resetStablePrice ?? false,
params.positivePnlLiquidationFee,
params.name,
params.forceClose,
)
.accounts({
group: group.publicKey,
@ -1972,6 +2119,27 @@ export class MangoClient {
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async perpForceClosePosition(
group: Group,
perpMarketIndex: PerpMarketIndex,
accountA: MangoAccount,
accountB: MangoAccount,
): Promise<string> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const ix = await this.program.methods
.perpForceClosePosition()
.accounts({
group: group.publicKey,
perpMarket: perpMarket.publicKey,
accountA: accountA.publicKey,
accountB: accountB.publicKey,
oracle: perpMarket.oracle,
})
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async perpCloseMarket(
group: Group,
perpMarketIndex: PerpMarketIndex,
@ -2447,8 +2615,19 @@ export class MangoClient {
accounts: PublicKey[],
limit: number,
): Promise<TransactionSignature> {
return await this.sendAndConfirmTransactionForGroup(group, [
await this.perpConsumeEventsIx(group, perpMarketIndex, accounts, limit),
]);
}
public async perpConsumeEventsIx(
group: Group,
perpMarketIndex: PerpMarketIndex,
accounts: PublicKey[],
limit: number,
): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const ix = await this.program.methods
return await this.program.methods
.perpConsumeEvents(new BN(limit))
.accounts({
group: group.publicKey,
@ -2462,7 +2641,6 @@ export class MangoClient {
),
)
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
public async perpConsumeAllEvents(
@ -2684,14 +2862,23 @@ export class MangoClient {
);
}
public async updateIndexAndRate(
public async tokenUpdateIndexAndRate(
group: Group,
mintPk: PublicKey,
): Promise<TransactionSignature> {
return await this.sendAndConfirmTransactionForGroup(group, [
await this.tokenUpdateIndexAndRateIx(group, mintPk),
]);
}
public async tokenUpdateIndexAndRateIx(
group: Group,
mintPk: PublicKey,
): Promise<TransactionInstruction> {
const bank = group.getFirstBankByMint(mintPk);
const mintInfo = group.mintInfosMapByMint.get(mintPk.toString())!;
const ix = await this.program.methods
return await this.program.methods
.tokenUpdateIndexAndRate()
.accounts({
group: group.publicKey,
@ -2707,7 +2894,6 @@ export class MangoClient {
} as AccountMeta,
])
.instruction();
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
}
/// liquidations

View File

@ -86,6 +86,7 @@ export interface PerpEditParams {
resetStablePrice: boolean | null;
positivePnlLiquidationFee: number | null;
name: string | null;
forceClose: boolean | null;
}
export const NullPerpEditParams: PerpEditParams = {
@ -118,6 +119,7 @@ export const NullPerpEditParams: PerpEditParams = {
resetStablePrice: null,
positivePnlLiquidationFee: null,
name: null,
forceClose: null,
};
// Use with TrueIxGateParams and buildIxGate
@ -175,6 +177,8 @@ export interface IxGateParams {
TokenWithdraw: boolean;
AccountBuybackFeesWithMngo: boolean;
TokenForceCloseBorrowsWithToken: boolean;
PerpForceClosePosition: boolean;
GroupWithdrawInsuranceFund: boolean;
}
// Default with all ixs enabled, use with buildIxGate
@ -232,6 +236,8 @@ export const TrueIxGateParams: IxGateParams = {
TokenWithdraw: true,
AccountBuybackFeesWithMngo: true,
TokenForceCloseBorrowsWithToken: true,
PerpForceClosePosition: true,
GroupWithdrawInsuranceFund: true,
};
// build ix gate e.g. buildIxGate(Builder(TrueIxGateParams).TokenDeposit(false).build()).toNumber(),
@ -299,6 +305,8 @@ export function buildIxGate(p: IxGateParams): BN {
toggleIx(ixGate, p, 'TokenWithdraw', 47);
toggleIx(ixGate, p, 'AccountBuybackFeesWithMngo', 48);
toggleIx(ixGate, p, 'TokenForceCloseBorrowsWithToken', 49);
toggleIx(ixGate, p, 'PerpForceClosePosition', 50);
toggleIx(ixGate, p, 'GroupWithdrawInsuranceFund', 51);
return ixGate;
}

View File

@ -20,6 +20,7 @@ export {
} from './clientIxParamBuilder';
export * from './constants';
export * from './numbers/I80F48';
export * from './utils';
export * from './router';
export * from './types';
export { Group, OracleProvider, StubOracle, MangoClient, MANGO_V4_ID };
export * from './utils';
export { Group, MANGO_V4_ID, MangoClient, OracleProvider, StubOracle };

View File

@ -1,5 +1,5 @@
export type MangoV4 = {
"version": "0.13.0",
"version": "0.14.0",
"name": "mango_v4",
"instructions": [
{
@ -177,6 +177,42 @@ export type MangoV4 = {
}
]
},
{
"name": "groupWithdrawInsuranceFund",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "admin",
"isMut": false,
"isSigner": true
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false
},
{
"name": "destination",
"isMut": true,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "amount",
"type": "u64"
}
]
},
{
"name": "ixGateSet",
"accounts": [
@ -1743,6 +1779,12 @@ export type MangoV4 = {
"type": {
"option": "bool"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -3092,6 +3134,12 @@ export type MangoV4 = {
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -3635,6 +3683,37 @@ export type MangoV4 = {
],
"args": []
},
{
"name": "perpForceClosePosition",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "accountA",
"isMut": true,
"isSigner": false
},
{
"name": "accountB",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpSettleFees",
"accounts": [
@ -3924,7 +4003,13 @@ export type MangoV4 = {
"benchmark",
""
],
"accounts": [],
"accounts": [
{
"name": "dummy",
"isMut": false,
"isSigner": false
}
],
"args": []
}
],
@ -5048,12 +5133,16 @@ export type MangoV4 = {
],
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding4",
"type": {
"array": [
"u8",
7
6
]
}
},
@ -5111,12 +5200,16 @@ export type MangoV4 = {
"name": "reduceOnly",
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding1",
"type": {
"array": [
"u8",
3
2
]
}
},
@ -7065,6 +7158,29 @@ export type MangoV4 = {
},
{
"name": "TokenForceCloseBorrowsWithToken"
},
{
"name": "PerpForceClosePosition"
},
{
"name": "GroupWithdrawInsuranceFund"
}
]
}
},
{
"name": "CheckLiquidatable",
"type": {
"kind": "enum",
"variants": [
{
"name": "NotLiquidatable"
},
{
"name": "Liquidatable"
},
{
"name": "BecameNotLiquidatable"
}
]
}
@ -8561,6 +8677,101 @@ export type MangoV4 = {
"index": false
}
]
},
{
"name": "PerpForceClosePositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "accountA",
"type": "publicKey",
"index": false
},
{
"name": "accountB",
"type": "publicKey",
"index": false
},
{
"name": "baseTransfer",
"type": "i64",
"index": false
},
{
"name": "quoteTransfer",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "TokenForceCloseBorrowsWithTokenLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "assetTokenIndex",
"type": "u16",
"index": false
},
{
"name": "liabTokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetTransfer",
"type": "i128",
"index": false
},
{
"name": "liabTransfer",
"type": "i128",
"index": false
},
{
"name": "assetPrice",
"type": "i128",
"index": false
},
{
"name": "liabPrice",
"type": "i128",
"index": false
},
{
"name": "feeFactor",
"type": "i128",
"index": false
}
]
}
],
"errors": [
@ -8803,7 +9014,7 @@ export type MangoV4 = {
};
export const IDL: MangoV4 = {
"version": "0.13.0",
"version": "0.14.0",
"name": "mango_v4",
"instructions": [
{
@ -8981,6 +9192,42 @@ export const IDL: MangoV4 = {
}
]
},
{
"name": "groupWithdrawInsuranceFund",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "admin",
"isMut": false,
"isSigner": true
},
{
"name": "insuranceVault",
"isMut": true,
"isSigner": false
},
{
"name": "destination",
"isMut": true,
"isSigner": false
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "amount",
"type": "u64"
}
]
},
{
"name": "ixGateSet",
"accounts": [
@ -10547,6 +10794,12 @@ export const IDL: MangoV4 = {
"type": {
"option": "bool"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -11896,6 +12149,12 @@ export const IDL: MangoV4 = {
"type": {
"option": "string"
}
},
{
"name": "forceCloseOpt",
"type": {
"option": "bool"
}
}
]
},
@ -12439,6 +12698,37 @@ export const IDL: MangoV4 = {
],
"args": []
},
{
"name": "perpForceClosePosition",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "perpMarket",
"isMut": true,
"isSigner": false
},
{
"name": "accountA",
"isMut": true,
"isSigner": false
},
{
"name": "accountB",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "perpSettleFees",
"accounts": [
@ -12728,7 +13018,13 @@ export const IDL: MangoV4 = {
"benchmark",
""
],
"accounts": [],
"accounts": [
{
"name": "dummy",
"isMut": false,
"isSigner": false
}
],
"args": []
}
],
@ -13852,12 +14148,16 @@ export const IDL: MangoV4 = {
],
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding4",
"type": {
"array": [
"u8",
7
6
]
}
},
@ -13915,12 +14215,16 @@ export const IDL: MangoV4 = {
"name": "reduceOnly",
"type": "u8"
},
{
"name": "forceClose",
"type": "u8"
},
{
"name": "padding1",
"type": {
"array": [
"u8",
3
2
]
}
},
@ -15869,6 +16173,29 @@ export const IDL: MangoV4 = {
},
{
"name": "TokenForceCloseBorrowsWithToken"
},
{
"name": "PerpForceClosePosition"
},
{
"name": "GroupWithdrawInsuranceFund"
}
]
}
},
{
"name": "CheckLiquidatable",
"type": {
"kind": "enum",
"variants": [
{
"name": "NotLiquidatable"
},
{
"name": "Liquidatable"
},
{
"name": "BecameNotLiquidatable"
}
]
}
@ -17365,6 +17692,101 @@ export const IDL: MangoV4 = {
"index": false
}
]
},
{
"name": "PerpForceClosePositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "perpMarketIndex",
"type": "u16",
"index": false
},
{
"name": "accountA",
"type": "publicKey",
"index": false
},
{
"name": "accountB",
"type": "publicKey",
"index": false
},
{
"name": "baseTransfer",
"type": "i64",
"index": false
},
{
"name": "quoteTransfer",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
{
"name": "TokenForceCloseBorrowsWithTokenLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "liqor",
"type": "publicKey",
"index": false
},
{
"name": "liqee",
"type": "publicKey",
"index": false
},
{
"name": "assetTokenIndex",
"type": "u16",
"index": false
},
{
"name": "liabTokenIndex",
"type": "u16",
"index": false
},
{
"name": "assetTransfer",
"type": "i128",
"index": false
},
{
"name": "liabTransfer",
"type": "i128",
"index": false
},
{
"name": "assetPrice",
"type": "i128",
"index": false
},
{
"name": "liabPrice",
"type": "i128",
"index": false
},
{
"name": "feeFactor",
"type": "i128",
"index": false
}
]
}
],
"errors": [

410
ts/client/src/router.ts Normal file
View File

@ -0,0 +1,410 @@
import {
AccountInfo,
AddressLookupTableAccount,
Connection,
PublicKey,
TransactionInstruction,
TransactionMessage,
VersionedTransaction,
} from '@solana/web3.js';
import fetch from 'node-fetch';
import { createAssociatedTokenAccountIdempotentInstruction } from './utils';
export const MANGO_ROUTER_API_URL = 'https://api.mngo.cloud/router/v1';
export interface QuoteParams {
sourceMint: string;
destinationMint: string;
amount: number;
swapMode: SwapMode;
}
export declare type TokenMintAddress = string;
export interface Quote {
notEnoughLiquidity: boolean;
minInAmount?: number;
minOutAmount?: number;
inAmount: number;
outAmount: number;
feeAmount: number;
feeMint: TokenMintAddress;
feePct: number;
priceImpactPct: number;
}
export declare type QuoteMintToReferrer = Map<TokenMintAddress, string>;
export interface SwapParams {
sourceMint: string;
destinationMint: string;
userSourceTokenAccount: string;
userDestinationTokenAccount: string;
userTransferAuthority: string;
/**
* amount is used for instruction and can be null when it is an intermediate swap, only the first swap has an amount
*/
amount: number;
swapMode: SwapMode;
openOrdersAddress?: string;
quoteMintToReferrer?: QuoteMintToReferrer;
}
export declare type PlatformFee = {
feeBps: number;
feeAccount: string;
};
export interface ExactOutSwapParams extends SwapParams {
inAmount: number;
slippageBps: number;
platformFee?: PlatformFee;
overflowFeeAccount?: string;
}
export declare type AccountInfoMap = Map<string, AccountInfo<Buffer> | null>;
export declare type AmmLabel =
| 'Aldrin'
| 'Crema'
| 'Cropper'
| 'Cykura'
| 'DeltaFi'
| 'GooseFX'
| 'Invariant'
| 'Lifinity'
| 'Lifinity V2'
| 'Marinade'
| 'Mercurial'
| 'Meteora'
| 'Raydium'
| 'Raydium CLMM'
| 'Saber'
| 'Serum'
| 'Orca'
| 'Step'
| 'Penguin'
| 'Saros'
| 'Stepn'
| 'Orca (Whirlpools)'
| 'Sencha'
| 'Saber (Decimals)'
| 'Dradex'
| 'Balansol'
| 'Openbook'
| 'Unknown';
export interface TransactionFeeInfo {
signatureFee: number;
openOrdersDeposits: number[];
ataDeposits: number[];
totalFeeAndDeposits: number;
minimumSOLForTransaction: number;
}
export declare enum SwapMode {
ExactIn = 'ExactIn',
ExactOut = 'ExactOut',
}
export interface Fee {
amount: number;
mint: string;
pct: number;
}
export interface MarketInfo {
id: string;
inAmount: number;
inputMint: string;
label: string;
lpFee: Fee;
notEnoughLiquidity: boolean;
outAmount: number;
outputMint: string;
platformFee: Fee;
priceImpactPct: number;
}
export interface RouteInfo {
amount: number;
inAmount: number;
marketInfos: MarketInfo[];
otherAmountThreshold: number;
outAmount: number;
priceImpactPct: number;
slippageBps: number;
swapMode: SwapMode;
instructions?: TransactionInstruction[];
mints?: PublicKey[];
routerName?: 'Mango';
}
export type Routes = {
routes: RouteInfo[];
bestRoute: RouteInfo | null;
};
export type Token = {
address: string;
chainId: number;
decimals: number;
name: string;
symbol: string;
logoURI: string;
extensions: {
coingeckoId?: string;
};
tags: string[];
};
const fetchJupiterRoutes = async (
inputMint,
outputMint,
amount = '0',
slippage = 50,
swapMode = 'ExactIn',
feeBps = '0',
): Promise<Routes> => {
{
const paramsString = new URLSearchParams({
inputMint: inputMint.toString(),
outputMint: outputMint.toString(),
amount: amount.toString(),
slippageBps: Math.ceil(slippage * 100).toString(),
feeBps: feeBps.toString(),
swapMode,
}).toString();
const response = await fetch(
`https://quote-api.jup.ag/v4/quote?${paramsString}`,
);
const res = await response.json();
const data = res.data;
return {
routes: res.data as RouteInfo[],
bestRoute: (data.length ? data[0] : null) as RouteInfo | null,
};
}
};
const fetchMangoRoutes = async (
inputMint,
outputMint,
amount = '0',
slippage = 50,
swapMode = 'ExactIn',
feeBps = '0',
wallet = PublicKey.default,
): Promise<Routes> => {
{
const defaultOtherAmount =
swapMode === 'ExactIn' ? 0 : Number.MAX_SAFE_INTEGER;
const paramsString = new URLSearchParams({
inputMint: inputMint.toString(),
outputMint: outputMint.toString(),
amount: amount.toString(),
slippage: ((slippage * 1) / 100).toString(),
feeBps: feeBps.toString(),
mode: swapMode,
wallet: wallet.toString(),
otherAmountThreshold: defaultOtherAmount.toString(),
}).toString();
const response = await fetch(
`${MANGO_ROUTER_API_URL}/swap?${paramsString}`,
);
const res = await response.json();
const data: RouteInfo[] = res.map((route: any) => ({
...route,
priceImpactPct: route.priceImpact,
slippageBps: slippage,
marketInfos: route.marketInfos.map((mInfo: any) => ({
...mInfo,
lpFee: {
...mInfo.fee,
pct: mInfo.fee.rate,
},
})),
mints: route.mints.map((x: string) => new PublicKey(x)),
instructions: route.instructions.map((ix: any) => ({
...ix,
programId: new PublicKey(ix.programId),
data: Buffer.from(ix.data, 'base64'),
keys: ix.keys.map((key: any) => ({
...key,
pubkey: new PublicKey(key.pubkey),
})),
})),
routerName: 'Mango',
}));
return {
routes: data,
bestRoute: (data.length ? data[0] : null) as RouteInfo | null,
};
}
};
export const fetchRoutes = async (
inputMint,
outputMint,
amount = '0',
slippage = 50,
swapMode = 'ExactIn',
feeBps = '0',
wallet = PublicKey.default,
): Promise<Routes> => {
try {
const responses = await Promise.allSettled([
fetchMangoRoutes(
inputMint,
outputMint,
amount,
slippage,
swapMode,
feeBps,
wallet,
),
fetchJupiterRoutes(
inputMint,
outputMint,
amount,
slippage,
swapMode,
feeBps,
),
]);
const routes: RouteInfo[] = responses
.filter((x) => x.status === 'fulfilled' && x.value.bestRoute !== null)
.map((x) => (x as any).value.routes)
.flat();
const sortedBestQuoteFirst = routes.sort(
(a, b) =>
swapMode == 'ExactIn'
? Number(b.outAmount) - Number(a.outAmount) // biggest out
: Number(a.inAmount) - Number(b.inAmount), // smallest in
);
return {
routes: sortedBestQuoteFirst,
bestRoute: sortedBestQuoteFirst[0],
};
} catch (e) {
return {
routes: [],
bestRoute: null,
};
}
};
export const prepareMangoRouterInstructions = async (
selectedRoute: RouteInfo,
inputMint: PublicKey,
outputMint: PublicKey,
userPublicKey: PublicKey,
): Promise<[TransactionInstruction[], AddressLookupTableAccount[]]> => {
if (!selectedRoute || !selectedRoute.mints || !selectedRoute.instructions) {
return [[], []];
}
const mintsToFilterOut = [inputMint, outputMint];
const filteredOutMints = [
...selectedRoute.mints.filter(
(routeMint) =>
!mintsToFilterOut.find((filterOutMint) =>
filterOutMint.equals(routeMint),
),
),
];
const additionalInstructions: TransactionInstruction[] = [];
for (const mint of filteredOutMints) {
const ix = await createAssociatedTokenAccountIdempotentInstruction(
userPublicKey,
userPublicKey,
mint,
);
additionalInstructions.push(ix);
}
const instructions = [
...additionalInstructions,
...selectedRoute.instructions,
];
return [instructions, []];
};
const deserializeJupiterIxAndAlt = async (
connection: Connection,
swapTransaction: string,
): Promise<[TransactionInstruction[], AddressLookupTableAccount[]]> => {
const parsedSwapTransaction = VersionedTransaction.deserialize(
Buffer.from(swapTransaction, 'base64'),
);
const message = parsedSwapTransaction.message;
// const lookups = message.addressTableLookups
const addressLookupTablesResponses = await Promise.all(
message.addressTableLookups.map((alt) =>
connection.getAddressLookupTable(alt.accountKey),
),
);
const addressLookupTables: AddressLookupTableAccount[] =
addressLookupTablesResponses
.map((alt) => alt.value)
.filter((x): x is AddressLookupTableAccount => x !== null);
const decompiledMessage = TransactionMessage.decompile(message, {
addressLookupTableAccounts: addressLookupTables,
});
return [decompiledMessage.instructions, addressLookupTables];
};
export const fetchJupiterTransaction = async (
connection: Connection,
selectedRoute: RouteInfo,
userPublicKey: PublicKey,
slippage: number,
inputMint: PublicKey,
outputMint: PublicKey,
): Promise<[TransactionInstruction[], AddressLookupTableAccount[]]> => {
const transactions = await (
await fetch('https://quote-api.jup.ag/v4/swap', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
// route from /quote api
route: selectedRoute,
// user public key to be used for the swap
userPublicKey,
// feeAccount is optional. Use if you want to charge a fee. feeBps must have been passed in /quote API.
// This is the ATA account for the output token where the fee will be sent to. If you are swapping from SOL->USDC then this would be the USDC ATA you want to collect the fee.
// feeAccount: 'fee_account_public_key',
slippageBps: Math.ceil(slippage * 100),
}),
})
).json();
const { swapTransaction } = transactions;
const [ixs, alts] = await deserializeJupiterIxAndAlt(
connection,
swapTransaction,
);
const isSetupIx = (pk: PublicKey): boolean =>
pk.toString() === 'ComputeBudget111111111111111111111111111111' ||
pk.toString() === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
const isDuplicateAta = (ix: TransactionInstruction): boolean => {
return (
ix.programId.toString() ===
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL' &&
(ix.keys[3].pubkey.toString() === inputMint.toString() ||
ix.keys[3].pubkey.toString() === outputMint.toString())
);
};
const filtered_jup_ixs = ixs
.filter((ix) => !isSetupIx(ix.programId))
.filter((ix) => !isDuplicateAta(ix));
return [filtered_jup_ixs, alts];
};

View File

@ -9,6 +9,8 @@
},
"include": [
"ts/client/src",
"ts/client/scripts"
"ts/client/scripts",
"ts/client/scripts/mm",
"ts/client/scripts/keeper"
]
}