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
|
||||
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',
|
||||
|
|
|
@ -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 group = await client.getGroup(new PublicKey(GROUP_PK));
|
||||
await group.reloadAll(client);
|
||||
|
@ -57,9 +57,6 @@ async function main() {
|
|||
);
|
||||
|
||||
for (const bank of Array.from(group.banksMapByMint.values())) {
|
||||
if (bank[0].name === 'USDC' || bank[0].reduceOnly === true) {
|
||||
continue;
|
||||
}
|
||||
const usdcMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||
|
||||
const pi1 = await computePriceImpact(
|
||||
|
|
|
@ -420,6 +420,54 @@ 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(
|
||||
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(
|
||||
group: Group,
|
||||
mintPk: PublicKey,
|
||||
|
@ -1247,6 +1295,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,
|
||||
|
@ -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(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
|
@ -1992,6 +2125,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,
|
||||
|
|
|
@ -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": [
|
||||
{
|
||||
|
@ -1743,6 +1743,12 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceCloseOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -5152,12 +5158,16 @@ export type MangoV4 = {
|
|||
"name": "reduceOnly",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "forceClose",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding1",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
3
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -7113,6 +7123,23 @@ export type MangoV4 = {
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CheckLiquidatable",
|
||||
"type": {
|
||||
"kind": "enum",
|
||||
"variants": [
|
||||
{
|
||||
"name": "NotLiquidatable"
|
||||
},
|
||||
{
|
||||
"name": "Liquidatable"
|
||||
},
|
||||
{
|
||||
"name": "BecameNotLiquidatable"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OracleType",
|
||||
"type": {
|
||||
|
@ -8847,7 +8874,7 @@ export type MangoV4 = {
|
|||
};
|
||||
|
||||
export const IDL: MangoV4 = {
|
||||
"version": "0.13.0",
|
||||
"version": "0.14.0",
|
||||
"name": "mango_v4",
|
||||
"instructions": [
|
||||
{
|
||||
|
@ -10591,6 +10618,12 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "forceCloseOpt",
|
||||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -14000,12 +14033,16 @@ export const IDL: MangoV4 = {
|
|||
"name": "reduceOnly",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "forceClose",
|
||||
"type": "u8"
|
||||
},
|
||||
{
|
||||
"name": "padding1",
|
||||
"type": {
|
||||
"array": [
|
||||
"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",
|
||||
"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