Merge tag 'program-v0.14.0' into deploy
This commit is contained in:
commit
514f861e2f
|
@ -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:
|
||||
|
|
|
@ -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.
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -3005,7 +3005,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "mango-v4"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
dependencies = [
|
||||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -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
|
||||
|
|
4
Procfile
4
Procfile
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
219
mango_v4.json
219
mango_v4.json
|
@ -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": [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "mango-v4"
|
||||
version = "0.13.0"
|
||||
version = "0.14.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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()?,
|
||||
|
|
|
@ -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(());
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(());
|
||||
}
|
||||
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -364,6 +364,8 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> {
|
|||
group,
|
||||
admin,
|
||||
perp_market,
|
||||
reduce_only: true,
|
||||
force_close: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -322,6 +322,7 @@ async function perpEdit(): Promise<void> {
|
|||
params.resetStablePrice ?? false,
|
||||
params.positivePnlLiquidationFee,
|
||||
params.name,
|
||||
params.forceClose,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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();
|
|
@ -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();
|
|
@ -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,
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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];
|
||||
};
|
|
@ -9,6 +9,8 @@
|
|||
},
|
||||
"include": [
|
||||
"ts/client/src",
|
||||
"ts/client/scripts"
|
||||
"ts/client/scripts",
|
||||
"ts/client/scripts/mm",
|
||||
"ts/client/scripts/keeper"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue