Various force close bots (#554)
* wip: force close perp positions Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * wip: force close cancel serum3 orders Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * wip: force close token borrows Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
1bf1a8deb5
commit
2305a160d0
|
@ -23,7 +23,3 @@ solana --url https://mango.devnet.rpcpool.com program deploy --program-id $PROGR
|
||||||
# publish idl
|
# publish idl
|
||||||
cargo run -p anchor-cli -- idl upgrade --provider.cluster https://mango.devnet.rpcpool.com --provider.wallet $WALLET_WITH_FUNDS \
|
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
|
--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
|
// https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/serum.json#L70
|
||||||
const DEVNET_SERUM3_MARKETS = new Map([
|
const DEVNET_SERUM3_MARKETS = new Map([
|
||||||
['SOL/USDC', '82iPEvGiTceyxYpeLK3DhSwga3R5m4Yfyoydd13CukQ9'],
|
['SOL/USDC', '6xYbSQyhajUqyatJDdkonpj7v41bKeEBWpf7kwRh5X7A'],
|
||||||
]);
|
]);
|
||||||
const DEVNET_MINTS = new Map([
|
const DEVNET_MINTS = new Map([
|
||||||
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
||||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||||
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
|
|
||||||
]);
|
]);
|
||||||
const DEVNET_ORACLES = new Map([
|
const DEVNET_ORACLES = new Map([
|
||||||
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
|
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
|
||||||
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
|
|
||||||
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
|
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
|
||||||
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
|
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
|
||||||
]);
|
]);
|
||||||
|
@ -180,54 +178,32 @@ async function main() {
|
||||||
console.log(
|
console.log(
|
||||||
`...edited group, https://explorer.solana.com/tx/${sig}?cluster=devnet`,
|
`...edited group, https://explorer.solana.com/tx/${sig}?cluster=devnet`,
|
||||||
);
|
);
|
||||||
console.log(`Registering MNGO...`);
|
|
||||||
const mngoDevnetMint = new PublicKey(DEVNET_MINTS.get('MNGO')!);
|
// register serum market
|
||||||
const mngoDevnetOracle = new PublicKey(DEVNET_ORACLES.get('MNGO')!);
|
console.log(`Registering serum3 market...`);
|
||||||
|
const serumMarketExternalPk = new PublicKey(
|
||||||
|
DEVNET_SERUM3_MARKETS.get('SOL/USDC')!,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
sig = await client.tokenRegisterTrustless(
|
sig = await client.serum3RegisterMarket(
|
||||||
group,
|
group,
|
||||||
mngoDevnetMint,
|
serumMarketExternalPk,
|
||||||
mngoDevnetOracle,
|
group.getFirstBankByMint(solDevnetMint),
|
||||||
2,
|
group.getFirstBankByMint(usdcDevnetMint),
|
||||||
'MNGO',
|
0,
|
||||||
|
'SOL/USDC',
|
||||||
);
|
);
|
||||||
await group.reloadAll(client);
|
await group.reloadAll(client);
|
||||||
const bank = group.getFirstBankByMint(mngoDevnetMint);
|
const serum3Market = group.getSerum3MarketByExternalMarket(
|
||||||
|
serumMarketExternalPk,
|
||||||
|
);
|
||||||
console.log(
|
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) {
|
} catch (error) {
|
||||||
console.log(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
|
// register perp market
|
||||||
console.log(`Registering perp market...`);
|
console.log(`Registering perp market...`);
|
||||||
try {
|
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();
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,16 @@ import { expect } from 'chai';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { Group } from '../../src/accounts/group';
|
import { Group } from '../../src/accounts/group';
|
||||||
import { HealthType } from '../../src/accounts/mangoAccount';
|
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 { MangoClient } from '../../src/client';
|
||||||
import { MANGO_V4_ID } from '../../src/constants';
|
import { MANGO_V4_ID } from '../../src/constants';
|
||||||
import { toUiDecimalsForQuote } from '../../src/utils';
|
import { toUiDecimalsForQuote } from '../../src/utils';
|
||||||
|
@ -22,17 +31,14 @@ const DEVNET_MINTS = new Map([
|
||||||
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
|
||||||
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
|
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
|
||||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||||
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
|
|
||||||
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
|
|
||||||
]);
|
]);
|
||||||
export const DEVNET_SERUM3_MARKETS = new Map([
|
export const DEVNET_SERUM3_MARKETS = new Map([
|
||||||
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
|
['SOL/USDC', new PublicKey('6xYbSQyhajUqyatJDdkonpj7v41bKeEBWpf7kwRh5X7A')],
|
||||||
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
|
||||||
|
|
||||||
async function main() {
|
async function main(): Promise<void> {
|
||||||
const options = AnchorProvider.defaultOptions();
|
const options = AnchorProvider.defaultOptions();
|
||||||
const connection = new Connection(
|
const connection = new Connection(
|
||||||
'https://mango.devnet.rpcpool.com',
|
'https://mango.devnet.rpcpool.com',
|
||||||
|
|
|
@ -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();
|
|
@ -45,7 +45,7 @@ async function computePriceImpact(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main(): Promise<void> {
|
||||||
const client = await buildClient();
|
const client = await buildClient();
|
||||||
const group = await client.getGroup(new PublicKey(GROUP_PK));
|
const group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||||
await group.reloadAll(client);
|
await group.reloadAll(client);
|
||||||
|
@ -57,9 +57,6 @@ async function main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const bank of Array.from(group.banksMapByMint.values())) {
|
for (const bank of Array.from(group.banksMapByMint.values())) {
|
||||||
if (bank[0].name === 'USDC' || bank[0].reduceOnly === true) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const usdcMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
const usdcMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||||
|
|
||||||
const pi1 = await computePriceImpact(
|
const pi1 = await computePriceImpact(
|
||||||
|
|
|
@ -420,6 +420,54 @@ export class MangoClient {
|
||||||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
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(
|
||||||
|
AccountRetriever.Scanning,
|
||||||
|
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(
|
public async tokenDeregister(
|
||||||
group: Group,
|
group: Group,
|
||||||
mintPk: PublicKey,
|
mintPk: PublicKey,
|
||||||
|
@ -1247,6 +1295,25 @@ export class MangoClient {
|
||||||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
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(
|
public async serum3deregisterMarket(
|
||||||
group: Group,
|
group: Group,
|
||||||
externalMarketPk: PublicKey,
|
externalMarketPk: PublicKey,
|
||||||
|
@ -1416,6 +1483,72 @@ 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(
|
||||||
|
AccountRetriever.Fixed,
|
||||||
|
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(
|
public async serum3PlaceOrderIx(
|
||||||
group: Group,
|
group: Group,
|
||||||
mangoAccount: MangoAccount,
|
mangoAccount: MangoAccount,
|
||||||
|
@ -1992,6 +2125,27 @@ export class MangoClient {
|
||||||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
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(
|
public async perpCloseMarket(
|
||||||
group: Group,
|
group: Group,
|
||||||
perpMarketIndex: PerpMarketIndex,
|
perpMarketIndex: PerpMarketIndex,
|
||||||
|
|
|
@ -20,6 +20,7 @@ export {
|
||||||
} from './clientIxParamBuilder';
|
} from './clientIxParamBuilder';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
export * from './numbers/I80F48';
|
export * from './numbers/I80F48';
|
||||||
export * from './utils';
|
export * from './router';
|
||||||
export * from './types';
|
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 = {
|
export type MangoV4 = {
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"name": "mango_v4",
|
"name": "mango_v4",
|
||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
|
@ -1743,6 +1743,12 @@ export type MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"option": "bool"
|
"option": "bool"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "forceCloseOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "bool"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -5152,12 +5158,16 @@ export type MangoV4 = {
|
||||||
"name": "reduceOnly",
|
"name": "reduceOnly",
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "forceClose",
|
||||||
|
"type": "u8"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "padding1",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
3
|
2
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -7113,6 +7123,23 @@ export type MangoV4 = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "CheckLiquidatable",
|
||||||
|
"type": {
|
||||||
|
"kind": "enum",
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"name": "NotLiquidatable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Liquidatable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BecameNotLiquidatable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "OracleType",
|
"name": "OracleType",
|
||||||
"type": {
|
"type": {
|
||||||
|
@ -8847,7 +8874,7 @@ export type MangoV4 = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IDL: MangoV4 = {
|
export const IDL: MangoV4 = {
|
||||||
"version": "0.13.0",
|
"version": "0.14.0",
|
||||||
"name": "mango_v4",
|
"name": "mango_v4",
|
||||||
"instructions": [
|
"instructions": [
|
||||||
{
|
{
|
||||||
|
@ -10591,6 +10618,12 @@ export const IDL: MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"option": "bool"
|
"option": "bool"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "forceCloseOpt",
|
||||||
|
"type": {
|
||||||
|
"option": "bool"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -14000,12 +14033,16 @@ export const IDL: MangoV4 = {
|
||||||
"name": "reduceOnly",
|
"name": "reduceOnly",
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "forceClose",
|
||||||
|
"type": "u8"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "padding1",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
3
|
2
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -15961,6 +15998,23 @@ export const IDL: MangoV4 = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "CheckLiquidatable",
|
||||||
|
"type": {
|
||||||
|
"kind": "enum",
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"name": "NotLiquidatable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Liquidatable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BecameNotLiquidatable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "OracleType",
|
"name": "OracleType",
|
||||||
"type": {
|
"type": {
|
||||||
|
|
|
@ -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];
|
||||||
|
};
|
Loading…
Reference in New Issue